291 lines
11 KiB
Python
291 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
|
|
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,
|
|
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
|
|
|
|
|