feat: add grid trading calculations
This commit is contained in:
1
src/grid_trading/domain/__init__.py
Normal file
1
src/grid_trading/domain/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Domain models and calculations for grid trading."""
|
||||||
217
src/grid_trading/domain/calculations.py
Normal file
217
src/grid_trading/domain/calculations.py
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
|
from typing import Iterable, Mapping, Sequence
|
||||||
|
|
||||||
|
from grid_trading.domain.models import (
|
||||||
|
Account,
|
||||||
|
AccountSummary,
|
||||||
|
CashLedgerEntry,
|
||||||
|
FeeEstimate,
|
||||||
|
FeeRules,
|
||||||
|
Instrument,
|
||||||
|
PositionSummary,
|
||||||
|
Trade,
|
||||||
|
TradeGroup,
|
||||||
|
TradeSide,
|
||||||
|
)
|
||||||
|
|
||||||
|
MONEY_PLACES = Decimal("0.01")
|
||||||
|
RATE_PLACES = Decimal("0.0001")
|
||||||
|
|
||||||
|
|
||||||
|
class CalculationError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _GroupState:
|
||||||
|
quantity: int = 0
|
||||||
|
cost: Decimal = Decimal("0")
|
||||||
|
realized_pnl: Decimal = Decimal("0")
|
||||||
|
|
||||||
|
|
||||||
|
def money(value: Decimal) -> Decimal:
|
||||||
|
return value.quantize(MONEY_PLACES, rounding=ROUND_HALF_UP)
|
||||||
|
|
||||||
|
|
||||||
|
def price(value: Decimal) -> Decimal:
|
||||||
|
return value.quantize(MONEY_PLACES, rounding=ROUND_HALF_UP)
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_fees(
|
||||||
|
side: TradeSide,
|
||||||
|
trade_price: Decimal,
|
||||||
|
quantity: int,
|
||||||
|
rules: FeeRules,
|
||||||
|
) -> FeeEstimate:
|
||||||
|
gross = trade_price * Decimal(quantity)
|
||||||
|
commission = max(gross * rules.commission_rate, rules.min_commission)
|
||||||
|
stamp_tax = gross * rules.stamp_tax_rate if side is TradeSide.SELL else Decimal("0")
|
||||||
|
transfer_fee = gross * rules.transfer_fee_rate
|
||||||
|
return FeeEstimate(
|
||||||
|
commission=money(commission),
|
||||||
|
stamp_tax=money(stamp_tax),
|
||||||
|
transfer_fee=money(transfer_fee),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_positions(
|
||||||
|
instruments: Sequence[Instrument],
|
||||||
|
trades: Sequence[Trade],
|
||||||
|
*,
|
||||||
|
as_of: date,
|
||||||
|
instrument_cashflows: Mapping[int, Decimal] | None = None,
|
||||||
|
) -> list[PositionSummary]:
|
||||||
|
cashflows = instrument_cashflows or {}
|
||||||
|
instrument_by_id = {instrument.id: instrument for instrument in instruments if instrument.id is not None}
|
||||||
|
states: dict[int, dict[TradeGroup, _GroupState]] = defaultdict(
|
||||||
|
lambda: {group: _GroupState() for group in TradeGroup}
|
||||||
|
)
|
||||||
|
net_invested: dict[int, Decimal] = defaultdict(lambda: Decimal("0"))
|
||||||
|
last_trade_price: dict[int, Decimal] = {}
|
||||||
|
today_buys: dict[int, int] = defaultdict(int)
|
||||||
|
|
||||||
|
for trade in sorted(trades, key=lambda item: (item.trade_date, item.id or 0)):
|
||||||
|
if trade.trade_date > as_of:
|
||||||
|
continue
|
||||||
|
_validate_trade(trade)
|
||||||
|
instrument = instrument_by_id.get(trade.instrument_id)
|
||||||
|
if instrument is None:
|
||||||
|
raise CalculationError(f"Trade references unknown instrument {trade.instrument_id}")
|
||||||
|
|
||||||
|
group_state = states[trade.instrument_id][trade.trade_group]
|
||||||
|
gross = money(trade.gross_amount)
|
||||||
|
fees = money(trade.total_fee)
|
||||||
|
last_trade_price[trade.instrument_id] = trade.price
|
||||||
|
|
||||||
|
if trade.side is TradeSide.BUY:
|
||||||
|
group_state.quantity += trade.quantity
|
||||||
|
group_state.cost = money(group_state.cost + gross + fees)
|
||||||
|
net_invested[trade.instrument_id] = money(net_invested[trade.instrument_id] + gross + fees)
|
||||||
|
if trade.trade_date == as_of:
|
||||||
|
today_buys[trade.instrument_id] += trade.quantity
|
||||||
|
else:
|
||||||
|
if trade.quantity > group_state.quantity:
|
||||||
|
raise CalculationError(
|
||||||
|
f"Insufficient {trade.trade_group.value} position for {instrument.code}"
|
||||||
|
)
|
||||||
|
average_cost = group_state.cost / Decimal(group_state.quantity)
|
||||||
|
released_cost = money(average_cost * Decimal(trade.quantity))
|
||||||
|
net_income = money(gross - fees)
|
||||||
|
realized = money(net_income - released_cost)
|
||||||
|
group_state.quantity -= trade.quantity
|
||||||
|
group_state.cost = money(group_state.cost - released_cost)
|
||||||
|
group_state.realized_pnl = money(group_state.realized_pnl + realized)
|
||||||
|
net_invested[trade.instrument_id] = money(net_invested[trade.instrument_id] - net_income)
|
||||||
|
|
||||||
|
summaries: list[PositionSummary] = []
|
||||||
|
for instrument in instruments:
|
||||||
|
if instrument.id is None:
|
||||||
|
continue
|
||||||
|
group_states = states[instrument.id]
|
||||||
|
base_quantity = group_states[TradeGroup.BASE].quantity
|
||||||
|
grid_quantity = group_states[TradeGroup.GRID].quantity
|
||||||
|
other_quantity = group_states[TradeGroup.OTHER].quantity
|
||||||
|
total_quantity = base_quantity + grid_quantity + other_quantity
|
||||||
|
remaining_cost = money(sum((state.cost for state in group_states.values()), Decimal("0")))
|
||||||
|
realized_pnl = money(sum((state.realized_pnl for state in group_states.values()), Decimal("0")))
|
||||||
|
grid_profit = money(group_states[TradeGroup.GRID].realized_pnl)
|
||||||
|
current_price, price_source = _resolve_current_price(instrument, last_trade_price.get(instrument.id))
|
||||||
|
market_value = money(current_price * Decimal(total_quantity)) if current_price is not None else None
|
||||||
|
floating_pnl = money(market_value - remaining_cost) if market_value is not None else None
|
||||||
|
available_quantity = max(0, total_quantity - today_buys[instrument.id])
|
||||||
|
adjusted_net_invested = money(net_invested[instrument.id] - cashflows.get(instrument.id, Decimal("0")))
|
||||||
|
|
||||||
|
summaries.append(
|
||||||
|
PositionSummary(
|
||||||
|
instrument_id=instrument.id,
|
||||||
|
code=instrument.code,
|
||||||
|
name=instrument.name,
|
||||||
|
market=instrument.market,
|
||||||
|
current_price=current_price,
|
||||||
|
price_source=price_source,
|
||||||
|
total_quantity=total_quantity,
|
||||||
|
available_quantity=available_quantity,
|
||||||
|
base_quantity=base_quantity,
|
||||||
|
grid_quantity=grid_quantity,
|
||||||
|
other_quantity=other_quantity,
|
||||||
|
remaining_cost=remaining_cost,
|
||||||
|
cost_price=_per_share(remaining_cost, total_quantity),
|
||||||
|
position_breakeven_price=_per_share(remaining_cost - grid_profit, total_quantity),
|
||||||
|
account_breakeven_price=_per_share(adjusted_net_invested, total_quantity),
|
||||||
|
realized_pnl=realized_pnl,
|
||||||
|
grid_profit=grid_profit,
|
||||||
|
floating_pnl=floating_pnl,
|
||||||
|
market_value=market_value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return summaries
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_account_summary(
|
||||||
|
account: Account,
|
||||||
|
positions: Iterable[PositionSummary],
|
||||||
|
cash_ledger_entries: Sequence[CashLedgerEntry],
|
||||||
|
trades: Sequence[Trade],
|
||||||
|
) -> AccountSummary:
|
||||||
|
trade_cash = Decimal("0")
|
||||||
|
for trade in trades:
|
||||||
|
gross = money(trade.gross_amount)
|
||||||
|
fees = money(trade.total_fee)
|
||||||
|
if trade.side is TradeSide.BUY:
|
||||||
|
trade_cash -= gross + fees
|
||||||
|
else:
|
||||||
|
trade_cash += gross - fees
|
||||||
|
|
||||||
|
ledger_cash = sum((entry.amount for entry in cash_ledger_entries), Decimal("0"))
|
||||||
|
cash = money(account.initial_cash + trade_cash + ledger_cash)
|
||||||
|
market_value = money(
|
||||||
|
sum((position.market_value or Decimal("0") for position in positions), Decimal("0"))
|
||||||
|
)
|
||||||
|
floating_pnl = money(
|
||||||
|
sum((position.floating_pnl or Decimal("0") for position in positions), Decimal("0"))
|
||||||
|
)
|
||||||
|
total_assets = money(cash + market_value)
|
||||||
|
capital_usage_rate = (
|
||||||
|
(market_value / total_assets).quantize(RATE_PLACES, rounding=ROUND_HALF_UP)
|
||||||
|
if total_assets > 0
|
||||||
|
else Decimal("0")
|
||||||
|
)
|
||||||
|
return AccountSummary(
|
||||||
|
total_assets=total_assets,
|
||||||
|
cash=cash,
|
||||||
|
market_value=market_value,
|
||||||
|
floating_pnl=floating_pnl,
|
||||||
|
capital_usage_rate=capital_usage_rate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_trade(trade: Trade) -> None:
|
||||||
|
if trade.price < 0:
|
||||||
|
raise CalculationError("Trade price cannot be negative")
|
||||||
|
if trade.quantity <= 0:
|
||||||
|
raise CalculationError("Trade quantity must be positive")
|
||||||
|
if trade.total_fee < 0:
|
||||||
|
raise CalculationError("Trade fees cannot be negative")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_current_price(
|
||||||
|
instrument: Instrument,
|
||||||
|
fallback_trade_price: Decimal | None,
|
||||||
|
) -> tuple[Decimal | None, str]:
|
||||||
|
if instrument.manual_price is not None:
|
||||||
|
return instrument.manual_price, "manual"
|
||||||
|
if fallback_trade_price is not None:
|
||||||
|
return fallback_trade_price, "last_trade"
|
||||||
|
return None, "missing"
|
||||||
|
|
||||||
|
|
||||||
|
def _per_share(total: Decimal, quantity: int) -> Decimal | None:
|
||||||
|
if quantity <= 0:
|
||||||
|
return None
|
||||||
|
return price(total / Decimal(quantity))
|
||||||
146
src/grid_trading/domain/models.py
Normal file
146
src/grid_trading/domain/models.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class TradeSide(StrEnum):
|
||||||
|
BUY = "buy"
|
||||||
|
SELL = "sell"
|
||||||
|
|
||||||
|
|
||||||
|
class TradeGroup(StrEnum):
|
||||||
|
BASE = "base"
|
||||||
|
GRID = "grid"
|
||||||
|
OTHER = "other"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Account:
|
||||||
|
id: int | None
|
||||||
|
name: str
|
||||||
|
initial_cash: Decimal
|
||||||
|
notes: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Instrument:
|
||||||
|
id: int | None
|
||||||
|
code: str
|
||||||
|
name: str
|
||||||
|
market: str = "A"
|
||||||
|
lot_size: int = 100
|
||||||
|
manual_price: Decimal | None = None
|
||||||
|
allow_odd_lot: bool = False
|
||||||
|
active: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class StrategyTemplate:
|
||||||
|
id: int | None
|
||||||
|
name: str
|
||||||
|
grid_spacing_pct: Decimal
|
||||||
|
amount_per_grid: Decimal
|
||||||
|
base_target_amount: Decimal
|
||||||
|
max_position_amount: Decimal
|
||||||
|
min_lot: int = 100
|
||||||
|
is_default: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class StrategyOverride:
|
||||||
|
instrument_id: int
|
||||||
|
template_id: int | None = None
|
||||||
|
grid_spacing_pct: Decimal | None = None
|
||||||
|
amount_per_grid: Decimal | None = None
|
||||||
|
base_target_amount: Decimal | None = None
|
||||||
|
max_position_amount: Decimal | None = None
|
||||||
|
min_lot: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Trade:
|
||||||
|
id: int | None
|
||||||
|
account_id: int
|
||||||
|
instrument_id: int
|
||||||
|
trade_date: date
|
||||||
|
side: TradeSide
|
||||||
|
price: Decimal
|
||||||
|
quantity: int
|
||||||
|
commission: Decimal = Decimal("0")
|
||||||
|
stamp_tax: Decimal = Decimal("0")
|
||||||
|
transfer_fee: Decimal = Decimal("0")
|
||||||
|
trade_group: TradeGroup = TradeGroup.GRID
|
||||||
|
notes: str = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gross_amount(self) -> Decimal:
|
||||||
|
return self.price * Decimal(self.quantity)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_fee(self) -> Decimal:
|
||||||
|
return self.commission + self.stamp_tax + self.transfer_fee
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CashLedgerEntry:
|
||||||
|
id: int | None
|
||||||
|
account_id: int
|
||||||
|
entry_date: date
|
||||||
|
amount: Decimal
|
||||||
|
category: str
|
||||||
|
instrument_id: int | None = None
|
||||||
|
notes: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FeeRules:
|
||||||
|
commission_rate: Decimal
|
||||||
|
min_commission: Decimal
|
||||||
|
stamp_tax_rate: Decimal
|
||||||
|
transfer_fee_rate: Decimal
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FeeEstimate:
|
||||||
|
commission: Decimal
|
||||||
|
stamp_tax: Decimal
|
||||||
|
transfer_fee: Decimal
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total(self) -> Decimal:
|
||||||
|
return self.commission + self.stamp_tax + self.transfer_fee
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PositionSummary:
|
||||||
|
instrument_id: int
|
||||||
|
code: str
|
||||||
|
name: str
|
||||||
|
market: str
|
||||||
|
current_price: Decimal | None
|
||||||
|
price_source: str
|
||||||
|
total_quantity: int
|
||||||
|
available_quantity: int
|
||||||
|
base_quantity: int
|
||||||
|
grid_quantity: int
|
||||||
|
other_quantity: int
|
||||||
|
remaining_cost: Decimal
|
||||||
|
cost_price: Decimal | None
|
||||||
|
position_breakeven_price: Decimal | None
|
||||||
|
account_breakeven_price: Decimal | None
|
||||||
|
realized_pnl: Decimal
|
||||||
|
grid_profit: Decimal
|
||||||
|
floating_pnl: Decimal | None
|
||||||
|
market_value: Decimal | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AccountSummary:
|
||||||
|
total_assets: Decimal
|
||||||
|
cash: Decimal
|
||||||
|
market_value: Decimal
|
||||||
|
floating_pnl: Decimal
|
||||||
|
capital_usage_rate: Decimal
|
||||||
156
tests/test_calculations.py
Normal file
156
tests/test_calculations.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
from datetime import date, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from grid_trading.domain.calculations import CalculationError, calculate_positions, estimate_fees
|
||||||
|
from grid_trading.domain.models import FeeRules, Instrument, Trade, TradeGroup, TradeSide
|
||||||
|
|
||||||
|
|
||||||
|
def make_trade(
|
||||||
|
*,
|
||||||
|
trade_id: int,
|
||||||
|
instrument_id: int = 1,
|
||||||
|
trade_date: date,
|
||||||
|
side: TradeSide,
|
||||||
|
price: str,
|
||||||
|
quantity: int,
|
||||||
|
commission: str = "0",
|
||||||
|
stamp_tax: str = "0",
|
||||||
|
transfer_fee: str = "0",
|
||||||
|
trade_group: TradeGroup = TradeGroup.GRID,
|
||||||
|
) -> Trade:
|
||||||
|
return Trade(
|
||||||
|
id=trade_id,
|
||||||
|
account_id=1,
|
||||||
|
instrument_id=instrument_id,
|
||||||
|
trade_date=trade_date,
|
||||||
|
side=side,
|
||||||
|
price=Decimal(price),
|
||||||
|
quantity=quantity,
|
||||||
|
commission=Decimal(commission),
|
||||||
|
stamp_tax=Decimal(stamp_tax),
|
||||||
|
transfer_fee=Decimal(transfer_fee),
|
||||||
|
trade_group=trade_group,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_buy_sell_grid_profit_and_breakeven():
|
||||||
|
today = date(2026, 7, 8)
|
||||||
|
instrument = Instrument(id=1, code="510300", name="沪深300ETF", manual_price=Decimal("9.50"))
|
||||||
|
trades = [
|
||||||
|
make_trade(
|
||||||
|
trade_id=1,
|
||||||
|
trade_date=today - timedelta(days=2),
|
||||||
|
side=TradeSide.BUY,
|
||||||
|
price="10",
|
||||||
|
quantity=100,
|
||||||
|
commission="1",
|
||||||
|
trade_group=TradeGroup.GRID,
|
||||||
|
),
|
||||||
|
make_trade(
|
||||||
|
trade_id=2,
|
||||||
|
trade_date=today - timedelta(days=1),
|
||||||
|
side=TradeSide.SELL,
|
||||||
|
price="11",
|
||||||
|
quantity=100,
|
||||||
|
commission="1",
|
||||||
|
trade_group=TradeGroup.GRID,
|
||||||
|
),
|
||||||
|
make_trade(
|
||||||
|
trade_id=3,
|
||||||
|
trade_date=today,
|
||||||
|
side=TradeSide.BUY,
|
||||||
|
price="9",
|
||||||
|
quantity=100,
|
||||||
|
commission="1",
|
||||||
|
trade_group=TradeGroup.BASE,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
[summary] = calculate_positions([instrument], trades, as_of=today)
|
||||||
|
|
||||||
|
assert summary.total_quantity == 100
|
||||||
|
assert summary.base_quantity == 100
|
||||||
|
assert summary.grid_quantity == 0
|
||||||
|
assert summary.remaining_cost == Decimal("901.00")
|
||||||
|
assert summary.realized_pnl == Decimal("98.00")
|
||||||
|
assert summary.grid_profit == Decimal("98.00")
|
||||||
|
assert summary.cost_price == Decimal("9.01")
|
||||||
|
assert summary.position_breakeven_price == Decimal("8.03")
|
||||||
|
assert summary.account_breakeven_price == Decimal("8.03")
|
||||||
|
assert summary.market_value == Decimal("950.00")
|
||||||
|
assert summary.floating_pnl == Decimal("49.00")
|
||||||
|
|
||||||
|
|
||||||
|
def test_t_plus_one_available_quantity_excludes_today_buys():
|
||||||
|
today = date(2026, 7, 8)
|
||||||
|
instrument = Instrument(id=1, code="600000", name="浦发银行", manual_price=Decimal("10"))
|
||||||
|
trades = [
|
||||||
|
make_trade(
|
||||||
|
trade_id=1,
|
||||||
|
trade_date=today - timedelta(days=1),
|
||||||
|
side=TradeSide.BUY,
|
||||||
|
price="10",
|
||||||
|
quantity=200,
|
||||||
|
trade_group=TradeGroup.BASE,
|
||||||
|
),
|
||||||
|
make_trade(
|
||||||
|
trade_id=2,
|
||||||
|
trade_date=today,
|
||||||
|
side=TradeSide.BUY,
|
||||||
|
price="9.8",
|
||||||
|
quantity=100,
|
||||||
|
trade_group=TradeGroup.BASE,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
[summary] = calculate_positions([instrument], trades, as_of=today)
|
||||||
|
|
||||||
|
assert summary.total_quantity == 300
|
||||||
|
assert summary.available_quantity == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_sell_more_than_group_position_raises():
|
||||||
|
today = date(2026, 7, 8)
|
||||||
|
instrument = Instrument(id=1, code="159915", name="创业板ETF")
|
||||||
|
trades = [
|
||||||
|
make_trade(
|
||||||
|
trade_id=1,
|
||||||
|
trade_date=today - timedelta(days=1),
|
||||||
|
side=TradeSide.BUY,
|
||||||
|
price="2",
|
||||||
|
quantity=100,
|
||||||
|
trade_group=TradeGroup.GRID,
|
||||||
|
),
|
||||||
|
make_trade(
|
||||||
|
trade_id=2,
|
||||||
|
trade_date=today,
|
||||||
|
side=TradeSide.SELL,
|
||||||
|
price="2.1",
|
||||||
|
quantity=200,
|
||||||
|
trade_group=TradeGroup.GRID,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
with pytest.raises(CalculationError, match="Insufficient grid position"):
|
||||||
|
calculate_positions([instrument], trades, as_of=today)
|
||||||
|
|
||||||
|
|
||||||
|
def test_estimate_fees_uses_min_commission_and_sell_tax():
|
||||||
|
rules = FeeRules(
|
||||||
|
commission_rate=Decimal("0.00025"),
|
||||||
|
min_commission=Decimal("5"),
|
||||||
|
stamp_tax_rate=Decimal("0.0005"),
|
||||||
|
transfer_fee_rate=Decimal("0.00001"),
|
||||||
|
)
|
||||||
|
|
||||||
|
buy_fees = estimate_fees(TradeSide.BUY, Decimal("10"), 100, rules)
|
||||||
|
sell_fees = estimate_fees(TradeSide.SELL, Decimal("10"), 100, rules)
|
||||||
|
|
||||||
|
assert buy_fees.commission == Decimal("5.00")
|
||||||
|
assert buy_fees.stamp_tax == Decimal("0.00")
|
||||||
|
assert buy_fees.transfer_fee == Decimal("0.01")
|
||||||
|
assert sell_fees.commission == Decimal("5.00")
|
||||||
|
assert sell_fees.stamp_tax == Decimal("0.50")
|
||||||
|
assert sell_fees.transfer_fee == Decimal("0.01")
|
||||||
Reference in New Issue
Block a user