feat: add grid level suggestions
This commit is contained in:
47
src/grid_trading/domain/grid_levels.py
Normal file
47
src/grid_trading/domain/grid_levels.py
Normal file
@@ -0,0 +1,47 @@
|
||||
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
|
||||
@@ -114,6 +114,17 @@ class FeeEstimate:
|
||||
return self.commission + self.stamp_tax + self.transfer_fee
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class QuoteSnapshot:
|
||||
symbol: str
|
||||
|
||||
@@ -12,11 +12,13 @@ from grid_trading.domain.calculations import (
|
||||
calculate_positions,
|
||||
estimate_fees,
|
||||
)
|
||||
from grid_trading.domain.grid_levels import generate_grid_levels
|
||||
from grid_trading.domain.models import (
|
||||
Account,
|
||||
AccountSummary,
|
||||
FeeEstimate,
|
||||
FeeRules,
|
||||
GridLevelSuggestion,
|
||||
Instrument,
|
||||
PositionSummary,
|
||||
QuoteSnapshot,
|
||||
@@ -206,6 +208,32 @@ class TradingService:
|
||||
]
|
||||
return calculate_account_summary(account, positions, ledger_entries, trades)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
def _require_account(self, account_id: int) -> Account:
|
||||
account = self.repository.get_account(account_id)
|
||||
if account is None:
|
||||
|
||||
@@ -18,6 +18,7 @@ from PySide6.QtWidgets import (
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QSplitter,
|
||||
QSpinBox,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
@@ -25,7 +26,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from grid_trading.config import DEFAULT_DB_PATH
|
||||
from grid_trading.domain.models import PositionSummary, Trade, TradeSide
|
||||
from grid_trading.domain.models import GridLevelSuggestion, PositionSummary, Trade, TradeSide
|
||||
from grid_trading.services.trading_service import TradingService
|
||||
from grid_trading.ui.dialogs import AccountDialog, InstrumentDialog, StrategyTemplateDialog, TradeDialog
|
||||
from grid_trading.ui.formatters import format_money, format_percent, format_price, format_quantity
|
||||
@@ -66,6 +67,15 @@ class MainWindow(QMainWindow):
|
||||
"浮动盈亏",
|
||||
]
|
||||
TRADE_COLUMNS = ["日期", "方向", "分组", "价格", "数量", "费用", "备注"]
|
||||
GRID_LEVEL_COLUMNS = [
|
||||
"档位",
|
||||
"买入价",
|
||||
"买入金额",
|
||||
"建议买入股数",
|
||||
"实际投入",
|
||||
"卖出价",
|
||||
"预计单轮毛利润",
|
||||
]
|
||||
|
||||
def __init__(self, service: TradingService):
|
||||
super().__init__()
|
||||
@@ -126,6 +136,26 @@ class MainWindow(QMainWindow):
|
||||
self.detail_layout.addWidget(QLabel("利润"), 3, 0)
|
||||
self.detail_layout.addWidget(self.detail_labels["profit"], 3, 1)
|
||||
|
||||
grid_group = QGroupBox("网格档位")
|
||||
grid_layout = QVBoxLayout(grid_group)
|
||||
grid_controls = QHBoxLayout()
|
||||
self.grid_levels_count_edit = QSpinBox()
|
||||
self.grid_levels_count_edit.setRange(1, 100)
|
||||
self.grid_levels_count_edit.setValue(10)
|
||||
self.grid_levels_count_edit.valueChanged.connect(lambda _value: self._refresh_details())
|
||||
self.grid_levels_hint = QLabel("请先刷新行情")
|
||||
grid_controls.addWidget(QLabel("档数"))
|
||||
grid_controls.addWidget(self.grid_levels_count_edit)
|
||||
grid_controls.addWidget(self.grid_levels_hint)
|
||||
grid_controls.addStretch()
|
||||
self.grid_levels_table = QTableWidget(0, len(self.GRID_LEVEL_COLUMNS))
|
||||
self.grid_levels_table.setHorizontalHeaderLabels(self.GRID_LEVEL_COLUMNS)
|
||||
self.grid_levels_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.grid_levels_table.horizontalHeader().setStretchLastSection(True)
|
||||
self.grid_levels_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
grid_layout.addLayout(grid_controls)
|
||||
grid_layout.addWidget(self.grid_levels_table)
|
||||
|
||||
trade_group = QGroupBox("最近成交")
|
||||
trade_layout = QVBoxLayout(trade_group)
|
||||
trade_buttons = QHBoxLayout()
|
||||
@@ -145,6 +175,7 @@ class MainWindow(QMainWindow):
|
||||
trade_layout.addWidget(self.trades_table)
|
||||
|
||||
details.addWidget(self.detail_box)
|
||||
details.addWidget(grid_group)
|
||||
details.addWidget(trade_group)
|
||||
splitter.addWidget(details)
|
||||
splitter.setSizes([470, 230])
|
||||
@@ -292,6 +323,7 @@ class MainWindow(QMainWindow):
|
||||
if position is None:
|
||||
for label in self.detail_labels.values():
|
||||
label.setText("-")
|
||||
self._refresh_grid_levels(None)
|
||||
self._fill_trades_table([])
|
||||
return
|
||||
self.detail_labels["price_source"].setText(position.price_source)
|
||||
@@ -302,8 +334,46 @@ class MainWindow(QMainWindow):
|
||||
self.detail_labels["profit"].setText(
|
||||
f"已实现 {format_money(position.realized_pnl)} / 网格 {format_money(position.grid_profit)}"
|
||||
)
|
||||
self._refresh_grid_levels(position)
|
||||
self._fill_trades_table(self.service.list_trades(instrument_id=position.instrument_id))
|
||||
|
||||
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
|
||||
try:
|
||||
levels = self.service.get_grid_level_suggestions(
|
||||
position.instrument_id,
|
||||
levels=self.grid_levels_count_edit.value(),
|
||||
)
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "网格档位失败", str(exc))
|
||||
self.grid_levels_hint.setText(str(exc))
|
||||
self._fill_grid_levels_table([])
|
||||
return
|
||||
self.grid_levels_hint.setText("")
|
||||
self._fill_grid_levels_table(levels)
|
||||
|
||||
def _fill_grid_levels_table(self, levels: list[GridLevelSuggestion]) -> 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))
|
||||
|
||||
def _fill_trades_table(self, trades: list[Trade]) -> None:
|
||||
self._trade_ids_by_row = {}
|
||||
self.trades_table.setRowCount(len(trades))
|
||||
|
||||
Reference in New Issue
Block a user