226 lines
7.0 KiB
Python
226 lines
7.0 KiB
Python
from datetime import date, timedelta
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
|
|
from grid_trading.domain.models import Instrument, QuoteSnapshot, Trade, TradeGroup, TradeSide
|
|
from grid_trading.services.trading_service import TradingService
|
|
|
|
|
|
class FakeQuoteProvider:
|
|
def fetch_quotes(self, instruments):
|
|
return {
|
|
"510300": QuoteSnapshot(
|
|
symbol="sh510300",
|
|
code="510300",
|
|
name="沪深300ETF",
|
|
price=Decimal("4.12"),
|
|
source="tencent",
|
|
quote_time="20260708150000",
|
|
)
|
|
}
|
|
|
|
|
|
def test_service_creates_default_account_and_computes_summary(tmp_path):
|
|
service = TradingService(tmp_path / "grid.db")
|
|
service.ensure_defaults()
|
|
account = service.get_active_account()
|
|
assert account.name == "默认账户"
|
|
|
|
account = service.save_account(account.__class__(id=account.id, name="主账户", initial_cash=Decimal("100000")))
|
|
instrument = service.add_instrument(
|
|
Instrument(id=None, code="510300", name="沪深300ETF", market="ETF", manual_price=Decimal("4.00"))
|
|
)
|
|
service.save_trade(
|
|
Trade(
|
|
id=None,
|
|
account_id=account.id,
|
|
instrument_id=instrument.id,
|
|
trade_date=date(2026, 7, 7),
|
|
side=TradeSide.BUY,
|
|
price=Decimal("3.90"),
|
|
quantity=1000,
|
|
commission=Decimal("5"),
|
|
trade_group=TradeGroup.GRID,
|
|
)
|
|
)
|
|
service.save_trade(
|
|
Trade(
|
|
id=None,
|
|
account_id=account.id,
|
|
instrument_id=instrument.id,
|
|
trade_date=date(2026, 7, 8),
|
|
side=TradeSide.SELL,
|
|
price=Decimal("4.10"),
|
|
quantity=500,
|
|
commission=Decimal("5"),
|
|
stamp_tax=Decimal("1.03"),
|
|
trade_group=TradeGroup.GRID,
|
|
)
|
|
)
|
|
|
|
positions = service.get_position_summaries(as_of=date(2026, 7, 8))
|
|
summary = service.get_account_summary(as_of=date(2026, 7, 8))
|
|
|
|
assert len(positions) == 1
|
|
assert positions[0].total_quantity == 500
|
|
assert positions[0].current_price is None
|
|
assert positions[0].price_source == "missing"
|
|
assert positions[0].grid_profit == Decimal("91.47")
|
|
assert summary.cash == Decimal("98138.97")
|
|
assert summary.market_value == Decimal("0.00")
|
|
assert summary.total_assets == Decimal("98138.97")
|
|
|
|
|
|
def test_service_validates_lot_size_and_available_sell_quantity(tmp_path):
|
|
service = TradingService(tmp_path / "grid.db")
|
|
service.ensure_defaults()
|
|
account = service.get_active_account()
|
|
instrument = service.add_instrument(Instrument(id=None, code="600000", name="浦发银行"))
|
|
|
|
with pytest.raises(ValueError, match="100"):
|
|
service.save_trade(
|
|
Trade(
|
|
id=None,
|
|
account_id=account.id,
|
|
instrument_id=instrument.id,
|
|
trade_date=date(2026, 7, 8),
|
|
side=TradeSide.BUY,
|
|
price=Decimal("10"),
|
|
quantity=50,
|
|
trade_group=TradeGroup.BASE,
|
|
)
|
|
)
|
|
|
|
service.save_trade(
|
|
Trade(
|
|
id=None,
|
|
account_id=account.id,
|
|
instrument_id=instrument.id,
|
|
trade_date=date(2026, 7, 8),
|
|
side=TradeSide.BUY,
|
|
price=Decimal("10"),
|
|
quantity=100,
|
|
trade_group=TradeGroup.BASE,
|
|
)
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="T\\+1"):
|
|
service.save_trade(
|
|
Trade(
|
|
id=None,
|
|
account_id=account.id,
|
|
instrument_id=instrument.id,
|
|
trade_date=date(2026, 7, 8),
|
|
side=TradeSide.SELL,
|
|
price=Decimal("10.1"),
|
|
quantity=100,
|
|
trade_group=TradeGroup.BASE,
|
|
)
|
|
)
|
|
|
|
service.save_trade(
|
|
Trade(
|
|
id=None,
|
|
account_id=account.id,
|
|
instrument_id=instrument.id,
|
|
trade_date=date(2026, 7, 8) - timedelta(days=1),
|
|
side=TradeSide.BUY,
|
|
price=Decimal("9.9"),
|
|
quantity=100,
|
|
trade_group=TradeGroup.BASE,
|
|
)
|
|
)
|
|
saved_sell = service.save_trade(
|
|
Trade(
|
|
id=None,
|
|
account_id=account.id,
|
|
instrument_id=instrument.id,
|
|
trade_date=date(2026, 7, 8),
|
|
side=TradeSide.SELL,
|
|
price=Decimal("10.1"),
|
|
quantity=100,
|
|
trade_group=TradeGroup.BASE,
|
|
)
|
|
)
|
|
|
|
assert saved_sell.id is not None
|
|
|
|
|
|
def test_service_rejects_historical_changes_that_break_future_sells(tmp_path):
|
|
service = TradingService(tmp_path / "grid.db")
|
|
service.ensure_defaults()
|
|
account = service.get_active_account()
|
|
instrument = service.add_instrument(Instrument(id=None, code="159915", name="创业板ETF"))
|
|
buy = service.save_trade(
|
|
Trade(
|
|
id=None,
|
|
account_id=account.id,
|
|
instrument_id=instrument.id,
|
|
trade_date=date(2026, 7, 7),
|
|
side=TradeSide.BUY,
|
|
price=Decimal("2.00"),
|
|
quantity=200,
|
|
trade_group=TradeGroup.GRID,
|
|
)
|
|
)
|
|
service.save_trade(
|
|
Trade(
|
|
id=None,
|
|
account_id=account.id,
|
|
instrument_id=instrument.id,
|
|
trade_date=date(2026, 7, 8),
|
|
side=TradeSide.SELL,
|
|
price=Decimal("2.10"),
|
|
quantity=200,
|
|
trade_group=TradeGroup.GRID,
|
|
)
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="后续成交"):
|
|
service.update_trade(
|
|
Trade(
|
|
id=buy.id,
|
|
account_id=account.id,
|
|
instrument_id=instrument.id,
|
|
trade_date=date(2026, 7, 7),
|
|
side=TradeSide.BUY,
|
|
price=Decimal("2.00"),
|
|
quantity=100,
|
|
trade_group=TradeGroup.GRID,
|
|
)
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="后续成交"):
|
|
service.delete_trade(buy.id)
|
|
|
|
|
|
def test_service_refresh_quotes_uses_realtime_price_in_summaries(tmp_path):
|
|
service = TradingService(tmp_path / "grid.db", quote_provider=FakeQuoteProvider())
|
|
service.ensure_defaults()
|
|
account = service.get_active_account()
|
|
instrument = service.add_instrument(
|
|
Instrument(id=None, code="510300", name="沪深300ETF", market="ETF", manual_price=Decimal("3.90"))
|
|
)
|
|
service.save_trade(
|
|
Trade(
|
|
id=None,
|
|
account_id=account.id,
|
|
instrument_id=instrument.id,
|
|
trade_date=date(2026, 7, 7),
|
|
side=TradeSide.BUY,
|
|
price=Decimal("4.00"),
|
|
quantity=1000,
|
|
trade_group=TradeGroup.BASE,
|
|
)
|
|
)
|
|
|
|
quotes = service.refresh_quotes()
|
|
[position] = service.get_position_summaries(as_of=date(2026, 7, 8))
|
|
summary = service.get_account_summary(as_of=date(2026, 7, 8))
|
|
|
|
assert quotes["510300"].price == Decimal("4.12")
|
|
assert position.current_price == Decimal("4.12")
|
|
assert position.price_source == "tencent"
|
|
assert summary.market_value == Decimal("4120.00")
|