This commit is contained in:
2026-06-16 11:07:09 +08:00
parent 2d8a0c3bca
commit 2e3202968d
4 changed files with 223 additions and 57 deletions
+5 -5
View File
@@ -327,11 +327,11 @@ class QmtApp:
def _pending_tags(self, stock_code: str) -> list:
"""返回该标的下挂单的方向标签列表:''(买单) / ''(卖单)"""
tags = []
_CANCELED = {54, 57}
_TERMINAL = {54, 56, 57}
for o in self._orders:
if _plain(getattr(o, 'stock_code', '')) != stock_code:
continue
if getattr(o, 'order_status', 0) in _CANCELED:
if getattr(o, 'order_status', 0) in _TERMINAL:
continue
ot = getattr(o, 'order_type', 0)
if ot == 23 and '' not in tags:
@@ -533,8 +533,8 @@ class QmtApp:
self._sel_actions.controls = []
self._uncl_list.controls = [self._build_unclassified_table()]
# 委托 — 过滤已/废单,按 order_id 去重(保留最后一条即最新状态)
_CANCELED = {54, 57}
# 委托 — 过滤已终结订单(已撤/已成/废单),按 order_id 去重
_TERMINAL = {54, 56, 57}
o_map = {} # order_id → latest order
for o in self._orders:
oid = str(getattr(o, 'order_id', ''))
@@ -544,7 +544,7 @@ class QmtApp:
o_rows = []
for o in o_map.values():
st = getattr(o, 'order_status', 0)
if st in _CANCELED:
if st in _TERMINAL:
continue
tv = getattr(o, 'traded_volume', 0) or 0
ov = getattr(o, 'order_volume', 0) or 0
+211 -37
View File
@@ -133,6 +133,31 @@ class _DataStore:
if t.strategy_type == STRATEGY_TYPE_GRID and t.enabled:
self.strategyCtrl[tid] = SFGridStrategy(t)
def remove_target(self, tid: int) -> str:
"""移除/降级网格策略标的。返回 'unclassified'(有持仓降级)或 'deleted'(无持仓删除)"""
target = self.tradeTargets.get(tid)
if target is None:
return 'none'
# 先停止策略
ctrl = self.strategyCtrl.pop(tid, None)
if ctrl:
ctrl.enabledTrading(False)
if target.current_position > 0:
# 有持仓 → 降级为未分类
target.strategy_type = 0 # STRATEGY_TYPE_UNCLASSIFIED
target.enabled = False
target.save()
return 'unclassified'
else:
# 无持仓 → 直接删除
target.delete_instance()
self.tradeTargets.pop(tid, None)
self.stockCodeIdMap.pop(target.stock_code, None)
self.marketPrices.pop(tid, None)
self.avgPrices.pop(tid, None)
self.preClose.pop(tid, None)
return 'deleted'
# ── 行情 ──
def apply_tick(self, data: dict) -> int:
@@ -189,7 +214,8 @@ class _DataStore:
def active_orders(self) -> list:
"""返回过滤+去重后的活跃委托行"""
_CANCELED = {54, 57}
# 过滤已终结的订单: 已撤(54) 已成(56) 废单(57)
_TERMINAL = {54, 56, 57}
o_map = {}
for o in self.orders:
oid = str(getattr(o, 'order_id', ''))
@@ -198,7 +224,7 @@ class _DataStore:
rows = []
for o in o_map.values():
st = getattr(o, 'order_status', 0)
if st in _CANCELED: continue
if st in _TERMINAL: continue
tv = getattr(o, 'traded_volume', 0) or 0
ov = getattr(o, 'order_volume', 0) or 0
rows.append([
@@ -234,6 +260,36 @@ class _DataStore:
])
return rows
def add_from_market(self, stock_code: str, stock_name: str) -> int:
"""从市场监控添加标的(默认: grid_index=0, upper=0, lower=10),返回 tid"""
plain = _plain(stock_code)
existing = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == plain)
if existing:
tid = existing.get_id()
if tid not in self.tradeTargets:
self.tradeTargets[tid] = existing
self.stockCodeIdMap[plain] = tid
return tid
mp = self.marketLog.get(stock_code, {})
price = mp.get('last_price', 10.0)
target = SFGridTradeTarget.create(
stock_code=plain, stock_name=stock_name,
current_position=0, init_price=0.0,
grid_index=0, enabled=False,
strategy_type=STRATEGY_TYPE_GRID,
grid_start_price=float(price) if price > 0 else 10.0,
grid_size=1.0, grid_volume=200,
grid_upper_count=0, grid_lower_count=10,
)
tid = target.get_id()
self.tradeTargets[tid] = target
self.stockCodeIdMap[plain] = tid
# 从市场监控列表中移除(加入交易池后不再监控)
self.marketLog.pop(stock_code, None)
if stock_code in self.listeningStocks:
self.listeningStocks.remove(stock_code)
return tid
def pending_tags(self, stock_code: str) -> list:
"""某标的挂单方向标签"""
tags = []
@@ -268,8 +324,8 @@ class _GridPanel:
return self._col
def _rebuild(self):
# 列: (宽度/expand, ...) expand为比重,无则固定宽
_C = [(40, 0), (0, 2), (80, 2), (0, 2), (55, 1), (65, 1), (75, 1), (100, 0)]
# (width, expand): 0=固定宽, >0=弹性比重
_C = [(35, 0), (0, 2), (70, 1), (0, 2), (50, 1), (55, 1), (60, 1), (0, 1)]
H = ["ID", "股票", "市场价", "网格基准", "持仓", "成本", "状态", "操作"]
header = []
for h, (w, e) in zip(H, _C):
@@ -302,20 +358,28 @@ class _GridPanel:
))
gcell = ft.Row(gcells, spacing=3) if len(gcells) > 1 else gcells[0]
# 操作按钮 — IconButton 图标居中,无点击偏移
# 操作按钮
_ICON = 22 # 图标大小,比默认 18 好按
if t.enabled:
btns = [
ft.IconButton(ft.Icons.PAUSE, icon_size=18, tooltip="暂停",
ft.IconButton(ft.Icons.PAUSE, icon_size=_ICON, tooltip="暂停",
on_click=lambda e, tt=t: self._dialogs.confirm_stop(tt)),
ft.IconButton(ft.Icons.SETTINGS, icon_size=_ICON, tooltip="设置",
icon_color='#BDBDBD',
on_click=lambda e, tt=t: self._dialogs.open_config(tt)),
ft.IconButton(ft.Icons.REMOVE, icon_size=_ICON, tooltip="运行中不可移除",
icon_color='#BDBDBD',
on_click=None),
]
else:
btns = [
ft.IconButton(ft.Icons.PLAY_ARROW, icon_size=18, tooltip="启动",
ft.IconButton(ft.Icons.PLAY_ARROW, icon_size=_ICON, tooltip="启动",
on_click=lambda e, tt=t: self._dialogs.confirm_start(tt)),
ft.IconButton(ft.Icons.SETTINGS, icon_size=_ICON, tooltip="设置",
on_click=lambda e, tt=t: self._dialogs.open_config(tt)),
ft.IconButton(ft.Icons.REMOVE, icon_size=_ICON, tooltip="移除",
on_click=lambda e, tt=t: self._dialogs.confirm_remove(tt)),
]
btns.append(ft.IconButton(ft.Icons.SETTINGS, icon_size=18, tooltip="设置",
icon_color='#BDBDBD' if t.enabled else None,
on_click=lambda e, tt=t: self._dialogs.open_config(tt)))
cells_text = [str(tid), f'{t.stock_code} {t.stock_name}',
f'{mp:.3f}', gcell,
@@ -331,11 +395,11 @@ class _GridPanel:
elif isinstance(content, str):
content = _text(content)
elif isinstance(content, list):
content = ft.Row(content, spacing=2)
content = ft.Row(content, spacing=6)
if e > 0:
row_cells.append(ft.Container(content, padding=4, expand=e))
else:
row_cells.append(ft.Container(content, width=w, padding=4 if i != 7 else 2))
row_cells.append(ft.Container(content, width=w, padding=4))
row = ft.Row(row_cells, spacing=0)
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
rows.append(ft.Divider(height=1, color='#f0f0f0'))
@@ -382,7 +446,6 @@ class _DrawerPanel:
panel = ft.Container(
ft.Column([
ft.Container(_text("监控面板", bold=True, size=14), padding=ft.Padding(10, 10, 10, 5)),
ft.Tabs(ft.Column([bar, view], expand=True), length=4, expand=True),
], expand=True),
width=700, bgcolor=ft.Colors.SURFACE,
@@ -416,26 +479,32 @@ class _DrawerPanel:
self._refresh_market()
def _refresh_grid(self):
W = [35, 180, 80, 60, 70, 65] # ID, 股票, 市场价, 持仓, 成本, 操作
# (width, expand): 0=固定宽, >0=弹性比重; 股票列 expand 自动填充剩余空间
_C = [(35, 0), (0, 1), (80, 0), (60, 0), (70, 0), (65, 0)]
H = ["ID", "股票", "市场价", "持仓", "成本", "操作"]
rows = [
ft.Row([ft.Container(_text(h, bold=True), width=w, padding=4)
for h, w in zip(H, W)], spacing=0),
ft.Divider(height=1, color='#e0e0e0'),
]
def _cell(text, w, e, color=None):
if e > 0:
return ft.Container(_text(text, color=color), padding=4, expand=e)
return ft.Container(_text(text, color=color), width=w, padding=4)
header = ft.Row([_cell(h, w, e) for h, (w, e) in zip(H, _C)], spacing=0)
rows = [header, ft.Divider(height=1, color='#e0e0e0')]
for tid, t in self._data.tradeTargets.items():
if t.strategy_type == STRATEGY_TYPE_GRID: continue
mp = self._data.marketPrices.get(tid, 0) or 0
row = ft.Row([
ft.Container(_text(str(tid)), width=W[0], padding=4),
ft.Container(_text(f'{t.stock_code} {t.stock_name}'), width=W[1], padding=4),
ft.Container(_text(f'{mp:.3f}'), width=W[2], padding=4),
ft.Container(_text(str(t.current_position)), width=W[3], padding=4),
ft.Container(_text(f'{self._data.avgPrices.get(tid, 0):.3f}'), width=W[4], padding=4),
ft.Container(ft.IconButton(ft.Icons.SETTINGS, icon_size=18, tooltip="网格配置",
cells = [
_cell(str(tid), *_C[0]),
_cell(f'{t.stock_code} {t.stock_name}', *_C[1]),
_cell(f'{mp:.3f}', *_C[2]),
_cell(str(t.current_position), *_C[3]),
_cell(f'{self._data.avgPrices.get(tid, 0):.3f}', *_C[4]),
ft.Container(ft.IconButton(ft.Icons.SETTINGS, icon_size=20, tooltip="网格配置",
on_click=lambda e, tt=t: self._dialogs.open_config(tt)),
width=W[5], padding=0),
], spacing=0)
width=_C[5][0], padding=0),
]
row = ft.Row(cells, spacing=0)
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
rows.append(ft.Divider(height=1, color='#f0f0f0'))
self._uncl_col.controls = rows
@@ -455,10 +524,57 @@ class _DrawerPanel:
[_datatable(cols, rows, [65,55,70,35,65,60,70,55])], expand=True)]
def _refresh_market(self):
rows = [[d['time'], f"{d['stock_name']}-{sc}", f"{d['last_price']:.3f}"]
for sc, d in self._data.marketLog.items()]
self._market_table.controls = [ft.ListView(
[_datatable(["时间", "股票名称", "最新价格"], rows)], expand=True)]
# (width, expand): 0=固定宽, >0=弹性比重; 股票名称列 expand 自动填充剩余空间
_C = [(80, 0), (0, 1), (80, 0), (45, 0)]
H = ["时间", "股票名称", "最新价格", "操作"]
def _header_cell(text, w, e):
if e > 0:
return ft.Container(_text(text, bold=True), padding=4, expand=e)
return ft.Container(_text(text, bold=True), width=w, padding=4)
header = ft.Row([_header_cell(h, w, e) for h, (w, e) in zip(H, _C)], spacing=0)
rows = [header, ft.Divider(height=1, color='#e0e0e0')]
for sc, d in self._data.marketLog.items():
plain = _plain(sc)
already_in_pool = plain in self._data.stockCodeIdMap
def _data_cell(text, w, e, color=None, size=None):
if e > 0:
return ft.Container(_text(text, color=color, size=size), padding=4, expand=e)
return ft.Container(_text(text, color=color, size=size), width=w, padding=4)
cells = [
_data_cell(d.get('time', ''), *_C[0]),
_data_cell(f"{d['stock_name']}-{sc}", *_C[1]),
_data_cell(f"{d['last_price']:.3f}", *_C[2]),
]
if already_in_pool:
cells.append(_data_cell("已在池", *_C[3], color='#AAAAAA', size=11))
else:
cells.append(ft.Container(
ft.IconButton(ft.Icons.ADD, icon_size=20, tooltip="添加持仓",
on_click=lambda e, code=sc: self._on_add_from_market(code)),
width=_C[3][0], padding=0))
row = ft.Row(cells, spacing=0)
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
rows.append(ft.Divider(height=1, color='#f0f0f0'))
self._market_table.controls = [ft.Column(rows, spacing=0)]
def _on_add_from_market(self, stock_code: str):
"""+ 按钮:从市场监控添加标的并打开网格配置"""
info = self._data.marketLog.get(stock_code)
if not info:
return
stock_name = info.get('stock_name', '')
tid = self._data.add_from_market(stock_code, stock_name)
target = self._data.tradeTargets.get(tid)
if target is None:
return
# 打开网格配置弹窗
self._dialogs.open_config(target)
# ══════════════════════════════════════════════════════════════
@@ -641,6 +757,31 @@ class _TradeDialogs:
self._on_done()
PrintLog(LogLevel.INFO, f'[Flet] 暂停交易: {target.targetName()}')
def confirm_remove(self, target):
"""移除网格策略 — 根据持仓量显示不同提示"""
if target.current_position > 0:
msg = f"该标的当前持仓 {target.current_position} 股,将移回「未分类持仓」。\n\n确定要移除网格策略吗?\n\n{target.stock_code} {target.stock_name}"
else:
msg = f"该标的当前无持仓,将直接删除。\n\n确定要移除吗?\n\n{target.stock_code} {target.stock_name}"
dlg = ft.AlertDialog(
title=_text("确认移除"),
content=_text(msg),
actions=[
ft.TextButton("取消", on_click=lambda e: self._close()),
ft.TextButton("确定", on_click=lambda e, t=target: self._do_remove(t)),
],
)
self._page.show_dialog(dlg)
def _do_remove(self, target):
self._close()
result = self._data.remove_target(target.get_id())
self._on_done()
if result == 'unclassified':
PrintLog(LogLevel.INFO, f'[Flet] 降级为未分类: {target.targetName()}')
elif result == 'deleted':
PrintLog(LogLevel.INFO, f'[Flet] 已删除: {target.targetName()}')
def _close(self):
self._page.pop_dialog()
self._page.update()
@@ -737,11 +878,21 @@ class QmtApp:
on_click=lambda e: self._toggle_drawer())
self._overlay = self._drawer.build()
# 市场状态指示灯
self._market_dot = ft.Container(width=8, height=8, border_radius=4,
bgcolor='#AAAAAA')
self._market_label = _text("休市", color='#888888', size=12)
market_status = ft.Row([self._market_dot, self._market_label], spacing=6,
alignment=ft.MainAxisAlignment.CENTER)
# 应用启动时同步当前状态
self._update_market_status(qmtv.isMarketActive)
# 顶部标题栏 — 左侧市场状态,右侧刷新 + 侧栏按钮
title = ft.Container(
ft.Row([
_text("网格策略持仓", bold=True, size=13),
market_status,
ft.Row([
ft.IconButton(ft.Icons.REFRESH, tooltip="刷新", icon_size=18,
ft.IconButton(ft.Icons.REFRESH, tooltip="刷新", icon_size=20,
on_click=lambda e: self._manual_refresh()),
self._sidebar_icon,
], spacing=0),
@@ -749,8 +900,17 @@ class QmtApp:
padding=ft.Padding(10, 10, 10, 5),
)
grid_body = ft.Container(content=self._grid.build(), expand=True,
padding=ft.Padding(10, 0, 10, 10))
# 网格持仓面板 — 标题 + 数据表,与右侧 drawer 风格一致
grid_body = ft.Container(
ft.Column([
ft.Container(_text("网格策略持仓", bold=True, size=14),
padding=ft.Padding(10, 10, 10, 5)),
ft.Container(content=self._grid.build(), expand=True,
padding=ft.Padding(10, 0, 10, 10)),
], expand=True),
bgcolor=ft.Colors.SURFACE,
expand=True,
)
self.page.add(ft.Column([
title, ft.Stack([grid_body, self._overlay], expand=True),
@@ -761,6 +921,8 @@ class QmtApp:
# ══════════════════════════════════════════════════════════
def _refresh_ui(self):
self._data.refresh_orders()
self._data.refresh_trades()
self._grid._rebuild()
self._drawer.refresh_all()
self.page.update()
@@ -795,7 +957,19 @@ class QmtApp:
self._refresh_ui()
def _on_market_active(self, is_active: bool):
pass
self._update_market_status(is_active)
self.page.update()
def _update_market_status(self, active: bool):
"""更新顶栏市场状态指示灯"""
if active:
self._market_dot.bgcolor = '#4CAF50'
self._market_label.value = '盘中'
self._market_label.color = '#2E7D32'
else:
self._market_dot.bgcolor = '#AAAAAA'
self._market_label.value = '休市'
self._market_label.color = '#888888'
# ══════════════════════════════════════════════════════════
# 后台刷新