feat: add grid level suggestions
This commit is contained in:
10
README.md
10
README.md
@@ -10,6 +10,7 @@
|
|||||||
- 手动录入、编辑、删除买入/卖出成交。
|
- 手动录入、编辑、删除买入/卖出成交。
|
||||||
- 自动计算持仓数量、T+1 可用数量、持仓成本、已实现盈亏、累计网格利润、持仓回本价、账户回本价。
|
- 自动计算持仓数量、T+1 可用数量、持仓成本、已实现盈亏、累计网格利润、持仓回本价、账户回本价。
|
||||||
- 点击“刷新行情”从腾讯接口获取实时价格,并用实时价更新持仓市值和账户摘要。
|
- 点击“刷新行情”从腾讯接口获取实时价格,并用实时价更新持仓市值和账户摘要。
|
||||||
|
- 根据刷新后的现价和默认网格策略,生成选中标的的网格档位建议。
|
||||||
- 使用 SQLite 本地保存数据,默认路径为 `data/grid_trading.db`。
|
- 使用 SQLite 本地保存数据,默认路径为 `data/grid_trading.db`。
|
||||||
|
|
||||||
## 行情说明
|
## 行情说明
|
||||||
@@ -24,6 +25,15 @@ http://qt.gtimg.cn/q=<symbol>
|
|||||||
|
|
||||||
实时行情只保存在当前程序内存中,用于显示现价、持仓市值、浮动盈亏、总资产和资金使用率;不会写入 SQLite。成交录入里的价格只作为成交价保存,不会作为现价兜底。关闭软件后再次打开,需要重新点击“刷新行情”。
|
实时行情只保存在当前程序内存中,用于显示现价、持仓市值、浮动盈亏、总资产和资金使用率;不会写入 SQLite。成交录入里的价格只作为成交价保存,不会作为现价兜底。关闭软件后再次打开,需要重新点击“刷新行情”。
|
||||||
|
|
||||||
|
## 网格档位说明
|
||||||
|
|
||||||
|
选中标的后,底部“网格档位”表会根据腾讯现价和默认策略模板生成建议:
|
||||||
|
|
||||||
|
- 买入价按 `现价 * (1 - 网格间距)` 逐档向下递减。
|
||||||
|
- 卖出价按 `买入价 * (1 + 网格间距)` 计算。
|
||||||
|
- 建议买入股数按标的交易单位向下取整。
|
||||||
|
- 预计单轮毛利润不扣手续费,生成结果只用于展示,不会写入成交记录或 SQLite。
|
||||||
|
|
||||||
## 开发环境
|
## 开发环境
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
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
|
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)
|
@dataclass(frozen=True)
|
||||||
class QuoteSnapshot:
|
class QuoteSnapshot:
|
||||||
symbol: str
|
symbol: str
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ from grid_trading.domain.calculations import (
|
|||||||
calculate_positions,
|
calculate_positions,
|
||||||
estimate_fees,
|
estimate_fees,
|
||||||
)
|
)
|
||||||
|
from grid_trading.domain.grid_levels import generate_grid_levels
|
||||||
from grid_trading.domain.models import (
|
from grid_trading.domain.models import (
|
||||||
Account,
|
Account,
|
||||||
AccountSummary,
|
AccountSummary,
|
||||||
FeeEstimate,
|
FeeEstimate,
|
||||||
FeeRules,
|
FeeRules,
|
||||||
|
GridLevelSuggestion,
|
||||||
Instrument,
|
Instrument,
|
||||||
PositionSummary,
|
PositionSummary,
|
||||||
QuoteSnapshot,
|
QuoteSnapshot,
|
||||||
@@ -206,6 +208,32 @@ class TradingService:
|
|||||||
]
|
]
|
||||||
return calculate_account_summary(account, positions, ledger_entries, trades)
|
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:
|
def _require_account(self, account_id: int) -> Account:
|
||||||
account = self.repository.get_account(account_id)
|
account = self.repository.get_account(account_id)
|
||||||
if account is None:
|
if account is None:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from PySide6.QtWidgets import (
|
|||||||
QMessageBox,
|
QMessageBox,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
|
QSpinBox,
|
||||||
QTableWidget,
|
QTableWidget,
|
||||||
QTableWidgetItem,
|
QTableWidgetItem,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
@@ -25,7 +26,7 @@ from PySide6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from grid_trading.config import DEFAULT_DB_PATH
|
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.services.trading_service import TradingService
|
||||||
from grid_trading.ui.dialogs import AccountDialog, InstrumentDialog, StrategyTemplateDialog, TradeDialog
|
from grid_trading.ui.dialogs import AccountDialog, InstrumentDialog, StrategyTemplateDialog, TradeDialog
|
||||||
from grid_trading.ui.formatters import format_money, format_percent, format_price, format_quantity
|
from grid_trading.ui.formatters import format_money, format_percent, format_price, format_quantity
|
||||||
@@ -66,6 +67,15 @@ class MainWindow(QMainWindow):
|
|||||||
"浮动盈亏",
|
"浮动盈亏",
|
||||||
]
|
]
|
||||||
TRADE_COLUMNS = ["日期", "方向", "分组", "价格", "数量", "费用", "备注"]
|
TRADE_COLUMNS = ["日期", "方向", "分组", "价格", "数量", "费用", "备注"]
|
||||||
|
GRID_LEVEL_COLUMNS = [
|
||||||
|
"档位",
|
||||||
|
"买入价",
|
||||||
|
"买入金额",
|
||||||
|
"建议买入股数",
|
||||||
|
"实际投入",
|
||||||
|
"卖出价",
|
||||||
|
"预计单轮毛利润",
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, service: TradingService):
|
def __init__(self, service: TradingService):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -126,6 +136,26 @@ class MainWindow(QMainWindow):
|
|||||||
self.detail_layout.addWidget(QLabel("利润"), 3, 0)
|
self.detail_layout.addWidget(QLabel("利润"), 3, 0)
|
||||||
self.detail_layout.addWidget(self.detail_labels["profit"], 3, 1)
|
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_group = QGroupBox("最近成交")
|
||||||
trade_layout = QVBoxLayout(trade_group)
|
trade_layout = QVBoxLayout(trade_group)
|
||||||
trade_buttons = QHBoxLayout()
|
trade_buttons = QHBoxLayout()
|
||||||
@@ -145,6 +175,7 @@ class MainWindow(QMainWindow):
|
|||||||
trade_layout.addWidget(self.trades_table)
|
trade_layout.addWidget(self.trades_table)
|
||||||
|
|
||||||
details.addWidget(self.detail_box)
|
details.addWidget(self.detail_box)
|
||||||
|
details.addWidget(grid_group)
|
||||||
details.addWidget(trade_group)
|
details.addWidget(trade_group)
|
||||||
splitter.addWidget(details)
|
splitter.addWidget(details)
|
||||||
splitter.setSizes([470, 230])
|
splitter.setSizes([470, 230])
|
||||||
@@ -292,6 +323,7 @@ class MainWindow(QMainWindow):
|
|||||||
if position is None:
|
if position is None:
|
||||||
for label in self.detail_labels.values():
|
for label in self.detail_labels.values():
|
||||||
label.setText("-")
|
label.setText("-")
|
||||||
|
self._refresh_grid_levels(None)
|
||||||
self._fill_trades_table([])
|
self._fill_trades_table([])
|
||||||
return
|
return
|
||||||
self.detail_labels["price_source"].setText(position.price_source)
|
self.detail_labels["price_source"].setText(position.price_source)
|
||||||
@@ -302,8 +334,46 @@ class MainWindow(QMainWindow):
|
|||||||
self.detail_labels["profit"].setText(
|
self.detail_labels["profit"].setText(
|
||||||
f"已实现 {format_money(position.realized_pnl)} / 网格 {format_money(position.grid_profit)}"
|
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))
|
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:
|
def _fill_trades_table(self, trades: list[Trade]) -> None:
|
||||||
self._trade_ids_by_row = {}
|
self._trade_ids_by_row = {}
|
||||||
self.trades_table.setRowCount(len(trades))
|
self.trades_table.setRowCount(len(trades))
|
||||||
|
|||||||
69
tests/test_grid_levels.py
Normal file
69
tests/test_grid_levels.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from grid_trading.domain.grid_levels import generate_grid_levels
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_grid_levels_compounds_percentage_spacing_and_rounds_values():
|
||||||
|
levels = generate_grid_levels(
|
||||||
|
current_price=Decimal("10.00"),
|
||||||
|
spacing=Decimal("0.03"),
|
||||||
|
amount_per_grid=Decimal("10000"),
|
||||||
|
lot_size=100,
|
||||||
|
levels=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [item.level for item in levels] == [1, 2, 3]
|
||||||
|
assert [item.buy_price for item in levels] == [Decimal("9.70"), Decimal("9.41"), Decimal("9.13")]
|
||||||
|
assert [item.buy_amount for item in levels] == [Decimal("10000.00")] * 3
|
||||||
|
assert [item.suggested_quantity for item in levels] == [1000, 1000, 1000]
|
||||||
|
assert [item.actual_investment for item in levels] == [
|
||||||
|
Decimal("9700.00"),
|
||||||
|
Decimal("9410.00"),
|
||||||
|
Decimal("9130.00"),
|
||||||
|
]
|
||||||
|
assert [item.sell_price for item in levels] == [Decimal("9.99"), Decimal("9.69"), Decimal("9.40")]
|
||||||
|
assert [item.estimated_gross_profit for item in levels] == [
|
||||||
|
Decimal("290.00"),
|
||||||
|
Decimal("280.00"),
|
||||||
|
Decimal("270.00"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_grid_levels_uses_zero_quantity_when_amount_cannot_buy_one_lot():
|
||||||
|
[level] = generate_grid_levels(
|
||||||
|
current_price=Decimal("10.00"),
|
||||||
|
spacing=Decimal("0.03"),
|
||||||
|
amount_per_grid=Decimal("500"),
|
||||||
|
lot_size=100,
|
||||||
|
levels=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert level.buy_price == Decimal("9.70")
|
||||||
|
assert level.suggested_quantity == 0
|
||||||
|
assert level.actual_investment == Decimal("0.00")
|
||||||
|
assert level.estimated_gross_profit == Decimal("0.00")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("current_price", "spacing", "amount_per_grid", "lot_size", "levels"),
|
||||||
|
[
|
||||||
|
(Decimal("0"), Decimal("0.03"), Decimal("10000"), 100, 10),
|
||||||
|
(Decimal("10"), Decimal("0"), Decimal("10000"), 100, 10),
|
||||||
|
(Decimal("10"), Decimal("1"), Decimal("10000"), 100, 10),
|
||||||
|
(Decimal("10"), Decimal("0.03"), Decimal("0"), 100, 10),
|
||||||
|
(Decimal("10"), Decimal("0.03"), Decimal("10000"), 0, 10),
|
||||||
|
(Decimal("10"), Decimal("0.03"), Decimal("10000"), 100, 0),
|
||||||
|
(Decimal("10"), Decimal("0.03"), Decimal("10000"), 100, 101),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_generate_grid_levels_validates_inputs(current_price, spacing, amount_per_grid, lot_size, levels):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
generate_grid_levels(
|
||||||
|
current_price=current_price,
|
||||||
|
spacing=spacing,
|
||||||
|
amount_per_grid=amount_per_grid,
|
||||||
|
lot_size=lot_size,
|
||||||
|
levels=levels,
|
||||||
|
)
|
||||||
@@ -223,3 +223,38 @@ def test_service_refresh_quotes_uses_realtime_price_in_summaries(tmp_path):
|
|||||||
assert position.current_price == Decimal("4.12")
|
assert position.current_price == Decimal("4.12")
|
||||||
assert position.price_source == "tencent"
|
assert position.price_source == "tencent"
|
||||||
assert summary.market_value == Decimal("4120.00")
|
assert summary.market_value == Decimal("4120.00")
|
||||||
|
|
||||||
|
|
||||||
|
def test_service_generates_grid_level_suggestions_from_realtime_price(tmp_path):
|
||||||
|
service = TradingService(tmp_path / "grid.db", quote_provider=FakeQuoteProvider())
|
||||||
|
service.ensure_defaults()
|
||||||
|
account = service.get_active_account()
|
||||||
|
instrument = service.add_instrument(Instrument(id=None, code="510300", name="沪深300ETF", market="ETF"))
|
||||||
|
service.save_trade(
|
||||||
|
Trade(
|
||||||
|
id=None,
|
||||||
|
account_id=account.id,
|
||||||
|
instrument_id=instrument.id,
|
||||||
|
trade_date=date(2026, 7, 7),
|
||||||
|
side=TradeSide.BUY,
|
||||||
|
price=Decimal("4.00"),
|
||||||
|
quantity=1000,
|
||||||
|
trade_group=TradeGroup.BASE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
service.refresh_quotes()
|
||||||
|
|
||||||
|
levels = service.get_grid_level_suggestions(instrument.id, levels=2)
|
||||||
|
|
||||||
|
assert [item.buy_price for item in levels] == [Decimal("4.00"), Decimal("3.88")]
|
||||||
|
assert [item.sell_price for item in levels] == [Decimal("4.12"), Decimal("4.00")]
|
||||||
|
assert [item.buy_amount for item in levels] == [Decimal("5000.00"), Decimal("5000.00")]
|
||||||
|
assert [item.suggested_quantity for item in levels] == [1200, 1200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_service_returns_empty_grid_levels_without_realtime_price(tmp_path):
|
||||||
|
service = TradingService(tmp_path / "grid.db")
|
||||||
|
service.ensure_defaults()
|
||||||
|
instrument = service.add_instrument(Instrument(id=None, code="510300", name="沪深300ETF", market="ETF"))
|
||||||
|
|
||||||
|
assert service.get_grid_level_suggestions(instrument.id, levels=10) == []
|
||||||
|
|||||||
@@ -35,6 +35,26 @@ def test_main_window_can_be_constructed_offscreen(tmp_path, monkeypatch):
|
|||||||
app.processEvents()
|
app.processEvents()
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_contains_grid_level_table(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QApplication, QGroupBox
|
||||||
|
|
||||||
|
from grid_trading.services.trading_service import TradingService
|
||||||
|
from grid_trading.ui.main_window import MainWindow
|
||||||
|
|
||||||
|
app = QApplication.instance() or QApplication([])
|
||||||
|
service = TradingService(tmp_path / "grid.db")
|
||||||
|
window = MainWindow(service)
|
||||||
|
|
||||||
|
assert window.grid_levels_table.columnCount() == 7
|
||||||
|
assert any(group.title() == "网格档位" for group in window.findChildren(QGroupBox))
|
||||||
|
|
||||||
|
window.close()
|
||||||
|
service.close()
|
||||||
|
app.processEvents()
|
||||||
|
|
||||||
|
|
||||||
def test_instrument_dialog_does_not_collect_manual_current_price(monkeypatch):
|
def test_instrument_dialog_does_not_collect_manual_current_price(monkeypatch):
|
||||||
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
|
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user