from __future__ import annotations from datetime import date from decimal import Decimal, InvalidOperation 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.manual_price_edit = QLineEdit(str(instrument.manual_price or "") if instrument else "") 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.manual_price_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: manual_price = _optional_decimal(self.manual_price_edit.text().strip(), "手动价格") 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=manual_price, 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) 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 def _optional_decimal(raw: str, label: str) -> Decimal | None: if not raw: return None try: return Decimal(raw) except InvalidOperation as exc: raise ValueError(f"{label} 不是有效数字") from exc