update
This commit is contained in:
+2
-2
@@ -1,5 +1,5 @@
|
||||
[config]
|
||||
miniqmtpath = D:/Programs/DTQMT/userdata_mini
|
||||
account_no = 99082560
|
||||
miniqmtpath = C:/Programs/GJQMT/userdata_mini
|
||||
account_no = 8882874667
|
||||
log_level = INFO
|
||||
|
||||
|
||||
+9
-140
@@ -27,8 +27,6 @@ class MainWindow:
|
||||
|
||||
self.logLevel = LogLevel[configLogLevel]
|
||||
PrintLog(LogLevel.DEBUG, f"系统启动成功 {self.logLevel.name}")
|
||||
# 当前选中的策略Tab索引
|
||||
self.current_strategy_index = 0
|
||||
# 存储各个Frame的引用
|
||||
self.strategy_frames = {}
|
||||
# 日志面板可见性标志
|
||||
@@ -44,112 +42,23 @@ class MainWindow:
|
||||
main_container = ttk.Frame(self.root)
|
||||
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# 中间主体区域(左右布局)
|
||||
# 中间主体区域
|
||||
content_area = ttk.Frame(main_container)
|
||||
content_area.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 左侧Tab按钮栏(垂直排列)
|
||||
tab_bar_frame = ttk.Frame(content_area)
|
||||
tab_bar_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
|
||||
|
||||
# 创建自定义样式
|
||||
self.create_custom_styles()
|
||||
|
||||
# 创建Tab按钮(垂直排列,文字垂直显示)
|
||||
self.tab_buttons = []
|
||||
strategy_names = ["网格", "复盘"]
|
||||
|
||||
for idx, name in enumerate(strategy_names):
|
||||
btn = ttk.Button(
|
||||
tab_bar_frame,
|
||||
text=name,
|
||||
command=lambda i=idx: self.switch_strategy_tab(i),
|
||||
width=4,
|
||||
style='Bookmark.TButton' # 使用自定义书签样式
|
||||
)
|
||||
btn.pack(side=tk.TOP, pady=2, fill=tk.X)
|
||||
self.tab_buttons.append(btn)
|
||||
|
||||
# 在Tab按钮下方添加退出按钮和日志按钮(底部对齐)
|
||||
# 使用一个填充Frame将按钮推到底部
|
||||
spacer = ttk.Frame(tab_bar_frame)
|
||||
spacer.pack(side=tk.TOP, fill=tk.X, ipady=10)
|
||||
|
||||
# 清空日志按钮(底部第三个)
|
||||
clear_log_btn = ttk.Button(
|
||||
tab_bar_frame,
|
||||
text="🗑", # 垃圾桶图标
|
||||
command=self.clear_logs,
|
||||
width=3
|
||||
)
|
||||
clear_log_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
|
||||
|
||||
# 日志显示按钮(退出按钮上方)
|
||||
self.log_toggle_btn = ttk.Button(
|
||||
tab_bar_frame,
|
||||
text="📋", # 日志图标
|
||||
command=self.toggle_log_panel,
|
||||
width=3
|
||||
)
|
||||
self.log_toggle_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
|
||||
|
||||
# 退出按钮(最底部)
|
||||
exit_btn = ttk.Button(
|
||||
tab_bar_frame,
|
||||
text="⏻", # 电源图标
|
||||
command=self.on_exit,
|
||||
width=3
|
||||
)
|
||||
exit_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
|
||||
|
||||
# 添加垂直分隔线
|
||||
separator = ttk.Separator(content_area, orient='vertical')
|
||||
separator.pack(side=tk.LEFT, fill=tk.Y, padx=1)
|
||||
|
||||
# 右侧内容区域容器(用于放置不同策略的Frame)
|
||||
# 右侧内容区域容器
|
||||
self.content_container = ttk.Frame(content_area)
|
||||
self.content_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
# 创建各个策略的Frame
|
||||
# 创建策略Frame
|
||||
strategy_names = ["网格"]
|
||||
self.create_strategy_frames(strategy_names)
|
||||
|
||||
# 创建全局日志面板(默认隐藏)
|
||||
self.create_global_log_panel(main_container)
|
||||
|
||||
# 默认显示第一个策略
|
||||
self.switch_strategy_tab(0)
|
||||
|
||||
def create_custom_styles(self):
|
||||
"""创建自定义样式"""
|
||||
style = ttk.Style()
|
||||
|
||||
# 创建书签样式
|
||||
style.configure(
|
||||
'Bookmark.TButton',
|
||||
relief='flat',
|
||||
borderwidth=1,
|
||||
padding=(5, 10),
|
||||
foreground='black',
|
||||
background='#FFE599', # 浅黄色背景,类似便签纸
|
||||
font=('Arial', 10, 'bold')
|
||||
)
|
||||
|
||||
# 设置焦点样式(选中状态)
|
||||
style.map(
|
||||
'Bookmark.TButton',
|
||||
background=[('active', '#F1C232'), ('pressed', '#F1C232')],
|
||||
relief=[('pressed', 'sunken')]
|
||||
)
|
||||
|
||||
# 创建选中状态的书签样式
|
||||
style.configure(
|
||||
'SelectedBookmark.TButton',
|
||||
relief='flat',
|
||||
borderwidth=1,
|
||||
padding=(5, 10),
|
||||
background='#3D85C6', # 蓝色背景表示选中状态
|
||||
font=('Arial', 10, 'bold')
|
||||
)
|
||||
self.show_strategy_frame(0)
|
||||
|
||||
def create_global_log_panel(self, parent):
|
||||
"""创建全局日志面板"""
|
||||
@@ -204,62 +113,22 @@ class MainWindow:
|
||||
|
||||
def create_strategy_frames(self, strategy_names):
|
||||
"""创建各个策略的Frame"""
|
||||
for idx, name in enumerate(strategy_names):
|
||||
if idx == 0:
|
||||
# 第一个Tab使用TradeTargetUI,传入main_window引用
|
||||
frame = TradeTargetUI(self.content_container)
|
||||
self.strategy_frames[idx] = frame
|
||||
else:
|
||||
# 其他策略使用占位Frame
|
||||
frame = ttk.Frame(self.content_container)
|
||||
self.strategy_frames[idx] = frame
|
||||
frame = TradeTargetUI(self.content_container)
|
||||
self.strategy_frames[0] = frame
|
||||
|
||||
# 添加占位内容
|
||||
placeholder = ttk.Label(
|
||||
frame,
|
||||
text=f"{name} - 策略界面将在此实现",
|
||||
font=('Arial', 14),
|
||||
foreground='gray'
|
||||
)
|
||||
placeholder.pack(expand=True)
|
||||
|
||||
def switch_strategy_tab(self, index):
|
||||
"""切换策略Tab"""
|
||||
# 隐藏当前Frame
|
||||
if self.current_strategy_index in self.strategy_frames:
|
||||
self.strategy_frames[self.current_strategy_index].pack_forget()
|
||||
|
||||
# 更新当前索引
|
||||
self.current_strategy_index = index
|
||||
|
||||
# 显示选中的Frame
|
||||
def show_strategy_frame(self, index):
|
||||
"""显示策略Frame"""
|
||||
if index in self.strategy_frames:
|
||||
self.strategy_frames[index].pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 更新Tab按钮样式(可选,用于视觉反馈)
|
||||
self.update_tab_button_styles()
|
||||
|
||||
def update_tab_button_styles(self):
|
||||
"""更新Tab按钮的样式以显示选中状态"""
|
||||
# 重置所有按钮为普通书签样式
|
||||
for i, btn in enumerate(self.tab_buttons):
|
||||
if i == self.current_strategy_index:
|
||||
btn.configure(style='SelectedBookmark.TButton') # 选中状态
|
||||
else:
|
||||
btn.configure(style='Bookmark.TButton') # 普通状态
|
||||
|
||||
def toggle_log_panel(self):
|
||||
"""切换日志面板的显示/隐藏"""
|
||||
if self.log_visible:
|
||||
# 隐藏日志面板
|
||||
self.log_frame.pack_forget()
|
||||
self.log_visible = False
|
||||
self.log_toggle_btn.config(text="📋") # 日志图标
|
||||
else:
|
||||
# 显示日志面板
|
||||
self.log_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0))
|
||||
self.log_visible = True
|
||||
self.log_toggle_btn.config(text="🔽") # 使用不同图标表示隐藏
|
||||
|
||||
def on_exit(self):
|
||||
"""退出程序"""
|
||||
|
||||
@@ -107,6 +107,13 @@ class DummyQmtV:
|
||||
}
|
||||
PrintLog(LogLevel.INFO, f'- [模拟] 已加载 {len(self._positions)} 个持仓')
|
||||
|
||||
def getAllPositions(self) -> dict:
|
||||
"""获取全部持仓,返回 {stock_code: position_object}"""
|
||||
result = {}
|
||||
for code, pos_data in self._positions.items():
|
||||
result[code] = type('DummyPos', (), pos_data)()
|
||||
return result
|
||||
|
||||
def getStockPosition(self, stock_code: str):
|
||||
"""获取持仓 (模拟)"""
|
||||
if stock_code in self._positions:
|
||||
@@ -207,6 +214,13 @@ class DummyQmtV:
|
||||
"""获取跌停价 (模拟)"""
|
||||
return 0.0
|
||||
|
||||
def getLastPrice(self, stock_code: str) -> float:
|
||||
"""主动获取最新市价(模拟)"""
|
||||
if stock_code in self._positions:
|
||||
return float(self._positions[stock_code].get('open_cost', 10.0))
|
||||
# 给一个合理模拟价
|
||||
return 10.0 + hash(stock_code) % 100
|
||||
|
||||
def startMarketDataSubscription(self):
|
||||
"""启动市场数据订阅 (模拟)"""
|
||||
try:
|
||||
|
||||
+362
-19
@@ -1,25 +1,368 @@
|
||||
"""
|
||||
QMT 模块统一入口
|
||||
根据环境自动选择真实 QMT 或模拟器
|
||||
QMT 真实交易实现 - 封装 xtquant SDK
|
||||
"""
|
||||
import sys
|
||||
import datetime
|
||||
import threading
|
||||
import time
|
||||
import config
|
||||
import core.eventbus as eBus
|
||||
from core.logger import LogLevel, PrintLog
|
||||
|
||||
def _get_qmt():
|
||||
"""获取 QMT 模块"""
|
||||
if sys.platform == 'win32':
|
||||
|
||||
class RealQmtV:
|
||||
"""
|
||||
真实 QMT 交易器
|
||||
封装 xtquant 的 XtQuantTrader,提供与模拟器一致的接口
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _to_plain_code(stock_code: str) -> str:
|
||||
"""将 xtquant 格式 '600519.SH' 转换为数据库格式 '600519'"""
|
||||
return stock_code.split('.')[0] if '.' in stock_code else stock_code
|
||||
|
||||
@staticmethod
|
||||
def _to_full_code(stock_code: str) -> str:
|
||||
"""将数据库格式 '600519' 转换为 xtquant 格式 '600519.SH'"""
|
||||
if '.' in stock_code:
|
||||
return stock_code # already has suffix
|
||||
code = stock_code
|
||||
if code.startswith(('6', '5', '9')):
|
||||
return f'{code}.SH'
|
||||
elif code.startswith(('0', '3', '2')):
|
||||
return f'{code}.SZ'
|
||||
# fallback: try both, prefer SH
|
||||
return f'{code}.SH'
|
||||
|
||||
@staticmethod
|
||||
def _strip_code_suffixes(datas: dict) -> dict:
|
||||
"""批量去除 xtquant 数据中的代码后缀"""
|
||||
result = {}
|
||||
for code, tick in datas.items():
|
||||
result[code] = tick
|
||||
if '.' in code:
|
||||
result[code.split('.')[0]] = tick
|
||||
return result
|
||||
def __init__(self) -> None:
|
||||
self.inited = False
|
||||
self.connected = False
|
||||
self.account = None
|
||||
self.xt_trader = None
|
||||
self.mini_qmt_path = ""
|
||||
self._positions = {}
|
||||
self._pending_orders = []
|
||||
self._market_data_thread = None
|
||||
self.isMarketActive = False
|
||||
self.lastMarketDataUpdateTimestamp = time.time()
|
||||
self.details = {}
|
||||
|
||||
def getTrader(self):
|
||||
return self
|
||||
|
||||
def init_qmtv(self):
|
||||
"""初始化 QMT 交易器"""
|
||||
try:
|
||||
# Windows 环境尝试导入真实 QMT
|
||||
import core.qmt_real as qmt_module
|
||||
return qmt_module.qmtv
|
||||
except ImportError:
|
||||
pass
|
||||
from xtquant.xttrader import XtQuantTrader
|
||||
from xtquant.xttype import StockAccount
|
||||
|
||||
# 非 Windows 或导入失败,使用模拟器
|
||||
try:
|
||||
import core.qmt_dummy as qmt_module
|
||||
return qmt_module.qmtv
|
||||
except ImportError:
|
||||
raise ImportError("无法加载 QMT 模块")
|
||||
self.mini_qmt_path = config.miniQMTPath
|
||||
self.account = StockAccount(config.account_no, 'STOCK')
|
||||
|
||||
# 导出单例
|
||||
qmtv = _get_qmt()
|
||||
# 创建 XtQuantTrader 实例
|
||||
session_id = int(time.time()) % 10000
|
||||
self.xt_trader = XtQuantTrader(self.mini_qmt_path, session_id)
|
||||
|
||||
# 注册回调 — xtquant 只接受一个回调对象,会在上面调用 on_xxx 方法
|
||||
self.xt_trader.register_callback(self)
|
||||
|
||||
self.inited = True
|
||||
PrintLog(LogLevel.INFO, f'- [真实] QMT 交易器初始化成功')
|
||||
except Exception as e:
|
||||
self.inited = False
|
||||
PrintLog(LogLevel.ERROR, f'- [失败] QMT 初始化: {e}')
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""连接 MiniQMT"""
|
||||
if not self.inited:
|
||||
PrintLog(LogLevel.ERROR, '- [失败] QMT 未初始化')
|
||||
return False
|
||||
|
||||
try:
|
||||
# 启动 trader 线程
|
||||
self.xt_trader.start()
|
||||
# 建立连接
|
||||
connect_result = self.xt_trader.connect()
|
||||
if connect_result == 0:
|
||||
# 订阅账户 (传入 StockAccount 对象而不是 account_id 字符串)
|
||||
self.xt_trader.subscribe(self.account)
|
||||
# 等待回调
|
||||
time.sleep(1)
|
||||
self.connected = True
|
||||
self.startMarketDataSubscription()
|
||||
PrintLog(LogLevel.INFO, f'- [成功] 真实交易连接成功 (账号: {config.account_no})')
|
||||
return True
|
||||
else:
|
||||
PrintLog(LogLevel.ERROR, f'- [失败] 连接失败, 返回码: {connect_result}')
|
||||
return False
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'- [失败] 连接异常: {e}')
|
||||
return False
|
||||
|
||||
def getAllPositions(self) -> dict:
|
||||
"""获取全部持仓,返回 {plain_code: position_object}"""
|
||||
if not self.connected:
|
||||
return {}
|
||||
try:
|
||||
positions = self.xt_trader.query_stock_positions(self.account)
|
||||
result = {}
|
||||
for pos in positions:
|
||||
code = self._to_plain_code(getattr(pos, 'stock_code', ''))
|
||||
result[code] = pos
|
||||
# 缓存以供 getStockPosition 使用
|
||||
self._position_cache = result
|
||||
return result
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'- [获取全部持仓失败]: {e}')
|
||||
return {}
|
||||
|
||||
def getStockPosition(self, stock_code: str):
|
||||
"""获取单只股票持仓(优先使用缓存)"""
|
||||
if not self.connected:
|
||||
return None
|
||||
try:
|
||||
# 优先查缓存
|
||||
if hasattr(self, '_position_cache') and stock_code in self._position_cache:
|
||||
return self._position_cache[stock_code]
|
||||
# 回退查询
|
||||
positions = self.xt_trader.query_stock_positions(self.account)
|
||||
for pos in positions:
|
||||
pos_code = self._to_plain_code(getattr(pos, 'stock_code', ''))
|
||||
if pos_code == stock_code:
|
||||
return pos
|
||||
return None
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'- [持仓查询失败] {stock_code}: {e}')
|
||||
return None
|
||||
|
||||
def queryPendingOrder(self, stock_code: str, tag: str) -> list:
|
||||
"""查询挂单"""
|
||||
if not self.connected:
|
||||
return []
|
||||
try:
|
||||
orders = self.xt_trader.query_stock_orders(self.account)
|
||||
return [o for o in orders
|
||||
if self._to_plain_code(getattr(o, 'stock_code', '')) == stock_code and
|
||||
(tag is None or getattr(o, 'strategy_name', None) == tag)]
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'- [查询挂单失败] {e}')
|
||||
return []
|
||||
|
||||
def orderAsync(self, stock_code, orderVolume, orderType, orderPrice, priceType, orderRemark, strategy_name):
|
||||
"""异步下单"""
|
||||
if not self.connected:
|
||||
PrintLog(LogLevel.ERROR, '- [下单失败] 未连接')
|
||||
return -1
|
||||
|
||||
try:
|
||||
seq = self.xt_trader.order_stock_async(
|
||||
account=self.account,
|
||||
stock_code=stock_code,
|
||||
order_volume=orderVolume,
|
||||
order_type=orderType,
|
||||
price=orderPrice,
|
||||
price_type=priceType,
|
||||
order_remark=orderRemark,
|
||||
strategy_name=strategy_name
|
||||
)
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'- [下单] {stock_code} 数量:{orderVolume} 价格:{orderPrice} 类型:{orderType} seq:{seq}')
|
||||
return 0
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'- [下单失败] {stock_code}: {e}')
|
||||
return -1
|
||||
|
||||
def cacheStockDetail(self, stock_code: str):
|
||||
"""获取股票详情"""
|
||||
if stock_code not in self.details:
|
||||
try:
|
||||
from xtquant import xtdata
|
||||
# xtquant 需要带后缀的完整代码
|
||||
full_code = self._to_full_code(stock_code)
|
||||
detail = xtdata.get_instrument_detail(full_code)
|
||||
if detail:
|
||||
# xtquant 返回 dict,使用 .get() 读取
|
||||
self.details[stock_code] = {
|
||||
'InstrumentName': detail.get('InstrumentName', stock_code) if isinstance(detail, dict) else getattr(detail, 'InstrumentName', stock_code),
|
||||
'UpStopPrice': detail.get('UpStopPrice', 0) if isinstance(detail, dict) else getattr(detail, 'UpStopPrice', 0),
|
||||
'DownStopPrice': detail.get('DownStopPrice', 0) if isinstance(detail, dict) else getattr(detail, 'DownStopPrice', 0)
|
||||
}
|
||||
else:
|
||||
self.details[stock_code] = {
|
||||
'InstrumentName': stock_code,
|
||||
'UpStopPrice': 0,
|
||||
'DownStopPrice': 0
|
||||
}
|
||||
except Exception:
|
||||
self.details[stock_code] = {
|
||||
'InstrumentName': stock_code,
|
||||
'UpStopPrice': 0,
|
||||
'DownStopPrice': 0
|
||||
}
|
||||
return self.details[stock_code]
|
||||
|
||||
def getInstrumentName(self, stock_code: str) -> str:
|
||||
"""获取股票名称"""
|
||||
return self.cacheStockDetail(stock_code)['InstrumentName']
|
||||
|
||||
def dailyUpStop(self, stock_code: str):
|
||||
"""获取涨停价"""
|
||||
detail = self.cacheStockDetail(stock_code)
|
||||
up_stop = detail.get('UpStopPrice', 0)
|
||||
PrintLog(LogLevel.DEBUG, f'- [详情] {stock_code} {detail["InstrumentName"]} 涨停价: {up_stop}')
|
||||
return up_stop or 0.0
|
||||
|
||||
def dailyDownStop(self, stock_code: str):
|
||||
"""获取跌停价"""
|
||||
detail = self.cacheStockDetail(stock_code)
|
||||
down_stop = detail.get('DownStopPrice', 0)
|
||||
return down_stop or 0.0
|
||||
|
||||
def getLastPrice(self, stock_code: str) -> float:
|
||||
"""主动获取最新市价(拉取模式,作为推送的兜底)"""
|
||||
try:
|
||||
from xtquant import xtdata
|
||||
import json
|
||||
full_code = self._to_full_code(stock_code)
|
||||
|
||||
# 方式1: 尝试 get_full_tick(参数是 list[str],返回 dict {code: {...}})
|
||||
raw = xtdata.get_full_tick([full_code])
|
||||
if raw:
|
||||
tick = json.loads(raw) if isinstance(raw, str) else raw
|
||||
if isinstance(tick, dict):
|
||||
# 格式: {'600519.SH': {'lastPrice': 8.97, ...}}
|
||||
for code, info in tick.items():
|
||||
if isinstance(info, dict) and info.get('lastPrice', 0) > 0:
|
||||
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → tick: {info["lastPrice"]:.3f}')
|
||||
return float(info['lastPrice'])
|
||||
|
||||
# 方式2: get_market_data 取最新1分钟K线收盘价
|
||||
data = xtdata.get_market_data(
|
||||
field_list=['close'],
|
||||
stock_list=[full_code],
|
||||
period='1m',
|
||||
count=1
|
||||
)
|
||||
if data:
|
||||
vals = None
|
||||
if full_code in data:
|
||||
row = data[full_code]
|
||||
if hasattr(row, '__iter__') and not isinstance(row, str):
|
||||
row = list(row)
|
||||
if row:
|
||||
vals = row
|
||||
if not vals and 'close' in data:
|
||||
field_data = data['close']
|
||||
if full_code in field_data:
|
||||
vals = list(field_data[full_code])
|
||||
if vals and len(vals) > 0 and float(vals[0]) > 0:
|
||||
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → kline: {float(vals[0]):.3f}')
|
||||
return float(vals[0])
|
||||
|
||||
# 方式3: 下载历史数据后再试
|
||||
xtdata.download_history_data(full_code, '1m', '')
|
||||
data = xtdata.get_market_data(
|
||||
field_list=['close'],
|
||||
stock_list=[full_code],
|
||||
period='1m',
|
||||
count=1
|
||||
)
|
||||
if data:
|
||||
vals = None
|
||||
if full_code in data:
|
||||
row = data[full_code]
|
||||
if hasattr(row, '__iter__') and not isinstance(row, str):
|
||||
row = list(row)
|
||||
if row:
|
||||
vals = row
|
||||
if not vals and 'close' in data:
|
||||
field_data = data['close']
|
||||
if full_code in field_data:
|
||||
vals = list(field_data[full_code])
|
||||
if vals and len(vals) > 0 and float(vals[0]) > 0:
|
||||
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → download+kline: {float(vals[0]):.3f}')
|
||||
return float(vals[0])
|
||||
|
||||
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → 失败: 所有方式均无数据, raw={raw}')
|
||||
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → 异常: {e}')
|
||||
return 0.0
|
||||
|
||||
def startMarketDataSubscription(self):
|
||||
"""启动市场数据订阅"""
|
||||
try:
|
||||
from xtquant import xtdata
|
||||
|
||||
# 订阅沪深全市场实时行情
|
||||
seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], self._on_market_data)
|
||||
PrintLog(LogLevel.INFO, f'- [市场数据订阅成功-真实] seq={seq}')
|
||||
|
||||
# 启动行情活跃监控线程
|
||||
self._market_data_thread = threading.Thread(
|
||||
target=self._market_data_watchdog, daemon=True
|
||||
)
|
||||
self._market_data_thread.start()
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'- [市场数据订阅失败-{e}]')
|
||||
|
||||
def _on_market_data(self, datas: dict):
|
||||
"""xtquant 行情回调 — 将数据转换为事件总线格式"""
|
||||
self.lastMarketDataUpdateTimestamp = time.time()
|
||||
if not self.isMarketActive:
|
||||
self.isMarketActive = True
|
||||
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, True)
|
||||
|
||||
# xtquant 返回 "600519.SH" 格式 key,UI 使用纯代码 "600519"
|
||||
# 构建同时包含两种 key 的数据确保匹配
|
||||
eBus.event_bus.publish(eBus.MarketDataUpdate, self._strip_code_suffixes(datas))
|
||||
|
||||
def _market_data_watchdog(self):
|
||||
"""行情活跃监控 — 超过 30 秒无数据则标记市场不活跃"""
|
||||
while True:
|
||||
time.sleep(10)
|
||||
if self.isMarketActive:
|
||||
elapsed = time.time() - self.lastMarketDataUpdateTimestamp
|
||||
if elapsed > 30:
|
||||
self.isMarketActive = False
|
||||
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, False)
|
||||
PrintLog(LogLevel.WARNING, f'- [行情] 超过 {elapsed:.0f} 秒无更新,市场标记为不活跃')
|
||||
|
||||
def stopMarketDataSubscription(self):
|
||||
"""停止市场数据订阅"""
|
||||
self.isMarketActive = False
|
||||
PrintLog(LogLevel.INFO, '- [市场数据订阅已停止]')
|
||||
|
||||
# ---- xtquant 回调处理 (xtquant 通过回调对象调用 on_xxx 方法) ----
|
||||
|
||||
def on_connected(self):
|
||||
print(datetime.datetime.now(), '真实 QMT 连接成功')
|
||||
|
||||
def on_disconnected(self):
|
||||
print(datetime.datetime.now(), '真实 QMT 连接断开')
|
||||
|
||||
def on_stock_order(self, order):
|
||||
self._pending_orders.append(order)
|
||||
|
||||
def on_stock_trade(self, trade):
|
||||
eBus.event_bus.publish(eBus.MarketOrderTraded, trade)
|
||||
|
||||
def on_order_stock_async_response(self, response):
|
||||
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
|
||||
|
||||
def on_order_error(self, order_error):
|
||||
print(f"\n真实委托报错回调 {order_error}")
|
||||
|
||||
def on_account_status(self, status):
|
||||
print(datetime.datetime.now(), status)
|
||||
|
||||
|
||||
qmtv = RealQmtV()
|
||||
|
||||
+18
-2
@@ -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) # 基线价格下方网格数
|
||||
|
||||
@@ -42,3 +47,14 @@ class SFGridTradeTarget(BaseModel):
|
||||
|
||||
|
||||
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
|
||||
@@ -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
|
||||
+443
-138
@@ -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,11 +174,85 @@ 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))
|
||||
# 表格区域(左右布局:左侧=工具栏+持仓表格,右侧=Notebook标签页)
|
||||
self.create_tables_area(main_frame)
|
||||
|
||||
# 启动刷新线程
|
||||
self.refresh_thread = threading.Thread(target=self.refresh_loop, daemon=True)
|
||||
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:
|
||||
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)
|
||||
|
||||
# ========== 左侧:工具栏 + 持仓表格 ==========
|
||||
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="🗑 删除标的",
|
||||
@@ -133,97 +263,122 @@ class TradeTargetUI(ttk.Frame):
|
||||
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)
|
||||
|
||||
# 上半部分: 网格策略持仓
|
||||
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.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))
|
||||
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_tables_area(main_frame)
|
||||
|
||||
# 启动刷新线程
|
||||
self.refresh_thread = threading.Thread(target=self.refresh_loop, daemon=True)
|
||||
self.refresh_thread.start()
|
||||
|
||||
|
||||
def refresh_loop(self):
|
||||
"""刷新循环"""
|
||||
while True:
|
||||
self.after(0, self.refresh_table)
|
||||
self.after(0, self.populate_market_table)
|
||||
time.sleep(0.5) # 每0.5秒刷新一次
|
||||
|
||||
|
||||
def create_tables_area(self, parent):
|
||||
"""创建表格区域"""
|
||||
# 创建主表格框架(水平排列)
|
||||
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))
|
||||
|
||||
# 创建市场监控表格
|
||||
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)
|
||||
|
||||
def create_trade_target_table(self, parent):
|
||||
"""创建交易标的表格"""
|
||||
# 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_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.grid_table.heading(col, text=col)
|
||||
self.grid_table.column(col, width=width, anchor=anchor) # type: ignore
|
||||
|
||||
# 填充数据
|
||||
self.populate_trade_table()
|
||||
self.populate_grid_table()
|
||||
|
||||
# 滚动条
|
||||
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.trade_table.yview)
|
||||
self.trade_table.configure(yscrollcommand=scrollbar.set)
|
||||
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.grid_table.yview)
|
||||
self.grid_table.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.trade_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
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,79 +498,174 @@ 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)
|
||||
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
|
||||
|
||||
def on_table_double_click(self, event):
|
||||
"""表格双击事件"""
|
||||
selected = self.trade_table.selection()
|
||||
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
|
||||
"""获取选中的交易标的(根据最后点击的表格优先)"""
|
||||
# 根据最后点击的表格决定检查顺序
|
||||
if self._active_table == 1:
|
||||
primary, secondary = self.unclassified_table, self.grid_table
|
||||
else:
|
||||
primary, secondary = self.grid_table, self.unclassified_table
|
||||
|
||||
# 获取选中行的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]
|
||||
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
|
||||
selected_values.append(values[0])
|
||||
|
||||
# 清空表格
|
||||
for item in self.trade_table.get_children():
|
||||
self.trade_table.delete(item)
|
||||
for item in self.grid_table.get_children():
|
||||
self.grid_table.delete(item)
|
||||
self.populate_grid_table()
|
||||
|
||||
# 重新填充
|
||||
self.populate_trade_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,7 +977,13 @@ 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) # 网格配置变更
|
||||
@@ -700,6 +991,9 @@ class TradeTargetUI(ttk.Frame):
|
||||
# 关闭窗口
|
||||
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):
|
||||
@@ -808,8 +1101,8 @@ class TradeTargetUI(ttk.Frame):
|
||||
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:
|
||||
# 创建只读的网格配置查看窗口
|
||||
@@ -821,6 +1114,11 @@ class TradeTargetUI(ttk.Frame):
|
||||
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
|
||||
@@ -854,6 +1152,11 @@ class TradeTargetUI(ttk.Frame):
|
||||
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):
|
||||
"""添加新的交易标的"""
|
||||
|
||||
Reference in New Issue
Block a user