Files
Grid_Trading/docs/superpowers/plans/2026-07-09-grid-level-suggestions.md
2026-07-09 10:44:47 +08:00

13 KiB

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:

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:

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:

@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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

pytest -q

Expected: all tests pass.

  • Step 3: Run CLI smoke

Run:

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.