feat: add pyside6 holdings gui
This commit is contained in:
300
src/grid_trading/ui/dialogs.py
Normal file
300
src/grid_trading/ui/dialogs.py
Normal file
@@ -0,0 +1,300 @@
|
||||
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
|
||||
Reference in New Issue
Block a user