init new structure
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# 删除交易标的事件
|
||||
EventTradeTargetUpdate = "trade_target_update"
|
||||
EventTradeTargetDeleted = "trade_target_deleted"
|
||||
@@ -0,0 +1,44 @@
|
||||
from peewee import CharField, IntegerField, FloatField, BooleanField
|
||||
|
||||
from core.database import BaseModel, db
|
||||
|
||||
|
||||
# 定义Target类,对应targets表
|
||||
class SFGridTradeTarget(BaseModel):
|
||||
stock_code = CharField(unique=True)
|
||||
stock_name = CharField()
|
||||
current_position = IntegerField()
|
||||
grid_index = IntegerField(default=0)
|
||||
init_price = FloatField(null=True) # 建仓成本
|
||||
grid_match_count = IntegerField(default=0)
|
||||
grid_total_profit = FloatField(default=0.0)
|
||||
status = IntegerField(default=0) # -1表示新标的,未完成交易配置,0表示新标的,已完成交易配置,1表示已建初始仓,正常交易中
|
||||
enabled = BooleanField(default=False) # 是否启动交易线程
|
||||
|
||||
grid_start_price = FloatField(default=10.0) # 基线价格
|
||||
grid_size = FloatField(default=0.1) # 网格价位差
|
||||
grid_volume = IntegerField(default=100) # 网格交易量
|
||||
grid_upper_count = IntegerField(default=1) # 基线价格上方网格数
|
||||
grid_lower_count = IntegerField(default=10) # 基线价格下方网格数
|
||||
|
||||
def targetName(self):
|
||||
return f'{self.stock_code}-{self.stock_name}'
|
||||
|
||||
def getPriceGrid(self) -> list:
|
||||
self.priceGrid: list = []
|
||||
# 网格大小,数量
|
||||
if self.priceGrid is None or len(self.priceGrid) == 0:
|
||||
for i in range(self.grid_upper_count): # type: ignore
|
||||
upperPrice = self.grid_start_price + (self.grid_upper_count - i) * self.grid_size
|
||||
self.priceGrid.append(round(upperPrice, 3))
|
||||
|
||||
self.priceGrid.append(self.grid_start_price)
|
||||
|
||||
for i in range(self.grid_lower_count): # type: ignore 5
|
||||
lowerPrice = self.grid_start_price - (i + 1) * self.grid_size
|
||||
self.priceGrid.append(round(lowerPrice, 3))
|
||||
|
||||
return self.priceGrid
|
||||
|
||||
|
||||
db.create_tables([SFGridTradeTarget])
|
||||
@@ -0,0 +1,223 @@
|
||||
from core.logger import LogLevel, PrintLog
|
||||
from core.qmt import qmtv
|
||||
from core.sfgrid import bus_events
|
||||
from core.sfgrid.bus_events import EventTradeTargetUpdate
|
||||
import core.sfgrid.model as model
|
||||
from core.eventbus import event_bus
|
||||
from core.constants import OrderTypeBuy, OrderTypeSell, OrderTypeInit
|
||||
|
||||
from xtquant import xtconstant
|
||||
from xtquant.xttype import XtOrderResponse, XtTrade
|
||||
import threading
|
||||
import core.eventbus as eBus
|
||||
|
||||
|
||||
class SFGridStrategy:
|
||||
|
||||
def __init__(self, tradeTarget: model.SFGridTradeTarget):
|
||||
self.tradeTarget:model.SFGridTradeTarget = tradeTarget
|
||||
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
|
||||
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}')
|
||||
self.orderGrid = {} # grid index, order_seq | order_id
|
||||
self.loadExistOrders()
|
||||
self.enabledTrading(tradeTarget.enabled) # type: ignore
|
||||
self.dataUpdateLock = threading.Lock()
|
||||
|
||||
def loadExistOrders(self):
|
||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||
for order in orders:
|
||||
if order.strategy_name != self.getName():
|
||||
continue
|
||||
gridIdx = int(order.order_remark.split(',')[1])
|
||||
self.orderGrid[gridIdx] = order.order_id
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 初始化: 加载现有订单, grid-{gridIdx} order_id:{self.orderGrid[gridIdx]}')
|
||||
|
||||
def printPendingOrder(self):
|
||||
for idx, order_id in self.orderGrid.items():
|
||||
PrintLog(LogLevel.DEBUG, f" {idx} : {order_id}")
|
||||
|
||||
def onMarketActiveSwitch(self, isActive: bool):
|
||||
if isActive and self.tradeTarget.enabled:
|
||||
self.refreshGridOrder()
|
||||
|
||||
def refreshGridOrder(self): # 下网格单
|
||||
if not qmtv.isMarketActive or not self.tradeTarget.enabled:
|
||||
PrintLog(LogLevel.INFO, f'|- 市场 {qmtv.isMarketActive}, 策略 {self.getName()} {self.tradeTarget.enabled}, 不下单')
|
||||
return
|
||||
|
||||
currentIdx:int = 0
|
||||
|
||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||
|
||||
if self.tradeTarget.status == 0 and len([order for order in orders if order.order_remark == f'{OrderTypeInit},1,{self.tradeTarget.stock_code}']) == 0: # status == 0 表示已配置好交易参数,且不存在执行中的建仓单
|
||||
price = self.tradeTarget.getPriceGrid()[0]
|
||||
remark = f'{OrderTypeInit},1,{self.tradeTarget.stock_code}'
|
||||
tmpOrderSeq = qmtv.orderAsync(
|
||||
str(self.tradeTarget.stock_code),
|
||||
self.tradeTarget.grid_volume,
|
||||
xtconstant.STOCK_BUY,
|
||||
price,
|
||||
xtconstant.FIX_PRICE,
|
||||
remark, # remark # type: ignore
|
||||
self.getName(), # strategy_name
|
||||
)
|
||||
self.orderGrid[1] = tmpOrderSeq # seq
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 初始化: 建仓单,建仓价: {price:.3f}')
|
||||
elif self.tradeTarget.status == 1: # 下网格单
|
||||
currentIdx = self.tradeTarget.grid_index # type: ignore
|
||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||
|
||||
# 向上下一单,向下下一单
|
||||
if currentIdx > 0: # 可以下空单
|
||||
sellIdx = currentIdx - 1
|
||||
sellPrice = self.tradeTarget.getPriceGrid()[sellIdx]
|
||||
remark = f'{OrderTypeSell},{sellIdx},{self.tradeTarget.stock_code}'
|
||||
if len([order for order in orders if order.order_remark == remark]) == 0: # 网格节点没有卖单,下单
|
||||
# 不存在策略内同价位订单,下单
|
||||
tmpOrderSeq = qmtv.orderAsync(
|
||||
str(self.tradeTarget.stock_code),
|
||||
self.tradeTarget.grid_volume,
|
||||
xtconstant.STOCK_SELL,
|
||||
sellPrice,
|
||||
xtconstant.FIX_PRICE,
|
||||
remark, # remark # type: ignore
|
||||
self.getName(), # strategy_name
|
||||
)
|
||||
self.orderGrid[sellIdx] = tmpOrderSeq # seq
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 下空单,价格: {sellPrice:.3f}')
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已存在同价位空单,跳过下单')
|
||||
if currentIdx < len(self.tradeTarget.getPriceGrid()) - 1: # 可以下多单
|
||||
print(f'length: {len(self.tradeTarget.getPriceGrid())}, currentIdx = {currentIdx}')
|
||||
buyIdx = currentIdx + 1
|
||||
buyPrice = self.tradeTarget.getPriceGrid()[buyIdx]
|
||||
remark = f'{OrderTypeBuy},{buyIdx},{self.tradeTarget.stock_code}'
|
||||
if len([order for order in orders if order.order_type == xtconstant.STOCK_BUY and order.price == buyPrice]) == 0:
|
||||
tmpOrderSeq = qmtv.orderAsync(
|
||||
str(self.tradeTarget.stock_code),
|
||||
self.tradeTarget.grid_volume,
|
||||
xtconstant.STOCK_BUY,
|
||||
buyPrice,
|
||||
xtconstant.FIX_PRICE,
|
||||
remark, # remark # type: ignore
|
||||
self.getName(), # strategy_name
|
||||
)
|
||||
self.orderGrid[buyIdx] = tmpOrderSeq # seq
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 下多单,价格: {buyPrice:.3f}')
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已存在同价位多单,跳过下单')
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已过下边界,停止多单交易')
|
||||
|
||||
def deleteTradeTarget(self, tradeTarget:model.SFGridTradeTarget):
|
||||
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}信息删除: START')
|
||||
self.dataUpdateLock.acquire()
|
||||
try:
|
||||
tradeTarget.delete_instance()
|
||||
event_bus.publish(bus_events.EventTradeTargetDeleted, tradeTarget)
|
||||
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}信息删除: END')
|
||||
finally:
|
||||
self.dataUpdateLock.release()
|
||||
|
||||
def enabledTrading(self, enabled: bool) -> model.SFGridTradeTarget:
|
||||
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]
|
||||
else: # 已建仓
|
||||
# 交易阶段,检查仓位,检查现有订单
|
||||
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 无需建初始仓 当前仓位: {self.tradeTarget.current_position} 状态: {self.tradeTarget.status}")
|
||||
minRequirePosition:int = self.tradeTarget.grid_volume * int(self.tradeTarget.grid_index) # type: ignore
|
||||
if minRequirePosition <= int(self.tradeTarget.current_position): # type: ignore
|
||||
PrintLog(LogLevel.INFO, f' |- 仓位检查: 持仓需求充足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}')
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f' |- 仓位检查: 持仓需求不足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}, 交易启动失败')
|
||||
self.tradeTarget.enabled = False # type: ignore
|
||||
self.refreshGridOrder()
|
||||
else:
|
||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||
for order in orders:
|
||||
qmtv.xttrader.cancel_order_stock_async(qmtv.account, order.order_id)
|
||||
if len(orders) > 0:
|
||||
PrintLog(LogLevel.INFO, f' |- 取消未成交订单 {len(orders)}')
|
||||
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易监控暂停")
|
||||
|
||||
self.saveProxy()
|
||||
return self.tradeTarget
|
||||
|
||||
def isEnabled(self) -> bool:
|
||||
print(f'|- 检查交易状态[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - {self.tradeTarget.enabled}')
|
||||
return bool(self.tradeTarget.enabled) # 修复返回类型问题
|
||||
|
||||
def onOrderCreateAsync(self, response:XtOrderResponse): # 下单成功回调,更新orderID到 self.orderGrid
|
||||
remark = response.order_remark.split(',')
|
||||
stockCode = remark[2] # 从remark中获取stockCode
|
||||
if response.strategy_name != self.getName() or len(remark) < 3 or self.tradeTarget.stock_code != stockCode:
|
||||
return
|
||||
self.dataUpdateLock.acquire()
|
||||
try:
|
||||
gridIdx = remark[1] # 从remark中获取gridIdx
|
||||
PrintLog(LogLevel.INFO, f"委托创建通知 onOrderCreateAsync[{self.tradeTarget.targetName()}]: {response.order_id}")
|
||||
self.orderGrid[gridIdx] = response.order_id
|
||||
PrintLog(LogLevel.INFO, f"委托创建通知 onOrderCreateAsync 更新 grid-{gridIdx} seq:{response.seq} -> order_id:{response.order_id}")
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f"|- 委托创建通知 onOrderCreateAsync[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}]: {response.order_id} - {str(e)}")
|
||||
finally:
|
||||
self.dataUpdateLock.release()
|
||||
|
||||
def onOrderTrade(self, trade:XtTrade): # TODO 委托成交通知,处理成交后网格切换
|
||||
remark = trade.order_remark.split(',')
|
||||
if trade.strategy_name != self.getName() or len(remark) < 3 or self.tradeTarget.stock_code != trade.stock_code:
|
||||
return
|
||||
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}] : {trade.order_id}')
|
||||
|
||||
self.dataUpdateLock.acquire()
|
||||
try:
|
||||
orderType = trade.order_remark.split(',')[0]
|
||||
gridIdx = trade.order_remark.split(',')[1] # 从remark中获取gridIdx
|
||||
type:str = ""
|
||||
if orderType == OrderTypeInit:
|
||||
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
|
||||
self.tradeTarget.grid_index = 1 # type: ignore
|
||||
type = "建仓单"
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] - 网格单成交')
|
||||
oriIdx = self.tradeTarget.grid_index
|
||||
if gridIdx > self.tradeTarget.grid_index:
|
||||
type = "下移一格"
|
||||
self.tradeTarget.grid_index +=1
|
||||
elif gridIdx < self.tradeTarget.grid_index:
|
||||
type = "上移一格"
|
||||
self.tradeTarget.grid_match_count += 1
|
||||
self.tradeTarget.grid_total_profit += self.tradeTarget.grid_size * trade.traded_volume
|
||||
self.tradeTarget.grid_index -= 1
|
||||
else:
|
||||
type = "保持格, 理论上不应该输出"
|
||||
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} - 原网格位置 {oriIdx}, 现网格位置 {self.tradeTarget.grid_index}')
|
||||
|
||||
self.saveProxy()
|
||||
del self.orderGrid[gridIdx]
|
||||
PrintLog(LogLevel.INFO, f"|- 成交报告[{self.tradeTarget.targetName()}] : ====================================")
|
||||
PrintLog(LogLevel.INFO, f"|- 标的[{self.tradeTarget.targetName()}] {type}-单号{trade.order_id}已成交 ")
|
||||
PrintLog(LogLevel.INFO, f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
|
||||
PrintLog(LogLevel.INFO, f' 手续费 : {trade.commission:.3f}')
|
||||
self.refreshGridOrder() # 更新网格订单
|
||||
finally:
|
||||
self.dataUpdateLock.release()
|
||||
|
||||
|
||||
def getName(self):
|
||||
return "SFGRID"
|
||||
|
||||
def saveProxy(self):
|
||||
rc = self.tradeTarget.save()
|
||||
event_bus.publish(EventTradeTargetUpdate, self.tradeTarget)
|
||||
return rc
|
||||
@@ -0,0 +1,985 @@
|
||||
from typing import Any
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
from datetime import datetime
|
||||
import threading
|
||||
import time
|
||||
import core.eventbus as eBus
|
||||
from core.logger import LogLevel, PrintLog
|
||||
from core.sfgrid import bus_events
|
||||
from core.sfgrid.model import SFGridTradeTarget
|
||||
from core.qmt import qmtv
|
||||
from core.sfgrid.sfgrid_strategy import SFGridStrategy
|
||||
|
||||
|
||||
class TradeTargetUI(ttk.Frame):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.tradeTargetData:dict[int, SFGridTradeTarget] = {} # id->trade_target
|
||||
self.stockCodeIdMap:dict[str, int] = {}
|
||||
self.strategy_ctrl:dict[int, SFGridStrategy] = {} # stock_code->trade_target
|
||||
self.targetMarketPrice: dict[int, float] = {}
|
||||
self.targetAvgPrice: dict[int, float] = {}
|
||||
self.listening_stock = []
|
||||
# 监控价格,默认值为10
|
||||
self.monitor_price = 10.0
|
||||
|
||||
self.init_trade_target_pool()
|
||||
|
||||
# 市场监控数据
|
||||
self.marketData: dict[str, Any] = {} # 存储市场数据 {stock_code: {stock_name, last_price, time}}
|
||||
|
||||
# 市场监控窗口显示状态
|
||||
self.market_monitor_visible = True
|
||||
|
||||
# 创建界面
|
||||
self.create_ui()
|
||||
eBus.event_bus.subscribe(eBus.MarketDataUpdate, self.onMarketDataUpdated)
|
||||
|
||||
eBus.event_bus.subscribe(bus_events.EventTradeTargetUpdate, self.onStrategyUpdate)
|
||||
eBus.event_bus.subscribe(bus_events.EventTradeTargetDeleted, self.onTradeTargetDeleted)
|
||||
|
||||
|
||||
def init_trade_target_pool(self):
|
||||
results = SFGridTradeTarget.select()
|
||||
for temp in results:
|
||||
tradeTarget:SFGridTradeTarget = temp
|
||||
pos = qmtv.getStockPosition(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
|
||||
else:
|
||||
self.targetAvgPrice[tradeTarget.get_id()] = pos.avg_price
|
||||
PrintLog(LogLevel.INFO, f'- [成功]获取持仓信息: {tradeTarget.stock_code} {tradeTarget.targetName()} {tradeTarget.current_position} {pos.avg_price}')
|
||||
|
||||
self.updateTradeTarget(tradeTarget, True) # 初始化的时候
|
||||
|
||||
PrintLog(LogLevel.INFO, f'- [成功]交易标的信息初始化, 共 {len(self.tradeTargetData)} 个标的')
|
||||
|
||||
|
||||
# 收集所有市场数据用于市场监控
|
||||
def onMarketDataUpdated(self, data):
|
||||
for stock_code, tickData in data.items():
|
||||
if stock_code in self.stockCodeIdMap:
|
||||
id:int = self.stockCodeIdMap[stock_code]
|
||||
self.targetMarketPrice[id] = tickData['lastPrice']
|
||||
tradeTarget = self.tradeTargetData[id]
|
||||
# timeStr = datetime.fromtimestamp(tickData['time']/1000)
|
||||
lastPrice = float("{:.3f}".format(tickData['lastPrice']))
|
||||
tradeTarget.market_price = lastPrice # type: ignore
|
||||
# PrintLog(LogLevel.INFO, f'|- 市价更新[{tradeTarget.targetName()}] - {timeStr.strftime("%H:%M:%S")} 市价更新: {lastPrice}======================{id}')
|
||||
self.updateTradeTarget(tradeTarget, False) # 市价更新
|
||||
else:
|
||||
# 非目标交易,发布市场数据更新事件用于市场监控
|
||||
lastPrice = tickData['lastPrice']
|
||||
# 使用用户设置的监控价格替代硬编码的10
|
||||
if lastPrice == self.monitor_price or stock_code in self.listening_stock:
|
||||
# 发布市场数据更新事件用于市场监控
|
||||
if stock_code not in self.listening_stock:
|
||||
self.listening_stock.append(stock_code)
|
||||
# 更新市场监控数据用于UI显示
|
||||
current_time = datetime.now().strftime("%H:%M:%S")
|
||||
self.marketData[str(stock_code)] = {
|
||||
'stock_name': qmtv.getInstrumentName(stock_code),
|
||||
'last_price': tickData['lastPrice'],
|
||||
'time': current_time
|
||||
}
|
||||
|
||||
|
||||
# 来自策略的数据更新
|
||||
def onStrategyUpdate(self, target: SFGridTradeTarget):
|
||||
id = target.get_id()
|
||||
self.tradeTargetData[id] = target
|
||||
|
||||
|
||||
# priceChange 用于控制是否对更新价格数据,进行交易判断
|
||||
def updateTradeTarget(self, target: SFGridTradeTarget, save: bool = True):
|
||||
if save:
|
||||
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:
|
||||
self.stockCodeIdMap[target.stock_code] = id # type: ignore
|
||||
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:
|
||||
self.targetAvgPrice[id] = pos.avg_price
|
||||
|
||||
# UI CREATE
|
||||
def create_ui(self):
|
||||
"""创建UI界面"""
|
||||
# 主框架(使用self作为父容器)
|
||||
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))
|
||||
|
||||
|
||||
# 表格区域
|
||||
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)
|
||||
|
||||
|
||||
def create_trade_target_table(self, parent):
|
||||
"""创建交易标的表格"""
|
||||
|
||||
columns = ("ID",
|
||||
"股票代码", "股票名称", "市场价", "当前持仓", "建仓成本",
|
||||
"平均成本", "网格匹配次数", "网格收益", "交易状态"
|
||||
)
|
||||
|
||||
self.trade_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),
|
||||
"平均成本": (60, tk.E),
|
||||
"网格匹配次数": (60, tk.E),
|
||||
"网格收益": (60, tk.E),
|
||||
"交易状态": (80, 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)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# 绑定双击事件
|
||||
self.trade_table.bind("<Double-1>", self.on_table_double_click)
|
||||
|
||||
def create_market_monitor_table(self, parent):
|
||||
"""创建市场监控表格"""
|
||||
columns = ("时间", "股票名称", "最新价格")
|
||||
|
||||
self.market_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
|
||||
|
||||
# 列配置
|
||||
column_configs = {
|
||||
"时间": (50, tk.CENTER),
|
||||
"股票名称": (80, tk.CENTER),
|
||||
"最新价格": (50, tk.CENTER)
|
||||
}
|
||||
|
||||
for col in columns:
|
||||
width, anchor = column_configs[col]
|
||||
self.market_table.heading(col, text=col)
|
||||
self.market_table.column(col, width=width, anchor=anchor) # type: ignore
|
||||
|
||||
# 滚动条
|
||||
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.market_table.yview)
|
||||
self.market_table.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.market_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# 绑定双击事件
|
||||
self.market_table.bind("<Double-1>", self.on_market_table_double_click)
|
||||
|
||||
# 填充初始数据
|
||||
self.populate_market_table()
|
||||
|
||||
def populate_market_table(self):
|
||||
"""填充市场监控表格数据"""
|
||||
# 保存当前选中的项
|
||||
selected_items = self.market_table.selection()
|
||||
selected_values = []
|
||||
for item in selected_items:
|
||||
values = self.market_table.item(item)['values']
|
||||
if values:
|
||||
selected_values.append(values[1]) # 保存股票代码
|
||||
|
||||
# 清空现有数据
|
||||
for item in self.market_table.get_children():
|
||||
self.market_table.delete(item)
|
||||
|
||||
# 填充市场数据
|
||||
tmp = self.marketData.copy()
|
||||
for stock_code, data in tmp.items():
|
||||
# 处理时间格式,仅显示 hh:mm:ss
|
||||
time_str = data['time']
|
||||
# 如果时间字符串包含空格,说明包含日期和时间,只取时间部分
|
||||
if ' ' in time_str:
|
||||
time_str = time_str.split(' ')[1]
|
||||
|
||||
# 确保时间格式为 hh:mm:ss,如果只有 hh:mm 则补充 :00
|
||||
if ':' in time_str:
|
||||
time_components = time_str.split(':')
|
||||
if len(time_components) == 2:
|
||||
# 只有小时和分钟,补充秒
|
||||
time_str = f"{time_components[0]}:{time_components[1]}:00"
|
||||
elif len(time_components) >= 3:
|
||||
# 有小时、分钟和秒,只取前三个部分
|
||||
time_str = f"{time_components[0]}:{time_components[1]}:{time_components[2]}"
|
||||
|
||||
values = [
|
||||
time_str,
|
||||
data['stock_name']+f"-{stock_code}",
|
||||
f"{data['last_price']:.3f}",
|
||||
stock_code
|
||||
]
|
||||
self.market_table.insert('', tk.END, values=values)
|
||||
|
||||
# 恢复之前选中的项
|
||||
if selected_values:
|
||||
for item in self.market_table.get_children():
|
||||
values = self.market_table.item(item)['values']
|
||||
if values and values[1] in selected_values: # 比较股票代码
|
||||
self.market_table.selection_add(item)
|
||||
|
||||
def on_market_table_double_click(self, event):
|
||||
"""市场监控表格双击事件"""
|
||||
selected = self.market_table.selection()
|
||||
if selected:
|
||||
item = selected[0]
|
||||
values = self.market_table.item(item)['values']
|
||||
print(values)
|
||||
stock_name = values[1]
|
||||
last_price = values[2]
|
||||
stock_code = values[3]
|
||||
|
||||
# 检查是否已在交易池中
|
||||
is_in_trade_pool = any(target.stock_code == stock_code for target in self.tradeTargetData.values())
|
||||
|
||||
if is_in_trade_pool:
|
||||
messagebox.showinfo("提示", f"{stock_code} ({stock_name}) 已在交易池中")
|
||||
else:
|
||||
result = messagebox.askyesno(
|
||||
"添加交易标的",
|
||||
f"确定要将以下股票添加到交易池吗?\n\n"
|
||||
f"股票代码: {stock_code}\n"
|
||||
f"股票名称: {stock_name}\n"
|
||||
f"最新价格: {last_price}"
|
||||
)
|
||||
|
||||
if result:
|
||||
# 发布事件通知主控制器添加标的
|
||||
self.addTradeTarget(stock_code)
|
||||
|
||||
def get_trade_enabled_indicator(self, target: SFGridTradeTarget) -> str:
|
||||
"""获取交易状态指示器"""
|
||||
if target.status == -1:
|
||||
return "请做交易设置"
|
||||
elif target.status >= 0:
|
||||
if target.enabled:
|
||||
return "▶ 运行中"
|
||||
else:
|
||||
return "⏸ 已停止"
|
||||
|
||||
|
||||
def populate_trade_table(self):
|
||||
"""填充交易标的表格数据"""
|
||||
for id, target in self.tradeTargetData.items():
|
||||
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
|
||||
]
|
||||
|
||||
self.trade_table.insert('', tk.END, values=values)
|
||||
|
||||
|
||||
def on_table_double_click(self, event):
|
||||
"""表格双击事件"""
|
||||
selected = self.trade_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()
|
||||
|
||||
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]
|
||||
|
||||
return None
|
||||
|
||||
def refresh_table(self):
|
||||
"""刷新表格数据"""
|
||||
# 保存当前选中的项
|
||||
selected_items = self.trade_table.selection()
|
||||
selected_values = []
|
||||
for item in selected_items:
|
||||
values = self.trade_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()
|
||||
|
||||
# 恢复之前选中的项
|
||||
if selected_values:
|
||||
for item in self.trade_table.get_children():
|
||||
values = self.trade_table.item(item)['values']
|
||||
if values and values[0] in selected_values:
|
||||
self.trade_table.selection_add(item)
|
||||
|
||||
# 刷新市场监控表格
|
||||
self.populate_market_table()
|
||||
|
||||
def create_grid_view_window(self, target: SFGridTradeTarget):
|
||||
"""创建网格配置查看窗口(只读)"""
|
||||
# 获取顶层窗口
|
||||
root = self.winfo_toplevel()
|
||||
|
||||
# 创建顶层窗口
|
||||
view_window = tk.Toplevel(root)
|
||||
view_window.title(f"网格配置查看 - {target.stock_code} ({target.stock_name})")
|
||||
view_window.geometry("500x450")
|
||||
view_window.resizable(False, False)
|
||||
|
||||
# 设置窗口模态
|
||||
view_window.transient(root)
|
||||
view_window.grab_set()
|
||||
|
||||
# 居中显示
|
||||
root.update_idletasks()
|
||||
x = root.winfo_x() + (root.winfo_width() // 2) - 250
|
||||
y = root.winfo_y() + (root.winfo_height() // 2) - 225
|
||||
view_window.geometry(f"500x450+{x}+{y}")
|
||||
|
||||
# 创建主框架
|
||||
main_frame = ttk.Frame(view_window, padding=20)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 显示股票信息
|
||||
info_frame = ttk.LabelFrame(main_frame, text="标的详情", padding=10)
|
||||
info_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
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)
|
||||
|
||||
# 创建网格配置查看框架
|
||||
config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=10)
|
||||
config_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# 基准价格
|
||||
base_price_frame = ttk.Frame(config_frame)
|
||||
base_price_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(base_price_frame, text="基准价格:", width=15).pack(side=tk.LEFT)
|
||||
ttk.Label(base_price_frame, text=f"{target.grid_start_price:.3f}", width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(base_price_frame, text="元", foreground='gray').pack(side=tk.LEFT)
|
||||
|
||||
# 网格大小
|
||||
grid_size_frame = ttk.Frame(config_frame)
|
||||
grid_size_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(grid_size_frame, text="网格大小:", width=15).pack(side=tk.LEFT)
|
||||
ttk.Label(grid_size_frame, text=f"{target.grid_size:.3f}", width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(grid_size_frame, text="元", foreground='gray').pack(side=tk.LEFT)
|
||||
|
||||
# 网格交易量
|
||||
grid_volume_frame = ttk.Frame(config_frame)
|
||||
grid_volume_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(grid_volume_frame, text="网格交易量:", width=15).pack(side=tk.LEFT)
|
||||
ttk.Label(grid_volume_frame, text=str(target.grid_volume), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(grid_volume_frame, text="股", foreground='gray').pack(side=tk.LEFT)
|
||||
|
||||
# 上方网格数量
|
||||
upper_count_frame = ttk.Frame(config_frame)
|
||||
upper_count_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(upper_count_frame, text="上方网格数量:", width=15).pack(side=tk.LEFT)
|
||||
ttk.Label(upper_count_frame, text=str(target.grid_upper_count), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(upper_count_frame, text="格", foreground='gray').pack(side=tk.LEFT)
|
||||
|
||||
# 下方网格数量
|
||||
lower_count_frame = ttk.Frame(config_frame)
|
||||
lower_count_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(lower_count_frame, text="下方网格数量:", width=15).pack(side=tk.LEFT)
|
||||
ttk.Label(lower_count_frame, text=str(target.grid_lower_count), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(lower_count_frame, text="格", foreground='gray').pack(side=tk.LEFT)
|
||||
|
||||
# 生成网格价格序列
|
||||
price_grid_frame = ttk.LabelFrame(main_frame, text="网格价格序列", padding=10)
|
||||
price_grid_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# 计算并显示网格价格序列
|
||||
price_list = target.getPriceGrid()
|
||||
price_text = ", ".join([f"{price:.3f}" for price in price_list])
|
||||
|
||||
# 创建文本框显示网格价格序列
|
||||
text_frame = ttk.Frame(price_grid_frame)
|
||||
text_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
text_widget = tk.Text(text_frame, height=4, wrap=tk.WORD)
|
||||
text_widget.insert(tk.END, price_text)
|
||||
text_widget.config(state=tk.DISABLED) # 只读
|
||||
|
||||
scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_widget.yview)
|
||||
text_widget.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# 关闭按钮
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(fill=tk.X, pady=(10, 0))
|
||||
ttk.Button(button_frame, text="关闭", command=view_window.destroy).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
def create_grid_config_window(self, target: SFGridTradeTarget):
|
||||
"""创建网格配置窗口(可编辑)"""
|
||||
# 获取顶层窗口
|
||||
root = self.winfo_toplevel()
|
||||
|
||||
# 创建顶层窗口
|
||||
config_window = tk.Toplevel(root)
|
||||
config_window.title(f"网格配置 - {target.stock_code} ({target.stock_name})")
|
||||
config_window.geometry("550x550")
|
||||
config_window.resizable(False, False)
|
||||
|
||||
# 设置窗口模态
|
||||
config_window.transient(root)
|
||||
config_window.grab_set()
|
||||
|
||||
# 居中显示
|
||||
root.update_idletasks()
|
||||
x = root.winfo_x() + (root.winfo_width() // 2) - 275
|
||||
y = root.winfo_y() + (root.winfo_height() // 2) - 275
|
||||
config_window.geometry(f"550x550+{x}+{y}")
|
||||
|
||||
# 创建主框架
|
||||
main_frame = ttk.Frame(config_window, padding=20)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 显示股票信息
|
||||
info_frame = ttk.LabelFrame(main_frame, text="标的详情", padding=10)
|
||||
info_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
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)
|
||||
|
||||
# 创建网格配置框架
|
||||
config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=15)
|
||||
config_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# 创建输入框字典用于保存引用
|
||||
entries = {}
|
||||
|
||||
# 基准价格
|
||||
base_price_frame = ttk.Frame(config_frame)
|
||||
base_price_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(base_price_frame, text="基准价格:", width=15).pack(side=tk.LEFT)
|
||||
base_price_entry = ttk.Entry(base_price_frame, width=15)
|
||||
base_price_entry.insert(0, str(target.grid_start_price))
|
||||
base_price_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(base_price_frame, text="元", foreground='gray').pack(side=tk.LEFT)
|
||||
entries['grid_start_price'] = base_price_entry
|
||||
|
||||
# 网格大小
|
||||
grid_size_frame = ttk.Frame(config_frame)
|
||||
grid_size_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(grid_size_frame, text="网格大小:", width=15).pack(side=tk.LEFT)
|
||||
grid_size_entry = ttk.Entry(grid_size_frame, width=15)
|
||||
grid_size_entry.insert(0, str(target.grid_size))
|
||||
grid_size_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(grid_size_frame, text="元", foreground='gray').pack(side=tk.LEFT)
|
||||
entries['grid_size'] = grid_size_entry
|
||||
|
||||
# 网格交易量
|
||||
grid_volume_frame = ttk.Frame(config_frame)
|
||||
grid_volume_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(grid_volume_frame, text="网格交易量:", width=15).pack(side=tk.LEFT)
|
||||
grid_volume_entry = ttk.Entry(grid_volume_frame, width=15)
|
||||
grid_volume_entry.insert(0, str(target.grid_volume))
|
||||
grid_volume_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(grid_volume_frame, text="手", foreground='gray').pack(side=tk.LEFT)
|
||||
entries['grid_volume'] = grid_volume_entry
|
||||
|
||||
# 上方网格数量
|
||||
upper_count_frame = ttk.Frame(config_frame)
|
||||
upper_count_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(upper_count_frame, text="上方网格数量:", width=15).pack(side=tk.LEFT)
|
||||
upper_count_entry = ttk.Entry(upper_count_frame, width=15)
|
||||
upper_count_entry.insert(0, str(target.grid_upper_count))
|
||||
upper_count_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(upper_count_frame, text="格", foreground='gray').pack(side=tk.LEFT)
|
||||
entries['grid_upper_count'] = upper_count_entry
|
||||
|
||||
# 下方网格数量
|
||||
lower_count_frame = ttk.Frame(config_frame)
|
||||
lower_count_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(lower_count_frame, text="下方网格数量:", width=15).pack(side=tk.LEFT)
|
||||
lower_count_entry = ttk.Entry(lower_count_frame, width=15)
|
||||
lower_count_entry.insert(0, str(target.grid_lower_count))
|
||||
lower_count_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(lower_count_frame, text="格", foreground='gray').pack(side=tk.LEFT)
|
||||
entries['grid_lower_count'] = lower_count_entry
|
||||
|
||||
# 预览按钮和结果显示
|
||||
preview_frame = ttk.LabelFrame(main_frame, text="网格价格序列预览", padding=10)
|
||||
preview_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
preview_result = tk.StringVar(value="点击'预览'查看生成的网格价格序列")
|
||||
|
||||
def calculate_grid_prices():
|
||||
"""计算网格价格序列"""
|
||||
try:
|
||||
base_price = float(base_price_entry.get())
|
||||
grid_size = float(grid_size_entry.get())
|
||||
upper_count = int(upper_count_entry.get())
|
||||
lower_count = int(lower_count_entry.get())
|
||||
|
||||
prices = []
|
||||
|
||||
# 计算上方网格价格
|
||||
for i in range(upper_count, 0, -1):
|
||||
price = base_price + grid_size * i
|
||||
prices.append(round(price, 3))
|
||||
|
||||
# 添加基准价格
|
||||
prices.append(base_price)
|
||||
|
||||
# 计算下方网格价格
|
||||
for i in range(1, lower_count + 1):
|
||||
price = base_price - grid_size * i
|
||||
# 确保价格不为负
|
||||
if price >= 0:
|
||||
prices.append(round(price, 3))
|
||||
else:
|
||||
break
|
||||
|
||||
return prices
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def update_preview():
|
||||
"""更新网格价格序列预览"""
|
||||
prices = calculate_grid_prices()
|
||||
if prices:
|
||||
price_str = ", ".join([str(p) for p in prices])
|
||||
preview_result.set(f"网格价格序列: {price_str}")
|
||||
else:
|
||||
preview_result.set("参数错误,请检查输入!")
|
||||
|
||||
# 绑定输入变化自动预览
|
||||
for entry_widget in entries.values():
|
||||
entry_widget.bind("<KeyRelease>", lambda e: update_preview())
|
||||
entry_widget.bind("<FocusOut>", lambda e: update_preview())
|
||||
|
||||
# 预览按钮
|
||||
preview_button_frame = ttk.Frame(preview_frame)
|
||||
preview_button_frame.pack(fill=tk.X, pady=5)
|
||||
# ttk.Button(preview_button_frame, text="预览", command=update_preview).pack(side=tk.LEFT)
|
||||
|
||||
# 预览结果显示
|
||||
preview_label = ttk.Label(preview_button_frame, textvariable=preview_result, foreground='blue')
|
||||
preview_label.pack(side=tk.LEFT, padx=10)
|
||||
|
||||
# 初始预览
|
||||
update_preview()
|
||||
|
||||
# 按钮框架
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(fill=tk.X, pady=(10, 0))
|
||||
|
||||
def save_config():
|
||||
"""保存配置"""
|
||||
try:
|
||||
# 获取输入值
|
||||
grid_start_price = float(base_price_entry.get())
|
||||
grid_size = float(grid_size_entry.get())
|
||||
grid_volume = int(grid_volume_entry.get())
|
||||
grid_upper_count = int(upper_count_entry.get())
|
||||
grid_lower_count = int(lower_count_entry.get())
|
||||
|
||||
# 更新target对象(使用setattr来正确设置Peewee字段的值)
|
||||
setattr(target, 'grid_start_price', grid_start_price)
|
||||
setattr(target, 'grid_size', grid_size)
|
||||
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)
|
||||
|
||||
# 更新策略控制器
|
||||
self.updateTradeTarget(target, True) # 网格配置变更
|
||||
|
||||
# 关闭窗口
|
||||
config_window.destroy()
|
||||
|
||||
# 添加日志
|
||||
PrintLog(LogLevel.INFO, f"网格配置已保存: {target.stock_code} - {target.stock_name}")
|
||||
messagebox.showinfo("成功", "网格配置已保存!")
|
||||
|
||||
except ValueError:
|
||||
messagebox.showerror("错误", "输入参数有误,请检查!")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"保存配置失败:{str(e)}")
|
||||
PrintLog(LogLevel.ERROR, f"保存网格配置失败: {str(e)}")
|
||||
|
||||
# 保存和取消按钮
|
||||
ttk.Button(button_frame, text="保存", command=save_config).pack(side=tk.RIGHT, padx=5)
|
||||
ttk.Button(button_frame, text="取消", command=config_window.destroy).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
def decrease_grid_index(self, grid_index_var: tk.IntVar, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
|
||||
"""减少网格序号"""
|
||||
current_value = grid_index_var.get()
|
||||
if current_value > 0:
|
||||
grid_index_var.set(current_value - 1)
|
||||
# 同步更新需求持仓量和持仓状态
|
||||
self.update_required_position_and_status(grid_index_var.get(), target, required_position_label, position_status_label)
|
||||
|
||||
def increase_grid_index(self, grid_index_var: tk.IntVar, max_index: int, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
|
||||
"""增加网格序号"""
|
||||
current_value = grid_index_var.get()
|
||||
if current_value < max_index:
|
||||
grid_index_var.set(current_value + 1)
|
||||
# 同步更新需求持仓量和持仓状态
|
||||
self.update_required_position_and_status(grid_index_var.get(), target, required_position_label, position_status_label)
|
||||
|
||||
def update_position_status(self, current_position: int, required_position: int, status_label: ttk.Label):
|
||||
"""更新持仓量状态提示"""
|
||||
if current_position >= required_position:
|
||||
status_label.config(text="持仓量充足", foreground="green")
|
||||
else:
|
||||
shortage = required_position - current_position
|
||||
status_label.config(text=f"还需补充 {shortage} 手仓位", foreground="red")
|
||||
|
||||
|
||||
def update_required_position_and_status(self, grid_index: int, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
|
||||
"""更新需求持仓量和持仓状态"""
|
||||
# 计算需求持仓量
|
||||
required_position:int = grid_index * target.grid_volume # type: ignore
|
||||
required_position_label.config(text=str(required_position))
|
||||
|
||||
# 更新持仓量状态
|
||||
current_position = getattr(target, 'current_position')
|
||||
self.update_position_status(current_position, required_position, position_status_label)
|
||||
|
||||
|
||||
# 交易池管理
|
||||
def addTradeTarget(self, stock_code: str, gridIndex: int = 1): # 新增
|
||||
"""处理添加交易标的事件"""
|
||||
try:
|
||||
stock_name = qmtv.getInstrumentName(stock_code)
|
||||
if not stock_name:
|
||||
PrintLog(LogLevel.ERROR, f'无法获取股票代码 {stock_code} 的名称,请检查代码是否正确')
|
||||
return
|
||||
PrintLog(LogLevel.DEBUG, f'添加交易标的: {stock_code} {stock_name}')
|
||||
|
||||
# 检查是否已存在该标的
|
||||
existing_target = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == stock_code)
|
||||
if existing_target:
|
||||
PrintLog(LogLevel.INFO, f'交易标的 {stock_code} {stock_name} 已存在')
|
||||
return
|
||||
|
||||
# 刷新标的持仓
|
||||
pos = qmtv.getStockPosition(stock_code) # type: ignore
|
||||
new_target = SFGridTradeTarget.create(
|
||||
stock_name=stock_name,
|
||||
stock_code=stock_code,
|
||||
current_position="0" if pos is None else str(pos.volume),
|
||||
grid_index=gridIndex,
|
||||
init_price=0.0,
|
||||
status=-1
|
||||
)
|
||||
# 更新标的池
|
||||
self.updateTradeTarget(new_target, True) # 新增标的,相当于也是初始化
|
||||
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'新增交易标的失败 {stock_code} {e}')
|
||||
|
||||
# button handlers =============================================================================================
|
||||
def btnHandlerGridCorrect(self):
|
||||
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
self.create_grid_correction_window(target)
|
||||
|
||||
|
||||
def btnHandlerToggleMarketMonitor(self):
|
||||
"""切换市场监控窗口显示/隐藏"""
|
||||
if self.market_monitor_visible:
|
||||
# 隐藏市场监控窗口
|
||||
self.market_frame.pack_forget()
|
||||
self.market_monitor_visible = False
|
||||
else:
|
||||
# 显示市场监控窗口
|
||||
self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
|
||||
self.market_monitor_visible = True
|
||||
|
||||
def btnHandlerTradeSettings(self):
|
||||
"""网格配置功能"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
# 检查标的的状态,status为1时仅可查看
|
||||
if target.status == -1 or target.status == 0:
|
||||
self.create_grid_config_window(target)
|
||||
else:
|
||||
# 创建只读的网格配置查看窗口
|
||||
self.create_grid_view_window(target)
|
||||
|
||||
def btnHandlerStartSelectedTrade(self):
|
||||
"""启动选中的交易"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
if target.status < 0:
|
||||
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 未配置交易参数, 请做交易设置。")
|
||||
return
|
||||
|
||||
if target.enabled: # type: ignore
|
||||
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经在运行中")
|
||||
return
|
||||
|
||||
result = messagebox.askyesno(
|
||||
"确认启动",
|
||||
f"确定要启动以下交易标的吗?\n\n"
|
||||
f"股票代码: {target.stock_code}\n"
|
||||
f"股票名称: {target.stock_name}"
|
||||
)
|
||||
|
||||
if result:
|
||||
PrintLog(LogLevel.INFO, f'启动标的交易 {target.targetName()}')
|
||||
target.enabled = True # type: ignore
|
||||
|
||||
id = target.get_id()
|
||||
if id in self.strategy_ctrl:
|
||||
tradeController: SFGridStrategy = self.strategy_ctrl[target.get_id()]
|
||||
tradeTarget = tradeController.enabledTrading(True)
|
||||
self.tradeTargetData[id] = tradeTarget
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f"\t创建标的交易控制器 {target.targetName()}")
|
||||
|
||||
def btnHandlerStopSelectedTrade(self):
|
||||
"""暂停选中的交易"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
if not target.enabled: # type: ignore
|
||||
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经是暂停状态")
|
||||
return
|
||||
|
||||
result = messagebox.askyesno(
|
||||
"确认暂停",
|
||||
f"确定要暂停以下交易标的吗?\n\n"
|
||||
f"股票代码: {target.stock_code}\n"
|
||||
f"股票名称: {target.stock_name}"
|
||||
)
|
||||
|
||||
if result:
|
||||
PrintLog(LogLevel.INFO, f'暂停标的交易 {target.targetName()}')
|
||||
id = target.get_id()
|
||||
if id in self.strategy_ctrl:
|
||||
tradeController: SFGridStrategy = self.strategy_ctrl[target.get_id()]
|
||||
tradeController.enabledTrading(False)
|
||||
else:
|
||||
print(f"标的交易控制器不存在 {target.stock_code} {target.stock_name}\n")
|
||||
|
||||
def btnHandlerDelSelectedTradeTarget(self):
|
||||
"""删除选中的交易标的"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
result = messagebox.askyesno(
|
||||
"确认删除",
|
||||
f"确定要删除以下交易标的吗?\n\n"
|
||||
f"股票代码: {target.stock_code}\n"
|
||||
f"股票名称: {target.stock_name}\n\n"
|
||||
f"⚠️ 此操作不可恢复!",
|
||||
icon='warning'
|
||||
)
|
||||
|
||||
if result:
|
||||
id = target.get_id()
|
||||
# try:
|
||||
if id in self.strategy_ctrl:
|
||||
ctrl = self.strategy_ctrl[id]
|
||||
ctrl.deleteTradeTarget(target)
|
||||
else:
|
||||
self.onTradeTargetDeleted(target)
|
||||
PrintLog(LogLevel.INFO, f"已发送删除请求: {target.stock_code} - {target.stock_name}")
|
||||
|
||||
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
|
||||
|
||||
def btnHandlerAddTradeTarget(self):
|
||||
"""添加新的交易标的"""
|
||||
# 获取顶层窗口
|
||||
root = self.winfo_toplevel()
|
||||
|
||||
# 创建顶层窗口
|
||||
add_window = tk.Toplevel(root)
|
||||
add_window.title("添加交易标的")
|
||||
add_window.geometry("400x150")
|
||||
add_window.resizable(False, False)
|
||||
|
||||
# 设置窗口模态
|
||||
add_window.transient(root)
|
||||
add_window.grab_set()
|
||||
|
||||
# 居中显示
|
||||
root.update_idletasks()
|
||||
x = root.winfo_x() + (root.winfo_width() // 2) - 200
|
||||
y = root.winfo_y() + (root.winfo_height() // 2) - 75
|
||||
add_window.geometry(f"400x150+{x}+{y}")
|
||||
|
||||
# 创建输入框架
|
||||
input_frame = ttk.Frame(add_window, padding=20)
|
||||
input_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 股票代码输入
|
||||
ttk.Label(input_frame, text="股票代码:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
stock_code_entry = ttk.Entry(input_frame, width=30)
|
||||
stock_code_entry.grid(row=0, column=1, pady=5, padx=(10, 0))
|
||||
stock_code_entry.focus()
|
||||
|
||||
# 按钮框架
|
||||
button_frame = ttk.Frame(input_frame)
|
||||
button_frame.grid(row=1, column=0, columnspan=2, pady=20)
|
||||
|
||||
def confirm_add():
|
||||
stock_code = stock_code_entry.get().strip()
|
||||
if not stock_code:
|
||||
messagebox.showwarning("输入错误", "请输入股票代码")
|
||||
return
|
||||
|
||||
# 发布事件通知主控制器添加标的
|
||||
self.addTradeTarget(stock_code)
|
||||
add_window.destroy()
|
||||
|
||||
def cancel_add():
|
||||
add_window.destroy()
|
||||
|
||||
# 确认和取消按钮
|
||||
ttk.Button(button_frame, text="确认", command=confirm_add, width=10).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="取消", command=cancel_add, width=10).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 绑定回车键确认
|
||||
stock_code_entry.bind('<Return>', lambda event: confirm_add())
|
||||
|
||||
PrintLog(LogLevel.INFO, "点击添加交易标的按钮")
|
||||
|
||||
def btnHandlerSetMonitorPrice(self):
|
||||
"""设置监控价格"""
|
||||
try:
|
||||
# 获取输入的价格
|
||||
price_str = self.monitor_price_entry.get()
|
||||
new_price = float(price_str)
|
||||
|
||||
# 更新监控价格
|
||||
self.monitor_price = new_price
|
||||
|
||||
# 清空当前监控的数据
|
||||
self.marketData.clear()
|
||||
self.listening_stock.clear()
|
||||
|
||||
# 清空市场监控表格
|
||||
for item in self.market_table.get_children():
|
||||
self.market_table.delete(item)
|
||||
|
||||
PrintLog(LogLevel.INFO, f"监控价格已更新为: {new_price}")
|
||||
except ValueError:
|
||||
messagebox.showerror("错误", "请输入有效的数字")
|
||||
@@ -0,0 +1,158 @@
|
||||
# coding:utf-8
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
import configparser
|
||||
from core.main_ui import MainWindow
|
||||
import config as sdConstants
|
||||
from core.qmt import qmtv
|
||||
|
||||
class ConfigWindow:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("系统配置")
|
||||
self.root.geometry("500x250")
|
||||
self.root.resizable(False, False)
|
||||
|
||||
# 居中显示
|
||||
self.root.withdraw() # 先隐藏窗口
|
||||
self.root.update_idletasks()
|
||||
x = (self.root.winfo_screenwidth() // 2) - (500 // 2)
|
||||
y = (self.root.winfo_screenheight() // 2) - (250 // 2)
|
||||
self.root.geometry(f"500x250+{x}+{y}")
|
||||
self.root.deiconify() # 再显示窗口
|
||||
|
||||
self.miniQMTPath = tk.StringVar()
|
||||
self.account_no = tk.StringVar()
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
# 创建主框架
|
||||
main_frame = ttk.Frame(self.root, padding="20")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# miniQMT路径配置
|
||||
path_frame = ttk.Frame(main_frame)
|
||||
path_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
path_label = ttk.Label(path_frame, text="miniQMT路径:")
|
||||
path_label.pack(side=tk.LEFT)
|
||||
|
||||
path_entry = ttk.Entry(path_frame, textvariable=self.miniQMTPath, width=40)
|
||||
path_entry.pack(side=tk.LEFT, padx=(10, 5), fill=tk.X, expand=True)
|
||||
|
||||
browse_btn = ttk.Button(path_frame, text="浏览", command=self.browse_folder)
|
||||
browse_btn.pack(side=tk.LEFT)
|
||||
|
||||
# 资金账号配置
|
||||
account_frame = ttk.Frame(main_frame)
|
||||
account_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
account_label = ttk.Label(account_frame, text="资金账号:")
|
||||
account_label.pack(side=tk.LEFT)
|
||||
|
||||
account_entry = ttk.Entry(account_frame, textvariable=self.account_no, width=40)
|
||||
account_entry.pack(side=tk.LEFT, padx=(10, 0))
|
||||
|
||||
# 说明文本
|
||||
info_label = ttk.Label(
|
||||
main_frame,
|
||||
text="请配置miniQMT的userdata_mini路径和资金账号\n路径示例: D:/Programs/DTQMT/userdata_mini",
|
||||
foreground="gray"
|
||||
)
|
||||
info_label.pack(pady=10)
|
||||
|
||||
# 按钮框架
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(fill=tk.X, pady=10)
|
||||
|
||||
save_btn = ttk.Button(button_frame, text="保存配置", command=self.save_config)
|
||||
save_btn.pack(side=tk.RIGHT)
|
||||
|
||||
cancel_btn = ttk.Button(button_frame, text="取消", command=self.root.destroy)
|
||||
cancel_btn.pack(side=tk.RIGHT, padx=(0, 10))
|
||||
|
||||
def browse_folder(self):
|
||||
folder_selected = filedialog.askdirectory()
|
||||
if folder_selected:
|
||||
self.miniQMTPath.set(folder_selected)
|
||||
|
||||
def save_config(self):
|
||||
mini_qmt_path = self.miniQMTPath.get().strip()
|
||||
account_number = self.account_no.get().strip()
|
||||
|
||||
# 检查miniQMT路径
|
||||
if not mini_qmt_path:
|
||||
messagebox.showerror("错误", "请选择miniQMT路径")
|
||||
return
|
||||
|
||||
if not os.path.exists(mini_qmt_path):
|
||||
messagebox.showerror("错误", "miniQMT路径不存在")
|
||||
return
|
||||
|
||||
# 检查账号
|
||||
if not account_number:
|
||||
messagebox.showerror("错误", "请输入资金账号")
|
||||
return
|
||||
|
||||
# 保存配置
|
||||
config = configparser.ConfigParser()
|
||||
config['config'] = {
|
||||
'miniQMTPath': mini_qmt_path.replace('\\', '/'),
|
||||
'account_no': account_number,
|
||||
'log_level': 'INFO'
|
||||
}
|
||||
|
||||
config_path = sdConstants.get_config_path()
|
||||
try:
|
||||
with open(config_path, 'w') as configfile:
|
||||
config.write(configfile)
|
||||
messagebox.showinfo("成功", "配置已保存")
|
||||
self.root.destroy()
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"保存配置失败: {str(e)}")
|
||||
|
||||
def check_and_create_config():
|
||||
"""检查配置文件,如果不存在则打开配置窗口"""
|
||||
root = tk.Tk()
|
||||
config_window = ConfigWindow(root)
|
||||
root.mainloop()
|
||||
|
||||
def initialize_system():
|
||||
"""初始化系统"""
|
||||
|
||||
try:
|
||||
while True:
|
||||
# 初始化配置
|
||||
if sdConstants.exist_config() and sdConstants.initConfig():
|
||||
# 初始化qmtv
|
||||
qmtv.init_qmtv()
|
||||
connected = qmtv.connect()
|
||||
if connected:
|
||||
# 连接成功,启动主窗口
|
||||
window = MainWindow(sdConstants.log_level)
|
||||
window.run()
|
||||
break
|
||||
else:
|
||||
option = messagebox.askokcancel("连接失败", "QMT连接失败,请检查")
|
||||
if option:
|
||||
check_and_create_config()
|
||||
else:
|
||||
break
|
||||
else:
|
||||
option = messagebox.askokcancel("错误", "请检查配置")
|
||||
if option:
|
||||
check_and_create_config()
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"系统初始化失败: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import tkinter as tk
|
||||
root = tk.Tk()
|
||||
app = MainBoardWindow(root)
|
||||
app.run()
|
||||
|
||||
# initialize_system()
|
||||
Reference in New Issue
Block a user