diff --git a/docs/superpowers/plans/2026-07-09-grid-level-suggestions.md b/docs/superpowers/plans/2026-07-09-grid-level-suggestions.md new file mode 100644 index 0000000..5c16c26 --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-grid-level-suggestions.md @@ -0,0 +1,419 @@ +# Grid Level Suggestions Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a GUI grid-level table that generates buy/sell grid suggestions from Tencent current price and the existing strategy percentage spacing. + +**Architecture:** Put the grid formula in a pure domain module, expose it through `TradingService`, and render it in the selected-instrument area of the PySide6 main window. The feature uses existing strategy template fields (`grid_spacing_pct`, `amount_per_grid`) and existing quote-backed `PositionSummary.current_price`. + +**Tech Stack:** Python 3.11, Decimal, PySide6, SQLite repository/service pattern, pytest. + +--- + +## File Structure + +- Create `src/grid_trading/domain/grid_levels.py`: pure grid-level calculation, independent of PySide6 and SQLite. +- Modify `src/grid_trading/domain/models.py`: add `GridLevelSuggestion`. +- Modify `src/grid_trading/services/trading_service.py`: add `get_grid_level_suggestions(instrument_id, levels=10)`. +- Modify `src/grid_trading/ui/main_window.py`: add a “网格档位” table and a non-persisted level-count spin box. +- Create `tests/test_grid_levels.py`: domain formula tests. +- Modify `tests/test_services.py`: service integration tests for quote-backed suggestions and missing-quote empty state. +- Modify `tests/test_ui.py`: GUI smoke test for the grid-level table. +- Modify `README.md`: document the grid-level table behavior. + +## Tasks + +### Task 1: Domain Grid-Level Formula + +**Files:** +- Create: `src/grid_trading/domain/grid_levels.py` +- Modify: `src/grid_trading/domain/models.py` +- Test: `tests/test_grid_levels.py` + +- [ ] **Step 1: Write failing formula tests** + +Add tests that exercise the wished-for API: + +```python +from decimal import Decimal + +import pytest + +from grid_trading.domain.grid_levels import generate_grid_levels + + +def test_generate_grid_levels_compounds_percentage_spacing_and_rounds_values(): + levels = generate_grid_levels( + current_price=Decimal("10.00"), + spacing=Decimal("0.03"), + amount_per_grid=Decimal("10000"), + lot_size=100, + levels=3, + ) + + assert [item.level for item in levels] == [1, 2, 3] + assert [item.buy_price for item in levels] == [Decimal("9.70"), Decimal("9.41"), Decimal("9.13")] + assert [item.buy_amount for item in levels] == [Decimal("10000.00")] * 3 + assert [item.suggested_quantity for item in levels] == [1000, 1000, 1000] + assert [item.actual_investment for item in levels] == [ + Decimal("9700.00"), + Decimal("9410.00"), + Decimal("9130.00"), + ] + assert [item.sell_price for item in levels] == [Decimal("9.99"), Decimal("9.69"), Decimal("9.40")] + assert [item.estimated_gross_profit for item in levels] == [ + Decimal("290.00"), + Decimal("280.00"), + Decimal("270.00"), + ] + + +def test_generate_grid_levels_uses_zero_quantity_when_amount_cannot_buy_one_lot(): + [level] = generate_grid_levels( + current_price=Decimal("10.00"), + spacing=Decimal("0.03"), + amount_per_grid=Decimal("500"), + lot_size=100, + levels=1, + ) + + assert level.buy_price == Decimal("9.70") + assert level.suggested_quantity == 0 + assert level.actual_investment == Decimal("0.00") + assert level.estimated_gross_profit == Decimal("0.00") + + +@pytest.mark.parametrize( + ("current_price", "spacing", "amount_per_grid", "lot_size", "levels"), + [ + (Decimal("0"), Decimal("0.03"), Decimal("10000"), 100, 10), + (Decimal("10"), Decimal("0"), Decimal("10000"), 100, 10), + (Decimal("10"), Decimal("1"), Decimal("10000"), 100, 10), + (Decimal("10"), Decimal("0.03"), Decimal("0"), 100, 10), + (Decimal("10"), Decimal("0.03"), Decimal("10000"), 0, 10), + (Decimal("10"), Decimal("0.03"), Decimal("10000"), 100, 0), + (Decimal("10"), Decimal("0.03"), Decimal("10000"), 100, 101), + ], +) +def test_generate_grid_levels_validates_inputs(current_price, spacing, amount_per_grid, lot_size, levels): + with pytest.raises(ValueError): + generate_grid_levels( + current_price=current_price, + spacing=spacing, + amount_per_grid=amount_per_grid, + lot_size=lot_size, + levels=levels, + ) +``` + +- [ ] **Step 2: Run formula tests and verify red** + +Run: + +```powershell +pytest tests/test_grid_levels.py -q +``` + +Expected: FAIL because `grid_trading.domain.grid_levels` does not exist. + +- [ ] **Step 3: Implement model and pure calculation** + +Add `GridLevelSuggestion` to `models.py`: + +```python +@dataclass(frozen=True) +class GridLevelSuggestion: + level: int + buy_price: Decimal + buy_amount: Decimal + suggested_quantity: int + actual_investment: Decimal + sell_price: Decimal + estimated_gross_profit: Decimal +``` + +Create `grid_levels.py` with: + +```python +from __future__ import annotations + +from decimal import Decimal + +from grid_trading.domain.calculations import money, price +from grid_trading.domain.models import GridLevelSuggestion + + +def generate_grid_levels( + *, + current_price: Decimal, + spacing: Decimal, + amount_per_grid: Decimal, + lot_size: int, + levels: int, +) -> list[GridLevelSuggestion]: + if current_price <= 0: + raise ValueError("现价必须大于 0") + if spacing <= 0 or spacing >= 1: + raise ValueError("网格间距必须大于 0 且小于 100%") + if amount_per_grid <= 0: + raise ValueError("每格金额必须大于 0") + if lot_size <= 0: + raise ValueError("交易单位必须大于 0") + if levels < 1 or levels > 100: + raise ValueError("档数必须在 1 到 100 之间") + + suggestions: list[GridLevelSuggestion] = [] + buy_price = price(current_price * (Decimal("1") - spacing)) + for level in range(1, levels + 1): + quantity = int(amount_per_grid / buy_price) // lot_size * lot_size + actual_investment = money(buy_price * Decimal(quantity)) + sell_price = price(buy_price * (Decimal("1") + spacing)) + estimated_gross_profit = money((sell_price - buy_price) * Decimal(quantity)) + suggestions.append( + GridLevelSuggestion( + level=level, + buy_price=buy_price, + buy_amount=money(amount_per_grid), + suggested_quantity=quantity, + actual_investment=actual_investment, + sell_price=sell_price, + estimated_gross_profit=estimated_gross_profit, + ) + ) + buy_price = price(buy_price * (Decimal("1") - spacing)) + return suggestions +``` + +- [ ] **Step 4: Run formula tests and verify green** + +Run: + +```powershell +pytest tests/test_grid_levels.py -q +``` + +Expected: PASS. + +### Task 2: Service API + +**Files:** +- Modify: `src/grid_trading/services/trading_service.py` +- Test: `tests/test_services.py` + +- [ ] **Step 1: Write failing service tests** + +Add tests: + +```python +def test_service_generates_grid_level_suggestions_from_realtime_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.BASE, + ) + ) + service.refresh_quotes() + + levels = service.get_grid_level_suggestions(instrument.id, levels=2) + + assert [item.buy_price for item in levels] == [Decimal("4.00"), Decimal("3.88")] + assert [item.sell_price for item in levels] == [Decimal("4.12"), Decimal("4.00")] + assert [item.buy_amount for item in levels] == [Decimal("5000.00"), Decimal("5000.00")] + assert [item.suggested_quantity for item in levels] == [1200, 1200] + + +def test_service_returns_empty_grid_levels_without_realtime_price(tmp_path): + service = TradingService(tmp_path / "grid.db") + service.ensure_defaults() + instrument = service.add_instrument(Instrument(id=None, code="510300", name="沪深300ETF", market="ETF")) + + assert service.get_grid_level_suggestions(instrument.id, levels=10) == [] +``` + +- [ ] **Step 2: Run service tests and verify red** + +Run: + +```powershell +pytest tests/test_services.py::test_service_generates_grid_level_suggestions_from_realtime_price tests/test_services.py::test_service_returns_empty_grid_levels_without_realtime_price -q +``` + +Expected: FAIL because `get_grid_level_suggestions` does not exist. + +- [ ] **Step 3: Implement service API** + +Import `generate_grid_levels` and `GridLevelSuggestion`, then add: + +```python +def get_grid_level_suggestions( + self, + instrument_id: int, + *, + levels: int = 10, +) -> list[GridLevelSuggestion]: + instrument = self._require_instrument(instrument_id) + position = next( + ( + item + for item in self.get_position_summaries() + if item.instrument_id == instrument_id + ), + None, + ) + if position is None or position.current_price is None: + return [] + template = self.get_default_strategy_template() + return generate_grid_levels( + current_price=position.current_price, + spacing=template.grid_spacing_pct, + amount_per_grid=template.amount_per_grid, + lot_size=instrument.lot_size, + levels=levels, + ) +``` + +- [ ] **Step 4: Run service tests and verify green** + +Run: + +```powershell +pytest tests/test_services.py -q +``` + +Expected: PASS. + +### Task 3: GUI Grid-Level Table + +**Files:** +- Modify: `src/grid_trading/ui/main_window.py` +- Test: `tests/test_ui.py` + +- [ ] **Step 1: Write failing GUI smoke test** + +Add: + +```python +def test_main_window_contains_grid_level_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.grid_levels_table.columnCount() == 7 + assert any(group.title() == "网格档位" for group in window.findChildren(QGroupBox)) + + window.close() + service.close() + app.processEvents() +``` + +- [ ] **Step 2: Run GUI smoke test and verify red** + +Run: + +```powershell +pytest tests/test_ui.py::test_main_window_contains_grid_level_table -q +``` + +Expected: FAIL because `grid_levels_table` does not exist. + +- [ ] **Step 3: Implement GUI table** + +Add `GRID_LEVEL_COLUMNS`, import `QSpinBox`, create `self.grid_levels_table`, `self.grid_levels_count_edit`, and `self.grid_levels_hint` in `_build_ui`. Add helper methods: + +```python +def _refresh_grid_levels(self, position: PositionSummary | None) -> None: + if position is None: + self.grid_levels_hint.setText("-") + self._fill_grid_levels_table([]) + return + if position.current_price is None: + self.grid_levels_hint.setText("请先刷新行情") + self._fill_grid_levels_table([]) + return + levels = self.service.get_grid_level_suggestions( + position.instrument_id, + levels=self.grid_levels_count_edit.value(), + ) + self.grid_levels_hint.setText("") + self._fill_grid_levels_table(levels) + + +def _fill_grid_levels_table(self, levels) -> None: + self.grid_levels_table.setRowCount(len(levels)) + for row, level in enumerate(levels): + values = [ + str(level.level), + format_price(level.buy_price), + format_money(level.buy_amount), + format_quantity(level.suggested_quantity), + format_money(level.actual_investment), + format_price(level.sell_price), + format_money(level.estimated_gross_profit), + ] + for column, value in enumerate(values): + self.grid_levels_table.setItem(row, column, QTableWidgetItem(value)) +``` + +Call `_refresh_grid_levels(position)` from `_refresh_details`. + +- [ ] **Step 4: Run GUI tests and verify green** + +Run: + +```powershell +pytest tests/test_ui.py -q +``` + +Expected: PASS. + +### Task 4: Docs And Verification + +**Files:** +- Modify: `README.md` + +- [ ] **Step 1: Update README** + +Add a first-phase bullet for the grid-level table and note that it depends on Tencent current price. + +- [ ] **Step 2: Run all tests** + +Run: + +```powershell +pytest -q +``` + +Expected: all tests pass. + +- [ ] **Step 3: Run CLI smoke** + +Run: + +```powershell +python -m grid_trading.app --help +``` + +Expected: help text prints normally. + +## Self-Review + +- Spec coverage: formula, service API, GUI table, no quote empty state, non-persistent levels count, and docs are covered. +- Placeholder scan: no unresolved placeholder text is intentionally left. +- Type consistency: `GridLevelSuggestion`, `generate_grid_levels`, and `get_grid_level_suggestions` names match across tasks.