This commit is contained in:
2026-06-04 18:17:48 +08:00
parent db910e03d6
commit 1816d585bf
7 changed files with 894 additions and 340 deletions
+19 -3
View File
@@ -2,6 +2,10 @@ from peewee import CharField, IntegerField, FloatField, BooleanField
from core.database import BaseModel, db
# 策略类型常量
STRATEGY_TYPE_UNCLASSIFIED = 0 # 未分类持仓
STRATEGY_TYPE_GRID = 1 # 网格策略
# 定义Target类,对应targets表
class SFGridTradeTarget(BaseModel):
@@ -14,10 +18,11 @@ class SFGridTradeTarget(BaseModel):
grid_total_profit = FloatField(default=0.0)
status = IntegerField(default=0) # -1表示新标的,未完成交易配置,0表示新标的,已完成交易配置,1表示已建初始仓,正常交易中
enabled = BooleanField(default=False) # 是否启动交易线程
strategy_type = IntegerField(default=0) # 0=未分类, 1=网格策略
grid_start_price = FloatField(default=10.0) # 基线价格
grid_size = FloatField(default=0.1) # 网格价位差
grid_volume = IntegerField(default=100) # 网格交易量
grid_size = FloatField(default=1.0) # 网格价位差
grid_volume = IntegerField(default=200) # 网格交易量
grid_upper_count = IntegerField(default=1) # 基线价格上方网格数
grid_lower_count = IntegerField(default=10) # 基线价格下方网格数
@@ -41,4 +46,15 @@ class SFGridTradeTarget(BaseModel):
return self.priceGrid
db.create_tables([SFGridTradeTarget])
db.create_tables([SFGridTradeTarget])
# 数据库迁移: 为已有表添加 strategy_type 字段(如果不存在)
try:
from playhouse.migrate import migrate, SqliteMigrator
migrator = SqliteMigrator(db)
migrate(
migrator.add_column('sfgridtradetarget', 'strategy_type', SFGridTradeTarget.strategy_type),
)
except Exception:
# 字段已存在或迁移失败 — 静默跳过
pass
+10 -3
View File
@@ -20,11 +20,12 @@ class SFGridStrategy:
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
self.todayUpStopPrice=qmtv.dailyUpStop(tradeTarget.stock_code) # type: ignore
self.todayDownStopPrice=qmtv.dailyDownStop(tradeTarget.stock_code) # type: ignore
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}初始化: 停涨价 {self.todayUpStopPrice:.3f}, 停跌价 {self.todayDownStopPrice:.3f}')
PrintLog(LogLevel.INFO, f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造开始: grid_index={tradeTarget.grid_index}, status={tradeTarget.status}, enabled={tradeTarget.enabled}')
self.orderGrid = {} # grid index, order_seq | order_id
self.loadExistOrders()
self.enabledTrading(tradeTarget.enabled) # type: ignore
self.dataUpdateLock = threading.Lock()
PrintLog(LogLevel.INFO, f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造结束: grid_index={self.tradeTarget.grid_index}')
def loadExistOrders(self):
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
@@ -123,13 +124,17 @@ class SFGridStrategy:
self.dataUpdateLock.release()
def enabledTrading(self, enabled: bool) -> model.SFGridTradeTarget:
PrintLog(LogLevel.INFO, f" |- [DEBUG] enabledTrading({enabled}) 调用前: grid_index={self.tradeTarget.grid_index}, status={self.tradeTarget.status}")
self.tradeTarget.enabled = enabled # type: ignore
if enabled:
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易启动, 持仓量:{self.tradeTarget.current_position}")
if self.tradeTarget.status == 0: # 未建仓
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}初始状态, 设置网格序号 1,")
self.tradeTarget.grid_index = 1 # pyright: ignore[reportAttributeAccessIssue]
if self.tradeTarget.grid_index == 0:
self.tradeTarget.grid_index = 1 # pyright: ignore[reportAttributeAccessIssue]
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}初始状态, 设置网格序号 1,")
else:
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}初始状态, 保留网格序号 {self.tradeTarget.grid_index},")
else: # 已建仓
# 交易阶段,检查仓位,检查现有订单
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 无需建初始仓 当前仓位: {self.tradeTarget.current_position} 状态: {self.tradeTarget.status}")
@@ -186,6 +191,7 @@ class SFGridStrategy:
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] - 建仓单成交')
self.tradeTarget.status = 1 # type: ignore
self.tradeTarget.init_price = trade.traded_price # type: ignore
PrintLog(LogLevel.INFO, f'|- [DEBUG] 建仓单成交: grid_index {self.tradeTarget.grid_index} → 1')
self.tradeTarget.grid_index = 1 # type: ignore
type = "建仓单"
else:
@@ -218,6 +224,7 @@ class SFGridStrategy:
return "SFGRID"
def saveProxy(self):
PrintLog(LogLevel.DEBUG, f'|- [DEBUG] saveProxy: {self.tradeTarget.targetName()} grid_index={self.tradeTarget.grid_index}, status={self.tradeTarget.status}')
rc = self.tradeTarget.save()
event_bus.publish(EventTradeTargetUpdate, self.tradeTarget)
return rc
+470 -165
View File
@@ -24,6 +24,8 @@ class TradeTargetUI(ttk.Frame):
self.listening_stock = []
# 监控价格,默认值为10
self.monitor_price = 10.0
# 追踪最后点击的表格 (0=网格, 1=未分类)
self._active_table = 0
self.init_trade_target_pool()
@@ -32,20 +34,66 @@ class TradeTargetUI(ttk.Frame):
# 市场监控窗口显示状态
self.market_monitor_visible = True
# 市场活跃状态 + 刷新控制
self._market_active = qmtv.isMarketActive # type: ignore
self._refresh_event = threading.Event()
self._prices_pulled_after_close = False # 收盘后是否已拉取过
# 创建界面
self.create_ui()
eBus.event_bus.subscribe(eBus.MarketDataUpdate, self.onMarketDataUpdated)
eBus.event_bus.subscribe(eBus.EventMarketActiveSwitch, self._on_market_active_switch)
eBus.event_bus.subscribe(bus_events.EventTradeTargetUpdate, self.onStrategyUpdate)
eBus.event_bus.subscribe(bus_events.EventTradeTargetDeleted, self.onTradeTargetDeleted)
def init_trade_target_pool(self):
# 一次性迁移: 已配置过的标的 (status >= 0) → 网格策略
from core.sfgrid.model import STRATEGY_TYPE_GRID
migrated = SFGridTradeTarget.update(strategy_type=STRATEGY_TYPE_GRID).where(
SFGridTradeTarget.status >= 0
).execute()
if migrated:
PrintLog(LogLevel.INFO, f'- [迁移] {migrated} 个已配置标的标记为网格策略')
# 一次性从 QMT 获取全部持仓
all_positions = qmtv.getAllPositions()
PrintLog(LogLevel.INFO, f'- [持仓] 从 QMT 获取到 {len(all_positions)} 个持仓')
# 自动将 QMT 持仓导入到数据库(持仓但未在交易池中的标的)
imported_count = 0
for code, pos in all_positions.items():
existing = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == code)
if existing is None:
name = getattr(pos, 'instrument_name', '') or qmtv.getInstrumentName(code)
volume = int(pos.volume)
avg_price = float(pos.avg_price) if pos.avg_price else 0.0
SFGridTradeTarget.create(
stock_code=code,
stock_name=name,
current_position=volume,
grid_index=0,
init_price=avg_price,
grid_match_count=0,
grid_total_profit=0.0,
status=-1,
enabled=False,
grid_start_price=avg_price if avg_price > 0 else 10.0,
grid_size=1.0,
grid_volume=200,
grid_upper_count=1,
grid_lower_count=10
)
imported_count += 1
PrintLog(LogLevel.INFO, f'- [导入] QMT持仓 → 交易池: {code} {name} 持仓:{volume} 成本:{avg_price:.4f}')
if imported_count:
PrintLog(LogLevel.INFO, f'- [导入] 共新增 {imported_count} 个标的到交易池')
results = SFGridTradeTarget.select()
for temp in results:
tradeTarget:SFGridTradeTarget = temp
pos = qmtv.getStockPosition(tradeTarget.stock_code)
pos = all_positions.get(tradeTarget.stock_code)
tradeTarget.current_position = 0 if pos is None else pos.volume # type: ignore
if pos is None:
self.targetAvgPrice[tradeTarget.get_id()] = 0.0
@@ -53,7 +101,9 @@ class TradeTargetUI(ttk.Frame):
self.targetAvgPrice[tradeTarget.get_id()] = pos.avg_price
PrintLog(LogLevel.INFO, f'- [成功]获取持仓信息: {tradeTarget.stock_code} {tradeTarget.targetName()} {tradeTarget.current_position} {pos.avg_price}')
PrintLog(LogLevel.DEBUG, f'- [DEBUG] updateTradeTarget 前: {tradeTarget.stock_code} grid_index={tradeTarget.grid_index}, status={tradeTarget.status}, enabled={tradeTarget.enabled}')
self.updateTradeTarget(tradeTarget, True) # 初始化的时候
PrintLog(LogLevel.DEBUG, f'- [DEBUG] updateTradeTarget 后: {tradeTarget.stock_code} grid_index={tradeTarget.grid_index}')
PrintLog(LogLevel.INFO, f'- [成功]交易标的信息初始化, 共 {len(self.tradeTargetData)} 个标的')
@@ -99,13 +149,19 @@ class TradeTargetUI(ttk.Frame):
target.save()
id = target.get_id()
# PrintLog(LogLevel.INFO, f' [序号-{id}] 股票代码: {target.stock_code}-{target.stock_name}: {target.plan_buy_price} {target.plan_sell_price}') # type: ignore
# 更新或添加数据到本地缓存
self.tradeTargetData[id] = target
if id not in self.strategy_ctrl:
# 注册到 stockCodeIdMap(所有标的都需要行情数据)
if id not in self.stockCodeIdMap.values() and target.stock_code not in self.stockCodeIdMap:
self.stockCodeIdMap[target.stock_code] = id # type: ignore
self.strategy_ctrl[id] = SFGridStrategy(target) # pyright: ignore[reportArgumentType]
# 只有网格策略标的才创建策略控制器
from core.sfgrid.model import STRATEGY_TYPE_GRID
if target.strategy_type == STRATEGY_TYPE_GRID: # type: ignore
if id not in self.strategy_ctrl:
self.strategy_ctrl[id] = SFGridStrategy(target) # pyright: ignore[reportArgumentType]
if id in self.targetAvgPrice:
pos = qmtv.getStockPosition(target.stock_code)
if pos is not None:
@@ -118,35 +174,7 @@ class TradeTargetUI(ttk.Frame):
main_frame = ttk.Frame(self)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建工具栏
toolbar_frame = ttk.Frame(main_frame)
toolbar_frame.pack(fill=tk.X, pady=(0, 10))
# 工具栏按钮
ttk.Button(toolbar_frame, text=" 添加标的",
command=self.btnHandlerAddTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🗑 删除标的",
command=self.btnHandlerDelSelectedTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="▶️ 启动交易",
command=self.btnHandlerStartSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="⏸ 暂停交易",
command=self.btnHandlerStopSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🛠 交易设置",
command=self.btnHandlerTradeSettings, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="▣ 边栏",
command=self.btnHandlerToggleMarketMonitor, width=8).pack(side=tk.RIGHT, padx=2)
# 添加价格监控输入字段和确认按钮
ttk.Button(toolbar_frame, text="确认",
command=self.btnHandlerSetMonitorPrice, width=8).pack(side=tk.RIGHT, padx=2)
self.monitor_price_entry = ttk.Entry(toolbar_frame, width=8)
self.monitor_price_entry.insert(0, str(self.monitor_price))
self.monitor_price_entry.pack(side=tk.RIGHT, padx=2)
ttk.Label(toolbar_frame, text="价格").pack(side=tk.RIGHT, padx=(20, 2))
ttk.Label(toolbar_frame, text="监控配置").pack(side=tk.RIGHT, padx=(20, 2))
# 表格区域
# 表格区域(左右布局:左侧=工具栏+持仓表格,右侧=Notebook标签页)
self.create_tables_area(main_frame)
# 启动刷新线程
@@ -154,76 +182,203 @@ class TradeTargetUI(ttk.Frame):
self.refresh_thread.start()
def _on_market_active_switch(self, is_active: bool):
"""市场活跃状态变更回调"""
self._market_active = is_active
if is_active:
self._prices_pulled_after_close = False
self._refresh_event.set() # 唤醒 refresh_loop
def refresh_loop(self):
"""刷新循环"""
"""刷新循环(后台线程:拉取市价 + 调度UI刷新)"""
while True:
self.after(0, self.refresh_table)
self.after(0, self.populate_market_table)
time.sleep(0.5) # 每0.5秒刷新一次
if self._market_active:
# 盘中:每5s拉取缺失的市价
prices = {} # id -> price
for id, target in list(self.tradeTargetData.items()):
if id not in self.targetMarketPrice or self.targetMarketPrice[id] == 0:
price = qmtv.getLastPrice(target.stock_code) # type: ignore
if price > 0:
prices[id] = price
if prices:
self.after(0, lambda p=prices: self._apply_prices(p))
self.after(0, self.refresh_table)
self.after(0, self.populate_market_table)
self._refresh_event.wait(timeout=5)
self._refresh_event.clear()
else:
# 收盘后:只拉取一次收盘价,之后等待市场重新活跃
if not self._prices_pulled_after_close:
prices = {}
for id, target in list(self.tradeTargetData.items()):
if id not in self.targetMarketPrice or self.targetMarketPrice[id] == 0:
price = qmtv.getLastPrice(target.stock_code) # type: ignore
if price > 0:
prices[id] = price
if prices:
self.after(0, lambda p=prices: self._apply_prices(p))
self.after(0, self.refresh_table)
self.after(0, self.populate_market_table)
self._prices_pulled_after_close = True
PrintLog(LogLevel.INFO, '[刷新] 收盘后已拉取收盘价,进入休眠等待开盘')
# 休眠直到市场重新活跃(或每60s检查一次以防漏信号)
self._refresh_event.wait(timeout=60)
self._refresh_event.clear()
def _apply_prices(self, prices: dict):
"""主线程回调:将后台拉取的市价写入缓存"""
for id, price in prices.items():
self.targetMarketPrice[id] = price
if id in self.tradeTargetData:
self.tradeTargetData[id].market_price = price # type: ignore
def create_tables_area(self, parent):
"""创建表格区域"""
"""创建表格区域 — 左侧工具栏+持仓表格,右侧Notebook"""
# 创建主表格框架(水平排列)
tables_frame = ttk.Frame(parent)
tables_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
# 左侧交易标的区域
trade_frame = ttk.LabelFrame(tables_frame, text="交易标的详情", padding=10)
trade_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# 创建交易标的表格
self.create_trade_target_table(trade_frame)
# 右侧市场监控区域
self.market_frame = ttk.LabelFrame(tables_frame, text="市场监控", padding=10)
self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
# 创建市场监控表格
tables_frame.pack(fill=tk.BOTH, expand=True)
# ========== 左侧:工具栏 + 持仓表格 ==========
left_frame = ttk.Frame(tables_frame)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# 工具栏(在左侧顶部)
toolbar_frame = ttk.Frame(left_frame)
toolbar_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Button(toolbar_frame, text=" 添加标的",
command=self.btnHandlerAddTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🗑 删除标的",
command=self.btnHandlerDelSelectedTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="▶️ 启动交易",
command=self.btnHandlerStartSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="⏸ 暂停交易",
command=self.btnHandlerStopSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🛠 交易设置",
command=self.btnHandlerTradeSettings, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="▣ 边栏",
command=self.btnHandlerToggleMarketMonitor, width=8).pack(side=tk.RIGHT, padx=2)
# 上半部分: 网格策略持仓
grid_frame = ttk.LabelFrame(left_frame, text="网格策略持仓", padding=5)
grid_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 3))
self.create_grid_table(grid_frame)
# 下半部分: 未分类持仓
unclassified_frame = ttk.LabelFrame(left_frame, text="未分类持仓", padding=5)
unclassified_frame.pack(fill=tk.BOTH, expand=True, pady=(3, 0))
self.create_unclassified_table(unclassified_frame)
# 右侧: Notebook 标签页容器
self.right_notebook = ttk.Notebook(tables_frame)
self.right_notebook.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
# Tab 1: 实时价格监控
self.market_frame = ttk.Frame(self.right_notebook)
self.right_notebook.add(self.market_frame, text="实时价格监控")
# 价格监控控件(tab 内顶部)
monitor_control_frame = ttk.Frame(self.market_frame)
monitor_control_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(monitor_control_frame, text="监控配置").pack(side=tk.LEFT, padx=(0, 5))
ttk.Label(monitor_control_frame, text="价格").pack(side=tk.LEFT, padx=(0, 2))
self.monitor_price_entry = ttk.Entry(monitor_control_frame, width=8)
self.monitor_price_entry.insert(0, str(self.monitor_price))
self.monitor_price_entry.pack(side=tk.LEFT, padx=2)
ttk.Button(monitor_control_frame, text="确认",
command=self.btnHandlerSetMonitorPrice, width=6).pack(side=tk.LEFT, padx=2)
self.create_market_monitor_table(self.market_frame)
# Tab 2: 订单记录 (占位)
model_tab = ttk.Frame(self.right_notebook)
self.right_notebook.add(model_tab, text="订单记录")
ttk.Label(model_tab, text="订单记录 - 待实现", font=('Arial', 12),
foreground='gray').pack(expand=True)
# Tab 3: 成交记录 (占位)
dataset_tab = ttk.Frame(self.right_notebook)
self.right_notebook.add(dataset_tab, text="成交记录")
ttk.Label(dataset_tab, text="成交记录 - 待实现", font=('Arial', 12),
foreground='gray').pack(expand=True)
def create_trade_target_table(self, parent):
"""创建交易标的表格"""
def create_grid_table(self, parent):
"""创建网格策略表格"""
columns = ("ID",
"股票代码", "股票名称", "市场价", "当前持仓", "建仓成本",
"平均成本", "网格匹配次数", "网格收益", "交易状态"
"股票", "市场价", "当前持仓",
"平均成本", "当前网格基准价", "交易状态"
)
self.trade_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
# 专业化的列配置
self.grid_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
column_configs = {
"ID": (50, tk.CENTER),
"股票代码": (80, tk.CENTER),
"股票名称": (80, tk.E),
"市场价": (70, tk.E),
"当前持仓": (80, tk.E),
"建仓成本": (60, tk.E),
"股票": (120, tk.CENTER),
"市场价": (60, tk.E),
"当前持仓": (70, tk.E),
"平均成本": (60, tk.E),
"网格匹配次数": (60, tk.E),
"网格收益": (60, tk.E),
"交易状态": (80, tk.CENTER)
"当前网格基准价": (100, tk.E),
"交易状态": (100, tk.CENTER)
}
for col in columns:
width, anchor = column_configs[col]
self.trade_table.heading(col, text=col)
self.trade_table.column(col, width=width, anchor=anchor) # type: ignore
# 填充数据
self.populate_trade_table()
# 滚动条
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.trade_table.yview)
self.trade_table.configure(yscrollcommand=scrollbar.set)
self.trade_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.grid_table.heading(col, text=col)
self.grid_table.column(col, width=width, anchor=anchor) # type: ignore
self.populate_grid_table()
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.grid_table.yview)
self.grid_table.configure(yscrollcommand=scrollbar.set)
self.grid_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 绑定双击事件
self.trade_table.bind("<Double-1>", self.on_table_double_click)
self.grid_table.bind("<Double-1>", self.on_grid_table_double_click)
self.grid_table.bind("<Button-3>", self.on_grid_table_right_click)
self.grid_table.bind("<Button-1>", lambda e: setattr(self, '_active_table', 0))
def create_unclassified_table(self, parent):
"""创建未分类持仓表格"""
columns = ("ID",
"股票", "市场价", "当前持仓",
"平均成本"
)
self.unclassified_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
column_configs = {
"ID": (50, tk.CENTER),
"股票": (120, tk.CENTER),
"市场价": (60, tk.E),
"当前持仓": (70, tk.E),
"平均成本": (60, tk.E),
}
for col in columns:
width, anchor = column_configs[col]
self.unclassified_table.heading(col, text=col)
self.unclassified_table.column(col, width=width, anchor=anchor) # type: ignore
self.populate_unclassified_table()
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.unclassified_table.yview)
self.unclassified_table.configure(yscrollcommand=scrollbar.set)
self.unclassified_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.unclassified_table.bind("<Double-1>", self.on_unclassified_table_double_click)
self.unclassified_table.bind("<Button-3>", self.on_unclassified_table_right_click)
self.unclassified_table.bind("<Button-1>", lambda e: setattr(self, '_active_table', 1))
def create_market_monitor_table(self, parent):
"""创建市场监控表格"""
@@ -343,80 +498,175 @@ class TradeTargetUI(ttk.Frame):
else:
return "⏸ 已停止"
def populate_trade_table(self):
"""填充交易标的表格数据"""
# ---- 右键菜单 ----
def on_grid_table_right_click(self, event):
"""网格策略表格右键菜单"""
item = self.grid_table.identify_row(event.y)
if item:
self.grid_table.selection_set(item)
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="移回未分类", command=self.move_to_unclassified)
menu.post(event.x_root, event.y_root)
def on_unclassified_table_right_click(self, event):
"""未分类持仓表格右键菜单"""
item = self.unclassified_table.identify_row(event.y)
if item:
self.unclassified_table.selection_set(item)
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="添加到网格策略", command=self.move_to_grid)
menu.post(event.x_root, event.y_root)
def move_to_grid(self):
"""打开网格配置窗口,保存后自动标记为网格策略"""
target = self.get_selected_target()
if not target:
return
# 对未分类标的应用新默认值(仅内存,不存库)
if target.strategy_type == 0: # type: ignore
target.grid_size = 1.0 # type: ignore
target.grid_volume = 200 # type: ignore
# 不提前标记策略类型,等用户配置保存后由 save_config 自动设置
self.create_grid_config_window(target)
def move_to_unclassified(self):
"""将网格策略标的移回未分类"""
target = self.get_selected_target()
if not target:
return
result = messagebox.askyesno("确认", f"{target.targetName()} 移回未分类?\n\n网格配置将保留但交易将停止。")
if not result:
return
from core.sfgrid.model import STRATEGY_TYPE_UNCLASSIFIED
# 如果正在交易,先停止
if target.enabled and target.get_id() in self.strategy_ctrl: # type: ignore
self.strategy_ctrl[target.get_id()].enabledTrading(False)
target.strategy_type = STRATEGY_TYPE_UNCLASSIFIED # type: ignore
target.enabled = False # type: ignore
target.save()
self.refresh_table()
PrintLog(LogLevel.INFO, f'- [分类] {target.targetName()} → 未分类')
def populate_grid_table(self):
"""填充网格策略表格数据"""
for id, target in self.tradeTargetData.items():
from core.sfgrid.model import STRATEGY_TYPE_GRID
if target.strategy_type != STRATEGY_TYPE_GRID: # type: ignore
continue
grid_info = '-'
if target.status >= 0:
grid_idx = target.grid_index
price_grid = target.getPriceGrid()
if 0 <= grid_idx < len(price_grid):
grid_info = f'{grid_idx}({price_grid[grid_idx]:.2f}元)'
else:
grid_info = f'{grid_idx}(?)'
values = [
id,
target.stock_code, # "股票代码"
target.stock_name, # "股票名称"
f"{self.targetMarketPrice[id]:.3f}" if id in self.targetMarketPrice else '-', # "市场价"
target.current_position, # "当前持仓"
'-' if target.init_price is None else f"{target.init_price:.3f}", # "建仓成本"
f"{self.targetAvgPrice[id]:.3f}", # "平均成本"
target.grid_match_count, # "网格匹配次数"
f"{target.grid_total_profit:.3f}", # "网格收益"
self.get_trade_enabled_indicator(target) # type: ignore
f"{target.stock_code} {target.stock_name}",
f"{self.targetMarketPrice[id]:.3f}" if id in self.targetMarketPrice else '-',
target.current_position,
f"{self.targetAvgPrice[id]:.3f}",
grid_info,
self.get_trade_enabled_indicator(target)
]
self.trade_table.insert('', tk.END, values=values)
def on_table_double_click(self, event):
"""表格双击事件"""
selected = self.trade_table.selection()
self.grid_table.insert('', tk.END, values=values)
def populate_unclassified_table(self):
"""填充未分类持仓表格数据"""
for id, target in self.tradeTargetData.items():
from core.sfgrid.model import STRATEGY_TYPE_UNCLASSIFIED
if target.strategy_type != STRATEGY_TYPE_UNCLASSIFIED: # type: ignore
continue
values = [
id,
f"{target.stock_code} {target.stock_name}",
f"{self.targetMarketPrice[id]:.3f}" if id in self.targetMarketPrice else '-',
target.current_position,
f"{self.targetAvgPrice[id]:.3f}",
]
self.unclassified_table.insert('', tk.END, values=values)
def on_grid_table_double_click(self, event):
"""网格表格双击事件"""
selected = self.grid_table.selection()
if selected:
item = selected[0]
values = self.trade_table.item(item)['values']
ctrl = self.strategy_ctrl[values[0]]
PrintLog(LogLevel.DEBUG, f"双击查看详情: {values[0]} - {values[1]}")
PrintLog(LogLevel.DEBUG, f"双击查看详情 - 订单网格")
ctrl.printPendingOrder()
values = self.grid_table.item(item)['values']
target_id = int(values[0])
if target_id in self.strategy_ctrl:
ctrl = self.strategy_ctrl[target_id]
PrintLog(LogLevel.DEBUG, f"双击查看详情: {values[0]} - {values[1]}")
PrintLog(LogLevel.DEBUG, f"双击查看详情 - 订单网格")
ctrl.printPendingOrder()
def on_unclassified_table_double_click(self, event):
"""未分类表格双击 — 添加到网格策略"""
self.move_to_grid()
def get_selected_target(self):
"""获取选中的交易标的"""
selected = self.trade_table.selection()
if not selected:
messagebox.showwarning("未选中", "请先选择一个交易标的")
return None
# 获取选中行的ID
item = selected[0]
values = self.trade_table.item(item)['values']
target_id = values[0]
# 从列表中找到对应的target对象
for id in self.tradeTargetData:
if int(target_id) == id: # type: ignore
return self.tradeTargetData[id]
"""获取选中的交易标的(根据最后点击的表格优先)"""
# 根据最后点击的表格决定检查顺序
if self._active_table == 1:
primary, secondary = self.unclassified_table, self.grid_table
else:
primary, secondary = self.grid_table, self.unclassified_table
for table in (primary, secondary):
selected = table.selection()
if selected:
item = selected[0]
values = table.item(item)['values']
target_id = values[0]
return self.tradeTargetData.get(int(target_id))
messagebox.showwarning("未选中", "请先选择一个交易标的")
return None
def refresh_table(self):
"""刷新表格数据"""
# 保存当前选中的项
selected_items = self.trade_table.selection()
"""刷新表格数据(纯UI操作,在主线程执行)"""
# 刷新网格策略表格
selected_items = self.grid_table.selection()
selected_values = []
for item in selected_items:
values = self.trade_table.item(item)['values']
values = self.grid_table.item(item)['values']
if values:
selected_values.append(values[0]) # 保存ID
# 清空表格
for item in self.trade_table.get_children():
self.trade_table.delete(item)
# 重新填充
self.populate_trade_table()
# 恢复之前选中的项
selected_values.append(values[0])
for item in self.grid_table.get_children():
self.grid_table.delete(item)
self.populate_grid_table()
if selected_values:
for item in self.trade_table.get_children():
values = self.trade_table.item(item)['values']
for item in self.grid_table.get_children():
values = self.grid_table.item(item)['values']
if values and values[0] in selected_values:
self.trade_table.selection_add(item)
self.grid_table.selection_add(item)
# 刷新未分类表格
unselected_items = self.unclassified_table.selection()
unselected_values = []
for item in unselected_items:
values = self.unclassified_table.item(item)['values']
if values:
unselected_values.append(values[0])
for item in self.unclassified_table.get_children():
self.unclassified_table.delete(item)
self.populate_unclassified_table()
if unselected_values:
for item in self.unclassified_table.get_children():
values = self.unclassified_table.item(item)['values']
if values and values[0] in unselected_values:
self.unclassified_table.selection_add(item)
# 刷新市场监控表格
self.populate_market_table()
@@ -451,7 +701,32 @@ class TradeTargetUI(ttk.Frame):
ttk.Label(info_frame, text=f"股票代码: {target.stock_code}").grid(row=0, column=0, sticky=tk.W, pady=2)
ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2)
ttk.Label(info_frame, text=f"状态: 已建初始仓(仅查看模式)").grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=2)
# 建仓状态(可变更)
status_text = "已建仓" if target.status >= 1 else "未建仓"
status_color = "green" if target.status >= 1 else "orange"
status_label = ttk.Label(info_frame, text=f"建仓状态: {status_text}", foreground=status_color)
status_label.grid(row=1, column=0, sticky=tk.W, pady=2)
def toggle_position_status():
new_status = 0 if target.status >= 1 else 1
setattr(target, 'status', new_status)
target.save()
new_text = "已建仓" if new_status >= 1 else "未建仓"
new_color = "green" if new_status >= 1 else "orange"
status_label.config(text=f"建仓状态: {new_text}", foreground=new_color)
toggle_btn.config(text="标记为未建仓" if new_status >= 1 else "标记为已建仓")
self.updateTradeTarget(target, False)
PrintLog(LogLevel.INFO, f"建仓状态变更: {target.stock_code}{new_text}")
toggle_btn = ttk.Button(
info_frame,
text="标记为未建仓" if target.status >= 1 else "标记为已建仓",
command=toggle_position_status
)
toggle_btn.grid(row=1, column=1, sticky=tk.W, padx=(20, 0), pady=2)
ttk.Label(info_frame, text=f"状态: 已建初始仓(仅查看模式)").grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=2)
# 创建网格配置查看框架
config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=10)
@@ -550,7 +825,17 @@ class TradeTargetUI(ttk.Frame):
ttk.Label(info_frame, text=f"股票代码: {target.stock_code}").grid(row=0, column=0, sticky=tk.W, pady=2)
ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2)
ttk.Label(info_frame, text=f"状态: 新标的(可配置模式)").grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=2)
# 建仓状态选择
ttk.Label(info_frame, text="建仓状态:", width=12).grid(row=1, column=0, sticky=tk.W, pady=2)
position_status_var = tk.StringVar(value="未建仓" if target.status < 1 else "已建仓")
position_status_combo = ttk.Combobox(
info_frame, textvariable=position_status_var,
values=["未建仓", "已建仓"], state="readonly", width=10
)
position_status_combo.grid(row=1, column=1, sticky=tk.W, padx=(20, 0), pady=2)
ttk.Label(info_frame, text=f"状态: 新标的(可配置模式)").grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=2)
# 创建网格配置框架
config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=15)
@@ -692,14 +977,23 @@ class TradeTargetUI(ttk.Frame):
setattr(target, 'grid_volume', grid_volume)
setattr(target, 'grid_upper_count', grid_upper_count)
setattr(target, 'grid_lower_count', grid_lower_count)
setattr(target, 'status', 0)
# 建仓状态: "已建仓" → 1, "未建仓" → 0
setattr(target, 'status', 1 if position_status_var.get() == "已建仓" else 0)
# grid_index 设为基准价在网格中的位置 (grid_upper_count)
setattr(target, 'grid_index', grid_upper_count)
# 自动标记为网格策略
from core.sfgrid.model import STRATEGY_TYPE_GRID
setattr(target, 'strategy_type', STRATEGY_TYPE_GRID)
# 更新策略控制器
self.updateTradeTarget(target, True) # 网格配置变更
# 关闭窗口
config_window.destroy()
# 立即刷新表格确保数据同步
self.refresh_table()
# 添加日志
PrintLog(LogLevel.INFO, f"网格配置已保存: {target.stock_code} - {target.stock_name}")
messagebox.showinfo("成功", "网格配置已保存!")
@@ -771,10 +1065,11 @@ class TradeTargetUI(ttk.Frame):
new_target = SFGridTradeTarget.create(
stock_name=stock_name,
stock_code=stock_code,
current_position="0" if pos is None else str(pos.volume),
current_position=0 if pos is None else int(pos.volume),
grid_index=gridIndex,
init_price=0.0,
status=-1
status=-1,
strategy_type=0 # 默认为未分类
)
# 更新标的池
self.updateTradeTarget(new_target, True) # 新增标的,相当于也是初始化
@@ -792,14 +1087,12 @@ class TradeTargetUI(ttk.Frame):
def btnHandlerToggleMarketMonitor(self):
"""切换市场监控窗口显示/隐藏"""
"""切换右侧面板显示/隐藏"""
if self.market_monitor_visible:
# 隐藏市场监控窗口
self.market_frame.pack_forget()
self.right_notebook.pack_forget()
self.market_monitor_visible = False
else:
# 显示市场监控窗口
self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
self.right_notebook.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
self.market_monitor_visible = True
def btnHandlerTradeSettings(self):
@@ -807,9 +1100,9 @@ class TradeTargetUI(ttk.Frame):
target = self.get_selected_target()
if not target:
return
# 检查标的的状态,status为1时仅可查看
if target.status == -1 or target.status == 0:
# 只要暂停交易就可以修改参数,运行中则仅可查看
if not target.enabled: # type: ignore
self.create_grid_config_window(target)
else:
# 创建只读的网格配置查看窗口
@@ -820,7 +1113,12 @@ class TradeTargetUI(ttk.Frame):
target = self.get_selected_target()
if not target:
return
from core.sfgrid.model import STRATEGY_TYPE_GRID
if target.strategy_type != STRATEGY_TYPE_GRID: # type: ignore
messagebox.showinfo("提示", "该标的不属于网格策略,请先转为网格策略后再启动交易。")
return
if target.status < 0:
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 未配置交易参数, 请做交易设置。")
return
@@ -853,7 +1151,12 @@ class TradeTargetUI(ttk.Frame):
target = self.get_selected_target()
if not target:
return
from core.sfgrid.model import STRATEGY_TYPE_GRID
if target.strategy_type != STRATEGY_TYPE_GRID: # type: ignore
messagebox.showinfo("提示", "该标的不属于网格策略。")
return
if not target.enabled: # type: ignore
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经是暂停状态")
return
@@ -902,8 +1205,10 @@ class TradeTargetUI(ttk.Frame):
def onTradeTargetDeleted(self, target: SFGridTradeTarget):
id = target.get_id()
del self.tradeTargetData[id]
del self.strategy_ctrl[id]
del self.stockCodeIdMap[target.stock_code] # type: ignore
if id in self.strategy_ctrl:
del self.strategy_ctrl[id]
if target.stock_code in self.stockCodeIdMap: # type: ignore
del self.stockCodeIdMap[target.stock_code] # type: ignore
def btnHandlerAddTradeTarget(self):
"""添加新的交易标的"""