docs: plan grid level suggestions
This commit is contained in:
419
docs/superpowers/plans/2026-07-09-grid-level-suggestions.md
Normal file
419
docs/superpowers/plans/2026-07-09-grid-level-suggestions.md
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user