Files
Grid_Trading/src/grid_trading/ui/dialogs.py

291 lines
11 KiB
Python
Raw Normal View History

2026-07-08 17:33:52 +08:00
from __future__ import annotations
from datetime import date
from decimal import Decimal
2026-07-08 17:33:52 +08:00
from PySide6.QtCore import QDate
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDateEdit,
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
QFormLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QMessageBox,
QPushButton,
QSpinBox,
QTextEdit,
QVBoxLayout,
)
from grid_trading.domain.models import (
Account,
Instrument,
StrategyTemplate,
Trade,
TradeGroup,
TradeSide,
)
from grid_trading.services.trading_service import TradingService
class AccountDialog(QDialog):
def __init__(self, account: Account, parent=None):
super().__init__(parent)
self.setWindowTitle("账户设置")
self._account = account
self.name_edit = QLineEdit(account.name)
self.cash_edit = _money_spinbox(account.initial_cash)
self.notes_edit = QTextEdit(account.notes)
self.notes_edit.setFixedHeight(70)
form = QFormLayout()
form.addRow("账户名称", self.name_edit)
form.addRow("初始资金", self.cash_edit)
form.addRow("备注", self.notes_edit)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout = QVBoxLayout(self)
layout.addLayout(form)
layout.addWidget(buttons)
def to_account(self) -> Account:
return Account(
id=self._account.id,
name=self.name_edit.text().strip(),
initial_cash=Decimal(str(self.cash_edit.value())),
notes=self.notes_edit.toPlainText().strip(),
)
class InstrumentDialog(QDialog):
def __init__(self, instrument: Instrument | None = None, parent=None):
super().__init__(parent)
self.setWindowTitle("标的设置")
self._instrument = instrument
self.code_edit = QLineEdit(instrument.code if instrument else "")
self.name_edit = QLineEdit(instrument.name if instrument else "")
self.market_edit = QComboBox()
self.market_edit.addItems(["A", "ETF"])
if instrument:
self.market_edit.setCurrentText(instrument.market)
self.lot_size_edit = QSpinBox()
self.lot_size_edit.setRange(1, 1_000_000)
self.lot_size_edit.setValue(instrument.lot_size if instrument else 100)
self.allow_odd_lot_edit = QCheckBox("允许非整手")
self.allow_odd_lot_edit.setChecked(instrument.allow_odd_lot if instrument else False)
form = QFormLayout()
form.addRow("代码", self.code_edit)
form.addRow("名称", self.name_edit)
form.addRow("市场", self.market_edit)
form.addRow("交易单位", self.lot_size_edit)
form.addRow("", self.allow_odd_lot_edit)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout = QVBoxLayout(self)
layout.addLayout(form)
layout.addWidget(buttons)
def to_instrument(self) -> Instrument:
return Instrument(
id=self._instrument.id if self._instrument else None,
code=self.code_edit.text().strip(),
name=self.name_edit.text().strip(),
market=self.market_edit.currentText(),
lot_size=self.lot_size_edit.value(),
manual_price=None,
2026-07-08 17:33:52 +08:00
allow_odd_lot=self.allow_odd_lot_edit.isChecked(),
active=True,
)
class StrategyTemplateDialog(QDialog):
def __init__(self, template: StrategyTemplate, parent=None):
super().__init__(parent)
self.setWindowTitle("默认网格策略")
self._template = template
self.name_edit = QLineEdit(template.name)
self.grid_spacing_edit = _percent_spinbox(template.grid_spacing_pct)
self.amount_per_grid_edit = _money_spinbox(template.amount_per_grid)
self.base_target_edit = _money_spinbox(template.base_target_amount)
self.max_position_edit = _money_spinbox(template.max_position_amount)
self.min_lot_edit = QSpinBox()
self.min_lot_edit.setRange(1, 1_000_000)
self.min_lot_edit.setValue(template.min_lot)
form = QFormLayout()
form.addRow("模板名称", self.name_edit)
form.addRow("网格间距", self.grid_spacing_edit)
form.addRow("每格金额", self.amount_per_grid_edit)
form.addRow("底仓目标金额", self.base_target_edit)
form.addRow("最大投入金额", self.max_position_edit)
form.addRow("最小交易单位", self.min_lot_edit)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout = QVBoxLayout(self)
layout.addLayout(form)
layout.addWidget(buttons)
def to_template(self) -> StrategyTemplate:
return StrategyTemplate(
id=self._template.id,
name=self.name_edit.text().strip(),
grid_spacing_pct=Decimal(str(self.grid_spacing_edit.value())) / Decimal("100"),
amount_per_grid=Decimal(str(self.amount_per_grid_edit.value())),
base_target_amount=Decimal(str(self.base_target_edit.value())),
max_position_amount=Decimal(str(self.max_position_edit.value())),
min_lot=self.min_lot_edit.value(),
is_default=True,
)
class TradeDialog(QDialog):
def __init__(
self,
service: TradingService,
account_id: int,
instruments: list[Instrument],
trade: Trade | None = None,
parent=None,
):
super().__init__(parent)
self.setWindowTitle("成交记录")
self._service = service
self._account_id = account_id
self._instruments = instruments
self._trade = trade
self.instrument_edit = QComboBox()
for instrument in instruments:
self.instrument_edit.addItem(f"{instrument.code} {instrument.name}", instrument.id)
if trade:
index = self.instrument_edit.findData(trade.instrument_id)
if index >= 0:
self.instrument_edit.setCurrentIndex(index)
self.date_edit = QDateEdit()
self.date_edit.setCalendarPopup(True)
trade_date = trade.trade_date if trade else date.today()
self.date_edit.setDate(QDate(trade_date.year, trade_date.month, trade_date.day))
self.side_edit = QComboBox()
self.side_edit.addItem("买入", TradeSide.BUY.value)
self.side_edit.addItem("卖出", TradeSide.SELL.value)
if trade:
self.side_edit.setCurrentIndex(self.side_edit.findData(trade.side.value))
self.group_edit = QComboBox()
self.group_edit.addItem("底仓", TradeGroup.BASE.value)
self.group_edit.addItem("网格", TradeGroup.GRID.value)
self.group_edit.addItem("其他", TradeGroup.OTHER.value)
if trade:
self.group_edit.setCurrentIndex(self.group_edit.findData(trade.trade_group.value))
self.price_edit = _money_spinbox(trade.price if trade else Decimal("0"))
self.quantity_edit = QSpinBox()
self.quantity_edit.setRange(1, 100_000_000)
self.quantity_edit.setSingleStep(100)
self.quantity_edit.setValue(trade.quantity if trade else 100)
self.commission_edit = _money_spinbox(trade.commission if trade else Decimal("0"))
self.stamp_tax_edit = _money_spinbox(trade.stamp_tax if trade else Decimal("0"))
self.transfer_fee_edit = _money_spinbox(trade.transfer_fee if trade else Decimal("0"))
self.notes_edit = QLineEdit(trade.notes if trade else "")
estimate_button = QPushButton("估算费用")
estimate_button.clicked.connect(self._estimate_fees)
fee_row = QHBoxLayout()
fee_row.addWidget(QLabel("手续费"))
fee_row.addWidget(self.commission_edit)
fee_row.addWidget(QLabel("印花税"))
fee_row.addWidget(self.stamp_tax_edit)
fee_row.addWidget(QLabel("过户费"))
fee_row.addWidget(self.transfer_fee_edit)
fee_row.addWidget(estimate_button)
form = QFormLayout()
form.addRow("标的", self.instrument_edit)
form.addRow("日期", self.date_edit)
form.addRow("方向", self.side_edit)
form.addRow("分组", self.group_edit)
form.addRow("成交价", self.price_edit)
2026-07-08 17:33:52 +08:00
form.addRow("数量", self.quantity_edit)
form.addRow(fee_row)
form.addRow("备注", self.notes_edit)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout = QVBoxLayout(self)
layout.addLayout(form)
layout.addWidget(buttons)
def to_trade(self) -> Trade:
selected_date = self.date_edit.date().toPython()
return Trade(
id=self._trade.id if self._trade else None,
account_id=self._account_id,
instrument_id=self.instrument_edit.currentData(),
trade_date=selected_date,
side=TradeSide(self.side_edit.currentData()),
price=Decimal(str(self.price_edit.value())),
quantity=self.quantity_edit.value(),
commission=Decimal(str(self.commission_edit.value())),
stamp_tax=Decimal(str(self.stamp_tax_edit.value())),
transfer_fee=Decimal(str(self.transfer_fee_edit.value())),
trade_group=TradeGroup(self.group_edit.currentData()),
notes=self.notes_edit.text().strip(),
)
def accept(self) -> None:
if self.instrument_edit.currentData() is None:
QMessageBox.warning(self, "无法保存", "请先添加标的")
return
super().accept()
def _estimate_fees(self) -> None:
fees = self._service.estimate_trade_fees(
TradeSide(self.side_edit.currentData()),
Decimal(str(self.price_edit.value())),
self.quantity_edit.value(),
)
self.commission_edit.setValue(float(fees.commission))
self.stamp_tax_edit.setValue(float(fees.stamp_tax))
self.transfer_fee_edit.setValue(float(fees.transfer_fee))
def _money_spinbox(value: Decimal) -> QDoubleSpinBox:
spinbox = QDoubleSpinBox()
spinbox.setRange(0, 1_000_000_000)
spinbox.setDecimals(4)
spinbox.setValue(float(value))
return spinbox
def _percent_spinbox(value: Decimal) -> QDoubleSpinBox:
spinbox = QDoubleSpinBox()
spinbox.setRange(0.01, 100)
spinbox.setDecimals(2)
spinbox.setSuffix("%")
spinbox.setValue(float(value * Decimal("100")))
return spinbox