feat: show open grid lots
This commit is contained in:
105
tests/test_open_grid_lots.py
Normal file
105
tests/test_open_grid_lots.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from grid_trading.domain.models import Trade, TradeGroup, TradeSide
|
||||
from grid_trading.domain.open_grid_lots import calculate_open_grid_lots
|
||||
|
||||
|
||||
def make_trade(
|
||||
*,
|
||||
trade_id: int,
|
||||
trade_date: date,
|
||||
side: TradeSide,
|
||||
price: str,
|
||||
quantity: int,
|
||||
trade_group: TradeGroup = TradeGroup.GRID,
|
||||
) -> Trade:
|
||||
return Trade(
|
||||
id=trade_id,
|
||||
account_id=1,
|
||||
instrument_id=1,
|
||||
trade_date=trade_date,
|
||||
side=side,
|
||||
price=Decimal(price),
|
||||
quantity=quantity,
|
||||
trade_group=trade_group,
|
||||
)
|
||||
|
||||
|
||||
def test_calculate_open_grid_lots_fifo_matches_grid_sells_against_grid_buys():
|
||||
today = date(2026, 7, 9)
|
||||
trades = [
|
||||
make_trade(
|
||||
trade_id=1,
|
||||
trade_date=today - timedelta(days=3),
|
||||
side=TradeSide.BUY,
|
||||
price="10.00",
|
||||
quantity=300,
|
||||
),
|
||||
make_trade(
|
||||
trade_id=2,
|
||||
trade_date=today - timedelta(days=2),
|
||||
side=TradeSide.BUY,
|
||||
price="9.50",
|
||||
quantity=200,
|
||||
),
|
||||
make_trade(
|
||||
trade_id=3,
|
||||
trade_date=today - timedelta(days=1),
|
||||
side=TradeSide.SELL,
|
||||
price="10.30",
|
||||
quantity=350,
|
||||
),
|
||||
make_trade(
|
||||
trade_id=4,
|
||||
trade_date=today,
|
||||
side=TradeSide.BUY,
|
||||
price="8.00",
|
||||
quantity=100,
|
||||
trade_group=TradeGroup.BASE,
|
||||
),
|
||||
]
|
||||
|
||||
lots = calculate_open_grid_lots(
|
||||
trades,
|
||||
spacing=Decimal("0.03"),
|
||||
current_price=Decimal("9.80"),
|
||||
as_of=today,
|
||||
)
|
||||
|
||||
assert len(lots) == 1
|
||||
[lot] = lots
|
||||
assert lot.buy_trade_id == 2
|
||||
assert lot.buy_date == today - timedelta(days=2)
|
||||
assert lot.buy_price == Decimal("9.50")
|
||||
assert lot.remaining_quantity == 150
|
||||
assert lot.actual_investment == Decimal("1425.00")
|
||||
assert lot.suggested_sell_price == Decimal("9.79")
|
||||
assert lot.estimated_gross_profit == Decimal("43.50")
|
||||
assert lot.current_price == Decimal("9.80")
|
||||
assert lot.status == "可卖"
|
||||
|
||||
|
||||
def test_calculate_open_grid_lots_reports_not_reached_and_missing_quote_statuses():
|
||||
today = date(2026, 7, 9)
|
||||
trades = [
|
||||
make_trade(trade_id=1, trade_date=today, side=TradeSide.BUY, price="10.00", quantity=100),
|
||||
]
|
||||
|
||||
[not_reached] = calculate_open_grid_lots(
|
||||
trades,
|
||||
spacing=Decimal("0.03"),
|
||||
current_price=Decimal("10.20"),
|
||||
as_of=today,
|
||||
)
|
||||
[missing_quote] = calculate_open_grid_lots(
|
||||
trades,
|
||||
spacing=Decimal("0.03"),
|
||||
current_price=None,
|
||||
as_of=today,
|
||||
)
|
||||
|
||||
assert not_reached.suggested_sell_price == Decimal("10.30")
|
||||
assert not_reached.status == "未到价"
|
||||
assert missing_quote.current_price is None
|
||||
assert missing_quote.status == "未刷新行情"
|
||||
@@ -258,3 +258,43 @@ def test_service_returns_empty_grid_levels_without_realtime_price(tmp_path):
|
||||
instrument = service.add_instrument(Instrument(id=None, code="510300", name="沪深300ETF", market="ETF"))
|
||||
|
||||
assert service.get_grid_level_suggestions(instrument.id, levels=10) == []
|
||||
|
||||
|
||||
def test_service_returns_open_grid_lots_with_suggested_sell_price(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"))
|
||||
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.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.12"),
|
||||
quantity=400,
|
||||
trade_group=TradeGroup.GRID,
|
||||
)
|
||||
)
|
||||
service.refresh_quotes()
|
||||
|
||||
[lot] = service.get_open_grid_lots(instrument.id, as_of=date(2026, 7, 9))
|
||||
|
||||
assert lot.buy_price == Decimal("4.00")
|
||||
assert lot.remaining_quantity == 600
|
||||
assert lot.suggested_sell_price == Decimal("4.12")
|
||||
assert lot.current_price == Decimal("4.12")
|
||||
assert lot.status == "可卖"
|
||||
|
||||
@@ -55,6 +55,26 @@ def test_main_window_contains_grid_level_table(tmp_path, monkeypatch):
|
||||
app.processEvents()
|
||||
|
||||
|
||||
def test_main_window_contains_open_grid_lots_table(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QGroupBox
|
||||
|
||||
from grid_trading.services.trading_service import TradingService
|
||||
from grid_trading.ui.main_window import MainWindow
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
service = TradingService(tmp_path / "grid.db")
|
||||
window = MainWindow(service)
|
||||
|
||||
assert window.open_grid_lots_table.columnCount() == 8
|
||||
assert any(group.title() == "待卖网格" for group in window.findChildren(QGroupBox))
|
||||
|
||||
window.close()
|
||||
service.close()
|
||||
app.processEvents()
|
||||
|
||||
|
||||
def test_instrument_dialog_does_not_collect_manual_current_price(monkeypatch):
|
||||
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user