Merge branch 'new_structure'

# Conflicts:
#	config.ini
#	config.py
This commit is contained in:
2025-11-12 11:53:35 +08:00
20 changed files with 852 additions and 819 deletions
+2
View File
@@ -1 +1,3 @@
__pycache__/
dist/
example.db
+3 -2
View File
@@ -1,5 +1,6 @@
[config]
miniqmtpath = D:/Programs/QMT/userdata_mini
miniqmtpath = D:/Programs/DTQMT/userdata_mini
grid_price = 11.0,10.0,9.0,8.0,7.0,6.0,5.0,4.0,3.0,2.0,1.0,0.0
grid_volume = 100
account_no = 00000000
account_no = 99082560
+7 -13
View File
@@ -1,6 +1,6 @@
from typing import List
import configparser
import os
from pathlib import Path
import sys
# miniQMTPath = r'D:\\Programs\\DTQMT_MN\\userdata_mini' # miniQMT软件的安装路径
@@ -10,21 +10,21 @@ miniQMTPath = r'D:\\Programs\\DTQMT\\userdata_mini' # miniQMT软件的安装路
grid_price:List[float] = [] # 网格价格设置,从高到低
grid_volume:int = 100 # 每个网格的交易手数
account_no:str = '99082560'
console_log = True
# account_no:str = '89009170' # 交易账号
def get_config_path():
def get_config_path() -> Path:
"""获取配置文件的正确路径(兼容开发环境和打包后的可执行文件)"""
if getattr(sys, 'frozen', False):
# 打包后的可执行文件环境
# sys._MEIPASS是PyInstaller解压临时文件的目录
# 配置文件应该放在可执行文件同目录下
base_path = os.path.dirname(sys.executable)
base_path = Path(sys.executable).parent
else:
# 开发环境
base_path = os.path.dirname(os.path.abspath(__file__))
print(f'base_path: {base_path}')
base_path = Path(__file__).resolve().parent
return os.path.join(base_path, 'config.ini')
return base_path / 'config.ini'
def create_default_config():
"""创建默认配置文件"""
@@ -47,7 +47,7 @@ def initConfig():
config_path = get_config_path()
# 检查配置文件是否存在,不存在则创建
if not os.path.exists(config_path):
if not config_path.exists():
create_default_config()
config = configparser.ConfigParser()
@@ -57,9 +57,3 @@ def initConfig():
grid_price = [float(item) for item in str_list]
grid_volume = config.getint('config','grid_volume')
account_no = config.get('config','account_no')
# 控制台打印
print(f'miniQMTPath: {miniQMTPath}')
print(f'grid_price: {grid_price}')
print(f'grid_volume: {grid_volume}')
print(f'account_no: {account_no}')
+6
View File
@@ -0,0 +1,6 @@
import xtquant.xtconstant as xtconstant
OrderTypeBuy = f'{xtconstant.STOCK_BUY}' # 买
OrderTypeSell = f'{xtconstant.STOCK_SELL}' # 卖
OrderTypeInit = "0" # 建仓
OrderTypeNone = "None"
+12
View File
@@ -0,0 +1,12 @@
from peewee import SqliteDatabase, Model
from core.logger import LogLevel, PrintLog
# 连接到SQLite数据库
db: SqliteDatabase = SqliteDatabase('example.db')
db.connect()
PrintLog(LogLevel.INFO, '- [成功]数据库连接')
# 定义基础模型类
class BaseModel(Model):
class Meta:
database: SqliteDatabase = db
+1 -13
View File
@@ -1,22 +1,10 @@
# 定义事件处理函数
ActionEventEnableTrade = "enable_trade"
ResultEventTradeEnabled = "trade_enabled"
ActionEventDisableTrade = "disable_trade"
ResultEventTradeDisabled = "trade_disabled"
# 市场数据监听控制事件
MarketDataUpdate = "market_data_update"
ActionEnableMarketData = "enable_market_data"
ActionDisableMarketData = "disable_market_data"
MarketDataEnabled = "market_data_enabled"
MarketDataDisabled = "market_data_disabled"
# 删除交易标的事件
EventTradeTargetUpdate = "trade_target_update"
ActionEventAddTradeTarget = "add_trade_target"
ResultEventTradeTargetAdded = "trade_target_added"
ActionEventDeleteTradeTarget = "delete_trade_target"
ResultEventTradeTargetDeleted = "trade_target_deleted"
# 网格修正事件
ActionEventGridFix = "grid_fix"
# Pring Log
EventPrintLog = "print_log"
+3
View File
@@ -1,6 +1,7 @@
from enum import Enum
from core.eventbus import EventPrintLog, event_bus
import config
class LogLevel(Enum):
DEBUG = "DEBUG"
@@ -17,3 +18,5 @@ class LogData:
def PrintLog(level:LogLevel, message:str):
data = LogData(level, message)
event_bus.publish(EventPrintLog, data)
if config.console_log:
print(f'{level.value} {message}')
-402
View File
@@ -1,402 +0,0 @@
# coding:utf-8
from core.strategy_db import TradeTarget
from core.eventbus import ActionDisableMarketData, ActionEnableMarketData, ActionEventAddTradeTarget, ActionEventDeleteTradeTarget, ActionEventDisableTrade, ActionEventEnableTrade, EventTradeTargetUpdate, MarketDataUpdate, MarketDataEnabled, MarketDataDisabled, ResultEventTradeDisabled, ResultEventTradeEnabled, ResultEventTradeTargetDeleted, ActionEventGridFix, event_bus
from xtquant.xttrader import XtQuantTrader
import time
from peewee import ModelSelect
import core.strategy_db as strategy_db
import sfgrid_constants
from core.sfgrid_strategy import SFGridStrategy
from core.util import getInstrumentName, getStockPosition, queryPendingOrder
from xtquant.xttrader import XtQuantTrader
from xtquant.xttype import StockAccount, XtAsset, XtOrder, XtOrderResponse, XtPosition, XtTrade
from xtquant import xtdata
from xtquant.xttrader import XtQuantTraderCallback
import datetime
import core.ui as ui
from core.logger import PrintLog, LogLevel
from core.objects import GridFixData
# 量化核心控制对象
class SFGridController(XtQuantTraderCallback):
def __init__(self, account_no: str, miniQmtPath: str):
super().__init__()
self.registerEventHandler()
self.appUi = ui.TradeTargetUI()
xtdata.enable_hello = False
session_id = int(time.time())
self.xt_trader: XtQuantTrader = XtQuantTrader(miniQmtPath, session_id)
self.xt_trader.register_callback(self)
self.xt_trader.start()
self.xt_trader.connect()
PrintLog(LogLevel.INFO, f'- [{'成功' if self.xt_trader.connected else '失败'}]市场交易连接: {miniQmtPath}')
if self.xt_trader.connected == False:
self.inited: bool = False
return
else:
self.inited = True
self.account= StockAccount(account_no, 'STOCK')
PrintLog(LogLevel.INFO, f'- [成功]交易账号对象初始化完成, 账号: {self.account.account_id}') # pyright: ignore[reportAttributeAccessIssue]
subscribe_result = self.xt_trader.subscribe(self.account)
PrintLog(LogLevel.INFO, f'- [{'成功' if subscribe_result == 0 else '失败'}:{subscribe_result}]交易状态订阅')
if subscribe_result == 0:
self.inited = True
else:
self.inited = False
return
self.listening_stock = []
self.stock_trade_ctrl = {}
self.init_instrument_pool(self.xt_trader, self.account) # type: ignore
self.seq = None
PrintLog(LogLevel.INFO, '- [成功]三疯交易系统初始化完成')
self.startMarketData()
def registerEventHandler(self):
event_bus.subscribe(ActionEventEnableTrade, self.onEnableTrade)
event_bus.subscribe(ActionEventDisableTrade, self.onDisableTrade)
event_bus.subscribe(ActionEnableMarketData, self.onMarketDataEnabled)
event_bus.subscribe(ActionDisableMarketData, self.onMarketDataDisabled)
event_bus.subscribe(ActionEventAddTradeTarget, self.onAddTradeTarget)
event_bus.subscribe(ActionEventDeleteTradeTarget, self.onDeleteTradeTarget)
event_bus.subscribe(ActionEventGridFix, self.onGridFix)
def onDeleteTradeTarget(self, id: int):
"""处理删除交易标的事件"""
self.del_trade_target(id)
# 发布删除完成事件
event_bus.publish(ResultEventTradeTargetDeleted, id)
def onAddTradeTarget(self, stock_code: str):
"""处理添加交易标的事件"""
self.add_trade_target(stock_code)
def onMarketDataEnabled(self, data):
"""处理市场数据监听启用事件"""
self.startMarketData()
def onMarketDataDisabled(self, data):
"""处理市场数据监听禁用事件"""
self.stopMarketData()
def onEnableTrade(self, id: int):
self.start_stock_trade(id)
def onDisableTrade(self, id: int):
self.pause_stock_trade(id)
def onGridFix(self, data: GridFixData):
"""处理网格修正事件"""
self.update_trade_target_grid(data)
def update_trade_target_grid(self, data: GridFixData):
"""更新交易标的网格信息"""
try:
target = data.tradeTarget
grid_index = data.grid_index
# 更新数据库中的网格索引
target.grid_index = grid_index
target.save()
# 更新内存中的交易标的
if target.get_id() in self.instrument_pool:
self.instrument_pool[target.get_id()] = target
# 更新交易控制器中的网格信息
if target.stock_code in self.stock_trade_ctrl:
trade_controller: SFGridStrategy = self.stock_trade_ctrl[target.stock_code]
trade_controller.updateGridIndex(grid_index) # type: ignore
PrintLog(LogLevel.INFO, f"网格修正已应用: {target.stock_code} - {target.stock_name}, 网格索引: {grid_index}")
except Exception as e:
PrintLog(LogLevel.ERROR, f"网格修正更新失败: {str(e)}")
def hold(self):
self.appUi.run()
def startMarketData(self):
PrintLog(LogLevel.INFO, '- 启动市场数据订阅')
self.seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], callback=self.onDataUpdate)
if self.seq == -1:
PrintLog(LogLevel.ERROR, '- 市场数据订阅失败')
else:
event_bus.publish(MarketDataEnabled, True)
PrintLog(LogLevel.INFO, f'- 市场数据订阅成功, 订阅号={self.seq}')
def stopMarketData(self):
PrintLog(LogLevel.INFO, '- 停止市场数据订阅')
if self.seq is not None and self.seq > 0:
xtdata.unsubscribe_quote(self.seq)
event_bus.publish(MarketDataDisabled, False)
def add_trade_target(self, stock_code: str):
try:
stock_name = getInstrumentName(stock_code)
if not stock_name:
PrintLog(LogLevel.ERROR, f'无法获取股票代码 {stock_code} 的名称,请检查代码是否正确')
return
# 检查是否已存在该标的
existing_target = strategy_db.TradeTarget.get_or_none(strategy_db.TradeTarget.stock_code == stock_code)
if existing_target:
PrintLog(LogLevel.INFO, f'交易标的 {stock_code} {stock_name} 已存在')
return
new_target = strategy_db.TradeTarget.create(
stock_name=stock_name,
stock_code=stock_code,
market_price=0.0,
current_position=0,
grid_index=0,
last_trade_price=0.0,
plan_buy_price=0.0,
plan_sell_price=0.0,
current_order_price=0.0,
current_order_no='',
current_order_type=''
)
new_target.save()
PrintLog(LogLevel.INFO, f'新增交易标的 {stock_code} {stock_name}, {new_target.id}')
# 刷新标的持仓
pos = getStockPosition(stock_code, self.xt_trader, self.account) # type: ignore
strategy_db.TradeTarget.update(current_position=pos).where(strategy_db.TradeTarget.stock_code == stock_code).execute()
# 更新标的池
self.refresh_targets()
# 添加交易控制器
stockTradeController = SFGridStrategy(new_target, self.xt_trader, self.account) # type: ignore
self.stock_trade_ctrl[stock_code] = stockTradeController
except Exception as e:
PrintLog(LogLevel.ERROR, f'新增交易标的失败 {stock_code} {e}')
def del_trade_target(self, id:int):
try:
# 检查标的是否存在
if id not in self.instrument_pool:
PrintLog(LogLevel.ERROR, f"交易标的 ID {id} 不存在")
return
target: strategy_db.TradeTarget = self.instrument_pool[id]
# 如果存在交易控制器,先停止交易
if target.stock_code in self.stock_trade_ctrl:
# 停止交易控制器
del self.stock_trade_ctrl[target.stock_code]
# 从数据库中删除
target.delete_instance()
# 从内存中删除
del self.instrument_pool[id]
# 刷新标的池
self.refresh_targets()
PrintLog(LogLevel.INFO, f"已删除交易标的: {target.stock_code} - {target.stock_name}")
except Exception as e:
PrintLog(LogLevel.ERROR, f"删除交易标的失败 ID {id}: {str(e)}")
def init_instrument_pool(self, xtTrader:XtQuantTrader, account:StockAccount):
self.refresh_targets()
for id in self.instrument_pool:
target:TradeTarget = self.instrument_pool[id]
status = "新建" if target.status == 0 else "已建初始仓"
PrintLog(LogLevel.INFO, f' [序号-{id}] 股票代码: {target.stock_code}-{target.stock_name} 当前持仓: {getStockPosition(target.stock_code, self.xt_trader, self.account)} 网格索引: {target.grid_index} 基准价格 {sfgrid_constants.grid_price[target.grid_index]} 状态: {status} 启用交易线程: {'自动交易中' if target.enabled else '交易已停止'}') # type: ignore
tradeTarget:strategy_db.TradeTarget = self.instrument_pool[id]
tradeTarget.current_position = getStockPosition(tradeTarget.stock_code, xtTrader, account) # type: ignore
result = tradeTarget.save()
PrintLog(LogLevel.INFO, f' |- 同步[{target.stock_code}-{target.stock_name}]持仓信息[{'成功' if result == 1 else '失败'}]')
stockTradeController = SFGridStrategy(tradeTarget, self.xt_trader, self.account) # type: ignore
self.stock_trade_ctrl[tradeTarget.stock_code] = stockTradeController
event_bus.publish(EventTradeTargetUpdate, tradeTarget)
PrintLog(LogLevel.INFO, f'- [成功]交易标的信息初始化, 共 {len(self.instrument_pool)} 个标的')
def refresh_targets(self):
# 更新标的池
results:ModelSelect = strategy_db.TradeTarget.select()
self.instrument_pool: dict[int, strategy_db.TradeTarget] = {}
for temp in results:
result :strategy_db.TradeTarget = temp
self.instrument_pool[result.get_id()] = result
def print_position_info(self):
positions:list[XtPosition] = self.xt_trader.query_stock_positions(self.account)
if positions:
PrintLog(LogLevel.INFO, "\n- 持仓信息")
for temp in positions:
pos : XtPosition = temp
if pos.volume <=0:
continue
PrintLog(LogLevel.INFO, f"股票代码: {pos.stock_code}-{getInstrumentName(pos.stock_code)}")
PrintLog(LogLevel.INFO, f"总持仓: {pos.volume}")
PrintLog(LogLevel.INFO, f"可用持仓: {pos.can_use_volume}")
PrintLog(LogLevel.INFO, f"持仓成本: {pos.avg_price}")
PrintLog(LogLevel.INFO, "---")
else:
PrintLog(LogLevel.INFO, "\n当前无持仓")
def print_account_info(self):
temp = self.xt_trader.query_stock_asset(self.account)
asset: XtAsset = temp # type: ignore
PrintLog(LogLevel.INFO, f"=== 账户信息 {self.account.account_id} ===") # type: ignore
PrintLog(LogLevel.INFO, f"可用资金: {asset.cash}")
PrintLog(LogLevel.INFO, f"总资产: {asset.total_asset}")
PrintLog(LogLevel.INFO, f"证券市值: {asset.market_value}")
def print_stock_orders(self):
orders = self.xt_trader.query_stock_orders(self.account, cancelable_only=True)
if orders:
PrintLog(LogLevel.INFO, "\n=== 委托信息 ===")
for order in orders:
PrintLog(LogLevel.INFO, f"委托编号: {order.order_id}")
PrintLog(LogLevel.INFO, f"股票代码: {order.stock_code} {getInstrumentName(order.stock_code)}")
PrintLog(LogLevel.INFO, f"委托方向: {order.offset_flag} ")
PrintLog(LogLevel.INFO, f"委托价格: {order.price}")
PrintLog(LogLevel.INFO, f"委托数量: {order.order_volume}")
PrintLog(LogLevel.INFO, f"已成交数量: {order.traded_volume}")
PrintLog(LogLevel.INFO, f"委托状态: {order.order_status} ")
PrintLog(LogLevel.INFO, "---")
else:
PrintLog(LogLevel.INFO, "\n当前无委托记录")
# 初始化指定标的交易控制器
def start_stock_trade(self, id: int):
tradeTarget: TradeTarget = self.instrument_pool[id]
# check existing thread
if tradeTarget.stock_code in self.stock_trade_ctrl:
tradeController: SFGridStrategy = self.stock_trade_ctrl[tradeTarget.stock_code]
tradeTarget = tradeController.enabledTrading(True)
self.instrument_pool[id] = tradeTarget
event_bus.publish(ResultEventTradeEnabled, tradeTarget)
else:
PrintLog(LogLevel.INFO, f"\t创建标的交易控制器 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}")
def pause_stock_trade(self, id: int):
localTarget: strategy_db.TradeTarget = self.instrument_pool[id]
print(f'暂停标的交易 {localTarget.stock_code} - enabled {localTarget.enabled}')
if localTarget.stock_code in self.stock_trade_ctrl:
tradeController: SFGridStrategy = self.stock_trade_ctrl[localTarget.stock_code]
tradeTarget = tradeController.enabledTrading(False)
orders = queryPendingOrder(localTarget.stock_code, tradeController.getName(), self.xt_trader, self.account) # type: ignore
for order in orders:
self.xt_trader.cancel_order_stock_async(self.account, order.order_id)
print(f'取消未成交订单 {len(orders)}')
self.instrument_pool[id] = tradeTarget
event_bus.publish(ResultEventTradeDisabled, tradeTarget)
else:
print(f"标的交易控制器不存在 {localTarget.stock_code} {localTarget.stock_name}\n")
# ====== 市场回调方法 -- 以下方法由XtQuantData调用 ======
def onDataUpdate(self, data):
# 收集所有市场数据用于市场监控
for stock_code, tickData in data.items():
if stock_code in self.stock_trade_ctrl:
stock_controller: SFGridStrategy = self.stock_trade_ctrl[stock_code]
stock_controller.onDataUpdate(data)
else:
# 非目标交易,发布市场数据更新事件用于市场监控
lastPrice = tickData['lastPrice']
if lastPrice == 10 or stock_code in self.listening_stock:
# 发布市场数据更新事件用于市场监控
market_target = TradeTarget()
market_target.stock_code = stock_code
market_target.stock_name = getInstrumentName(stock_code) # type: ignore
market_target.market_price = lastPrice # type: ignore
event_bus.publish(MarketDataUpdate, market_target)
if stock_code not in self.listening_stock:
self.listening_stock.append(stock_code)
# ====== 市场回调方法 -- 以下方法由XtQuantTrader调用 ======
def on_connected(self):
"""
连接成功推送
"""
print(datetime.datetime.now(), '连接成功回调')
def on_disconnected(self):
"""
连接断开
:return:
"""
print(datetime.datetime.now(), '连接断开回调')
def on_stock_order(self, order:XtOrder):
"""
委托回报推送
:param order: XtOrder对象
:return:
"""
print(f'orderd {order.strategy_name}-{order.stock_code} {order.order_id} {order.order_volume}-{order.order_status}')
stockCode = order.stock_code
ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# 如果存在对应的StockTradeController,则调用其onDataUpdate方法
if ctrl is not None and order.strategy_name == ctrl.getName():
print(f'controller info {ctrl.getName()}')
ctrl.onAsyncOrderResponse(order) # type: ignore
else:
print(f"委托下单回调 投资备注 orderId: {order.order_sysid} [{order.stock_code}-{order.instrument_name}] volume: {order.order_volume} 订单策略: '{order.strategy_name}'<-->'{ctrl.getName()}'")
def on_stock_trade(self, trade:XtTrade):
"""
成交变动推送
:param trade: XtTrade对象
:return:
"""
stockCode = trade.stock_code
ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# 如果存在对应的StockTradeController,则调用其onDataUpdate方法
if ctrl is not None and trade.strategy_name == ctrl.getName():
ctrl.onOrderTrade(trade)
else:
print(f"委托回调 投资备注 {trade.strategy_name} 不匹配 {ctrl.getName()}")
# def on_order_stock_async_response(self, response:XtOrderResponse):
# stockCode = response.order_remark
# ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# # 如果存在对应的StockTradeController,则调用其onDataUpdate方法
# if ctrl is not None and response.strategy_name == ctrl.getName():
# ctrl.onAsyncOrderResponse(response)
# else:
# print(f"委托回调 投资备注 {response.strategy_name} 不匹配 {ctrl.getName()}")
def on_order_error(self, order_error):
"""
委托失败推送
:param order_error:XtOrderError 对象
:return:
"""
# print("on order_error callback")
# print(order_error.order_id, order_error.error_id, order_error.error_msg)
print(f"\n委托报错回调 {order_error.order_remark} {order_error.error_msg}")
def on_account_status(self, status):
"""
:param response: XtAccountStatus 对象
:return:
"""
print(datetime.datetime.now(), status)
+213
View File
@@ -0,0 +1,213 @@
import tkinter as tk
from tkinter import ttk
from core.logger import LogLevel, PrintLog
from core.sfgrid.sfgrid_ui import TradeTargetUI
class MainWindow:
def __init__(self):
self.root = tk.Tk()
self.root.title("神之一手 - 交易系统")
self.root.geometry("1400x700")
# 当前选中的策略Tab索引
self.current_strategy_index = 0
# 存储各个Frame的引用
self.strategy_frames = {}
# 日志面板可见性标志
self.log_visible = False
# 创建界面
self.create_ui()
def create_ui(self):
"""创建UI界面"""
# 主容器
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))
# 创建Tab按钮(垂直排列,文字垂直显示)
self.tab_buttons = []
strategy_names = ["蒙派\n策略", "涨停\n分析"]
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
)
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
self.create_strategy_frames(strategy_names)
# 创建全局日志面板(默认隐藏)
self.create_global_log_panel(main_container)
# 默认显示第一个策略
self.switch_strategy_tab(0)
def create_global_log_panel(self, parent):
"""创建全局日志面板"""
# 日志区域(默认隐藏)
self.log_frame = ttk.LabelFrame(parent, text="操作日志", padding=10)
# 默认不显示,通过工具栏按钮控制
# 创建日志表格
columns = ("timestamp", "level", "message")
self.log_table = ttk.Treeview(self.log_frame, columns=columns, show='headings', height=8)
log_column_configs = {
"timestamp": ("时间", 100),
"level": ("级别", 50),
"message": ("消息", 1150) # 调整宽度适应全局布局
}
for col in columns:
title, width = log_column_configs[col]
self.log_table.heading(col, text=title)
self.log_table.column(col, width=width, anchor=tk.W)
# 添加初始日志
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_table.insert('', tk.END, values=(timestamp, "INFO", "系统启动成功"))
# 滚动条
scrollbar = ttk.Scrollbar(self.log_frame, orient=tk.VERTICAL, command=self.log_table.yview)
self.log_table.configure(yscrollcommand=scrollbar.set)
self.log_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
def add_log(self, level:LogLevel, message):
"""添加日志记录 - 全局方法"""
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_table.insert('', 0, values=(timestamp, level.value, message))
def clear_logs(self):
"""清空日志记录"""
# 删除所有日志项
for item in self.log_table.get_children():
self.log_table.delete(item)
self.add_log(LogLevel.DEBUG, "日志已清空")
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
# 添加占位内容
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
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按钮的样式以显示选中状态"""
# 注意:ttk.Button的样式需要通过ttk.Style来设置
# 这里简化处理,仅作为接口预留
pass
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):
"""退出程序"""
from tkinter import messagebox
result = messagebox.askyesno("确认退出", "确定要退出系统吗?")
if result:
self.root.destroy()
def run(self):
"""运行程序"""
self.root.mainloop()
+220
View File
@@ -0,0 +1,220 @@
import datetime
import time
import config
from xtquant.xttype import StockAccount, XtOrder, XtOrderResponse, XtPosition, XtTrade
from xtquant.xttrader import XtQuantTrader
from xtquant.xttype import StockAccount
from core.logger import LogLevel, PrintLog
from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount
from xtquant import xtconstant, xtdata
import core.eventbus as eBus
class QmtV(XtQuantTraderCallback):
def __init__(self) -> None:
self.xttrader: XtQuantTrader
self.inited: bool = False
def getTrader(self) -> XtQuantTrader:
return self.xttrader
def init_qmtv(self):
sessionId= int(time.time())
self.xttrader = XtQuantTrader(config.miniQMTPath, sessionId)
xtdata.enable_hello = False
def connect(self):
self.xttrader.register_callback(self)
self.xttrader.start()
self.xttrader.connect()
PrintLog(LogLevel.INFO, f'- [{'成功' if self.xttrader.connected else '失败'}]市场交易连接: {config.miniQMTPath}')
if self.xttrader.connected == False:
self.inited = False
return
else:
self.inited = True
self.account = StockAccount(config.account_no, 'STOCK') # pyright: ignore[reportAssignmentType, reportAttributeAccessIssue]
PrintLog(LogLevel.INFO, f'- [成功]交易账号对象初始化完成, 账号: {config.account_no}') # pyright: ignore[reportOptionalMemberAccess]
subscribe_result = self.xttrader.subscribe(self.account)
PrintLog(LogLevel.INFO, f'- [{'成功' if subscribe_result == 0 else '失败'}:{subscribe_result}]交易状态订阅')
if subscribe_result != 0:
self.inited = False
return
self.startMarketDataSubscription()
def getStockPosition(self, stock_code: str):
volume = 0
print(f'获取股票持仓: {stock_code}, {self.xttrader.connected}, {self.account.account_id if self.account else None}') # pyright: ignore[reportAttributeAccessIssue]
positions = self.xttrader.query_stock_positions(self.account)
if positions:
for temp in positions:
pos:XtPosition = temp
if pos.stock_code == stock_code:
volume = pos.volume
break
return volume
def queryPendingOrder(self, stock_code:str, tag: str) -> list[XtOrder]:
if stock_code == None or tag == None:
return []
orders = self.xttrader.query_stock_orders(self.account)
result = [order for order in orders if order.order_status == xtconstant.ORDER_REPORTED and order.stock_code == stock_code and order.strategy_name == tag]
return result
def orderAsync(self, stock_code, orderVolume, orderType, orderPrice, priceType, orderRemark, strategy_name):
return self.xttrader.order_stock_async(
self.account,
str(stock_code),
orderType,
orderVolume,
priceType,
orderPrice,
strategy_name, # strategy_name
orderRemark # remark # type: ignore
)
def getInstrumentName(self, stock_code:str):
# print(f"getInstrumentName: 获取标的名称 {stock_code}")
detail = xtdata.get_instrument_detail(stock_code, False)
if detail is None:
return None
return detail['InstrumentName']
# def print_position_info(self):
# positions:list[XtPosition] = self.xt_trader.query_stock_positions(self.account)
# if positions:
# PrintLog(LogLevel.INFO, "\n- 持仓信息")
# for temp in positions:
# pos : XtPosition = temp
# if pos.volume <=0:
# continue
# PrintLog(LogLevel.INFO, f"股票代码: {pos.stock_code}-{getInstrumentName(pos.stock_code)}")
# PrintLog(LogLevel.INFO, f"总持仓: {pos.volume}")
# PrintLog(LogLevel.INFO, f"可用持仓: {pos.can_use_volume}")
# PrintLog(LogLevel.INFO, f"持仓成本: {pos.avg_price}")
# PrintLog(LogLevel.INFO, "---")
# else:
# PrintLog(LogLevel.INFO, "\n当前无持仓")
# def print_account_info(self):
# temp = self.xt_trader.query_stock_asset(self.account)
# asset: XtAsset = temp # type: ignore
# PrintLog(LogLevel.INFO, f"=== 账户信息 {self.account.account_id} ===") # type: ignore
# PrintLog(LogLevel.INFO, f"可用资金: {asset.cash}")
# PrintLog(LogLevel.INFO, f"总资产: {asset.total_asset}")
# PrintLog(LogLevel.INFO, f"证券市值: {asset.market_value}")
# def print_stock_orders(self):
# orders = self.xt_trader.query_stock_orders(self.account, cancelable_only=True)
# if orders:
# PrintLog(LogLevel.INFO, "\n=== 委托信息 ===")
# for order in orders:
# PrintLog(LogLevel.INFO, f"委托编号: {order.order_id}")
# PrintLog(LogLevel.INFO, f"股票代码: {order.stock_code} {getInstrumentName(order.stock_code)}")
# PrintLog(LogLevel.INFO, f"委托方向: {order.offset_flag} ")
# PrintLog(LogLevel.INFO, f"委托价格: {order.price}")
# PrintLog(LogLevel.INFO, f"委托数量: {order.order_volume}")
# PrintLog(LogLevel.INFO, f"已成交数量: {order.traded_volume}")
# PrintLog(LogLevel.INFO, f"委托状态: {order.order_status} ")
# PrintLog(LogLevel.INFO, "---")
# else:
# PrintLog(LogLevel.INFO, "\n当前无委托记录")
# ========================================#
def startMarketDataSubscription(self):
self.subscriptionId = xtdata.subscribe_whole_quote(['SH', 'SZ'], self.onDataUpdate)
def stopMarketDataSubscription(self):
PrintLog(LogLevel.INFO, '- 停止市场数据订阅')
if self.subscriptionId is not None and self.subscriptionId > 0:
xtdata.unsubscribe_quote(self.subscriptionId)
# ====== 市场回调方法 -- 以下方法由XtQuantData调用 ======
def onDataUpdate(self, data):
# 收集所有市场数据用于市场监控
eBus.event_bus.publish(eBus.MarketDataUpdate, data)
# ====== 市场回调方法 -- 以下方法由XtQuantTrader调用 ======
def on_connected(self):
"""
连接成功推送
"""
print(datetime.datetime.now(), '连接成功回调')
def on_disconnected(self):
"""
连接断开
:return:
"""
print(datetime.datetime.now(), '连接断开回调')
def on_stock_order(self, order:XtOrder):
"""
委托回报推送
:param order: XtOrder对象
:return:
"""
print(f'orderd {order.strategy_name}-{order.stock_code} {order.order_id} {order.order_volume}-{order.order_status}')
# stockCode = order.stock_code
# ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# # 如果存在对应的StockTradeController,则调用其onDataUpdate方法
# if ctrl is not None and order.strategy_name == ctrl.getName():
# print(f'controller info {ctrl.getName()}')
# ctrl.onAsyncOrderResponse(order) # type: ignore
# else:
# print(f"委托下单回调 投资备注 orderId: {order.order_sysid} [{order.stock_code}-{order.instrument_name}] volume: {order.order_volume} 订单策略: '{order.strategy_name}'<-->'{ctrl.getName()}'")
def on_stock_trade(self, trade:XtTrade):
"""
成交变动推送
:param trade: XtTrade对象
:return:
"""
print(f"委托回调 投资备注 {trade.stock_code}-{trade.instrument_name} {trade.strategy_name} 不匹配 {trade.order_remark}")
# stockCode = trade.stock_code
# ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# # 如果存在对应的StockTradeController,则调用其onDataUpdate方法
# if ctrl is not None and trade.strategy_name == ctrl.getName():
# ctrl.onOrderTrade(trade)
# else:
# print(f"委托回调 投资备注 {trade.strategy_name} 不匹配 {ctrl.getName()}")
def on_order_stock_async_response(self, response:XtOrderResponse):
print(f"委托回调 投资备注 {response.error_msg}{response.strategy_name} {response.order_remark}")
# stockCode = response.order_remark
# ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# # 如果存在对应的StockTradeController,则调用其onDataUpdate方法
# if ctrl is not None and response.strategy_name == ctrl.getName():
# ctrl.onAsyncOrderResponse(response)
# else:
# print(f"委托回调 投资备注 {response.strategy_name} 不匹配 {ctrl.getName()}")
def on_order_error(self, order_error):
"""
委托失败推送
:param order_error:XtOrderError 对象
:return:
"""
print(f"\n委托报错回调 {order_error.order_remark} {order_error.error_msg}")
def on_account_status(self, status):
"""
:param response: XtAccountStatus 对象
:return:
"""
print(datetime.datetime.now(), status)
qmtv = QmtV()
+14
View File
@@ -0,0 +1,14 @@
# 删除交易标的事件
EventTradeTargetUpdate = "trade_target_update"
ActionEventAddTradeTarget = "add_trade_target"
ResultEventTradeTargetAdded = "trade_target_added"
ActionEventDeleteTradeTarget = "delete_trade_target"
ResultEventTradeTargetDeleted = "trade_target_deleted"
# 网格修正事件
ActionEventGridFix = "grid_fix"
# 定义事件处理函数
ActionEventEnableTrade = "enable_trade"
ResultEventTradeEnabled = "trade_enabled"
ActionEventDisableTrade = "disable_trade"
ResultEventTradeDisabled = "trade_disabled"
+6 -15
View File
@@ -1,22 +1,10 @@
from peewee import SqliteDatabase, Model, CharField, IntegerField, FloatField, BooleanField
from peewee import CharField, IntegerField, FloatField, BooleanField
from xtquant import xtconstant
from core.database import BaseModel, db
# 连接到SQLite数据库
db = SqliteDatabase('example.db')
# 定义基础模型类
class BaseModel(Model):
class Meta:
database = db
OrderTypeBuy = f'{xtconstant.STOCK_BUY}' # 买
OrderTypeSell = f'{xtconstant.STOCK_SELL}' # 卖
OrderTypeInit = "0" # 建仓
OrderTypeNone = "None"
# 定义Target类,对应targets表
class TradeTarget(BaseModel):
class SFGridTradeTarget(BaseModel):
stock_code = CharField(unique=True)
stock_name = CharField()
market_price = FloatField()
@@ -33,3 +21,6 @@ class TradeTarget(BaseModel):
def targetName(self):
return f'{self.stock_name}[{self.stock_code}]'
db.create_tables([SFGridTradeTarget])
+1 -1
View File
@@ -1,4 +1,4 @@
from core.strategy_db import TradeTarget
from core.sfgrid.model import SFGridTradeTarget as TradeTarget
class GridFixData:
@@ -1,20 +1,21 @@
from core import strategy_db
from core.eventbus import EventTradeTargetUpdate, event_bus
from core.strategy_db import OrderTypeBuy, OrderTypeInit, OrderTypeSell, TradeTarget
from core.qmt import qmtv
from core.sfgrid.bus_events import EventTradeTargetUpdate
import core.sfgrid.model as model
from core import constants
from core.eventbus import event_bus
from core.constants import OrderTypeBuy, OrderTypeInit, OrderTypeSell
from core.util import queryPendingOrder, is_trading_time
from xtquant import xttrader, xtconstant
from xtquant.xttype import StockAccount, XtOrder, XtTrade
import sfgrid_constants
from xtquant import xtconstant
from xtquant.xttype import XtOrder, XtTrade
import config
import threading
class SFGridStrategy:
def __init__(self, tradeTarget: TradeTarget, xt_trader: xttrader.XtQuantTrader, account: StockAccount):
self.tradeTarget:TradeTarget = tradeTarget
self.xt_trader: xttrader.XtQuantTrader = xt_trader
self.account:StockAccount = account
def __init__(self, tradeTarget: model.SFGridTradeTarget):
self.tradeTarget:model.SFGridTradeTarget = tradeTarget
self.enabledTrading(bool(tradeTarget.enabled)) # 修复类型兼容性问题
event_bus.publish(EventTradeTargetUpdate, self.tradeTarget)
@@ -26,7 +27,7 @@ class SFGridStrategy:
self.refreshPlanPrice()
self.saveProxy()
def enabledTrading(self, enabled: bool) -> TradeTarget:
def enabledTrading(self, enabled: bool) -> model.SFGridTradeTarget:
self.tradeTarget.enabled = enabled # type: ignore
self.saveProxy()
@@ -39,7 +40,7 @@ class SFGridStrategy:
else: # 已建仓
# 交易阶段,检查仓位,检查现有订单
print(f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 无需建初始仓 当前仓位: {self.tradeTarget.current_position} 状态: {self.tradeTarget.status}")
minRequirePosition:int = sfgrid_constants.grid_volume * int(self.tradeTarget.grid_index) # type: ignore
minRequirePosition:int = config.grid_volume * int(self.tradeTarget.grid_index) # type: ignore
if minRequirePosition <= int(self.tradeTarget.current_position): # type: ignore
print(f' |- 仓位检查: 持仓需求充足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}')
else:
@@ -54,40 +55,35 @@ class SFGridStrategy:
print(f'|- 检查交易状态[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - {self.tradeTarget.enabled}')
return bool(self.tradeTarget.enabled) # 修复返回类型问题
def onDataUpdate(self, data):
print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - START')
lastPrice = float("{:.3f}".format(data[self.tradeTarget.stock_code]['lastPrice']))
self.tradeTarget.market_price = lastPrice # type: ignore
self.saveProxy()
def onDataUpdate(self, inTradeTarget:model.SFGridTradeTarget):
if not is_trading_time():
print(f"|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - 非交易时间,不进行自动交易")
return
if not self.tradeTarget.enabled: # 未建仓,自动交易暂停
print(f"|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - 未建仓或交易监控暂停,不进行自动交易")
return
print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - START')
self.dataUpdateLock.acquire()
print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - LOCKED')
try:
lastPrice = inTradeTarget.market_price
orderPrice:float = -1
orderType = -1
index: int = self.tradeTarget.grid_index # pyright: ignore[reportAssignmentType]
orderRemark= ""
gridBasePrice = -1 if index>=len(sfgrid_constants.grid_price) or index < 0 else sfgrid_constants.grid_price[int(index)] # pyright: ignore[reportArgumentType]
gridBasePrice = -1 if index>=len(config.grid_price) or index < 0 else config.grid_price[int(index)] # pyright: ignore[reportArgumentType]
if self.tradeTarget.enabled and self.tradeTarget.status == 0 and lastPrice <= sfgrid_constants.grid_price[1]: # 已启用,未建仓,建仓
orderPrice = sfgrid_constants.grid_price[index]
if self.tradeTarget.enabled and self.tradeTarget.status == 0 and lastPrice <= config.grid_price[1]: # 已启用,未建仓,建仓
orderPrice = config.grid_price[index]
orderType = xtconstant.STOCK_BUY
orderRemark = OrderTypeInit
if self.tradeTarget.enabled and self.tradeTarget.status == 1: # 已启用,已建仓,网格单
lowPrice = -1 if index+1>=len(sfgrid_constants.grid_price) else sfgrid_constants.grid_price[int(index) + 1] # pyright: ignore[reportArgumentType]
highPrice = sfgrid_constants.grid_price[index - 1]
lowPrice = -1 if index+1>=len(config.grid_price) else config.grid_price[int(index) + 1] # pyright: ignore[reportArgumentType]
highPrice = config.grid_price[index - 1]
if lastPrice <= lowPrice: # 下下方多单
orderPrice = lowPrice
@@ -100,21 +96,20 @@ class SFGridStrategy:
orderRemark = OrderTypeSell
if orderType != -1:
orders = queryPendingOrder(str(self.tradeTarget.stock_code), self.getName(), self.xt_trader, self.account)
orders = qmtv.queryPendingOrder(str(self.tradeTarget.stock_code), self.getName())
if len([order for order in orders if order.order_type == orderType and order.price == orderPrice]) > 0:
# 已存在未交易的多单
print(f' |- [{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}]已存在未交易的{"多单" if orderType == xtconstant.STOCK_BUY else "空单"},不重复下单')
else:
print(f' |- 下网格{"多单" if orderType == xtconstant.STOCK_BUY else "空单"}')
self.tradeTarget.current_order_no = self.xt_trader.order_stock_async(
self.account,
self.tradeTarget.current_order_no = qmtv.orderAsync(
str(self.tradeTarget.stock_code),
config.grid_volume,
orderType,
sfgrid_constants.grid_volume,
xtconstant.FIX_PRICE,
orderPrice,
xtconstant.FIX_PRICE,
orderRemark, # remark # type: ignore
self.getName(), # strategy_name
orderRemark # remark # type: ignore
)
orderTypeName = ""
if orderRemark == OrderTypeBuy:
@@ -123,7 +118,7 @@ class SFGridStrategy:
orderTypeName = "空单"
elif orderRemark == OrderTypeInit:
orderTypeName = "建仓单"
print(f' |- {orderTypeName}委托, 单号 {self.tradeTarget.current_order_no}, 网格基准价 {gridBasePrice}, 下单价 {orderPrice}, 下单量 {sfgrid_constants.grid_volume}')
print(f' |- {orderTypeName}委托, 单号 {self.tradeTarget.current_order_no}, 网格基准价 {gridBasePrice}, 下单价 {orderPrice}, 下单量 {config.grid_volume}')
finally:
print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - release lock')
self.saveProxy()
@@ -156,13 +151,13 @@ class SFGridStrategy:
if not trade.strategy_name == self.getName():
print(f' |- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}]: 不在策略监控范围内{trade.strategy_name}')
return
if self.tradeTarget.status == 0 and trade.order_id == self.tradeTarget.current_order_no and trade.order_remark == strategy_db.OrderTypeInit:
if self.tradeTarget.status == 0 and trade.order_id == self.tradeTarget.current_order_no and trade.order_remark == constants.OrderTypeInit:
# 此时为建仓成交
self.tradeTarget.current_position = int(self.tradeTarget.current_position) + trade.traded_volume # 当前持仓数,账户原有持仓不在策略范围内 # type: ignore
self.tradeTarget.last_trade_price = float(trade.traded_price) # type: ignore
self.tradeTarget.grid_index = 1 # type: ignore
self.tradeTarget.plan_buy_price = float(sfgrid_constants.grid_price[2]) # type: ignore
self.tradeTarget.plan_sell_price = float(sfgrid_constants.grid_price[0]) # type: ignore
self.tradeTarget.plan_buy_price = float(config.grid_price[2]) # type: ignore
self.tradeTarget.plan_sell_price = float(config.grid_price[0]) # type: ignore
self.tradeTarget.status = 1 # type: ignore
self.saveProxy()
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 建初始仓订单ID: {self.tradeTarget.current_order_no}已成交 ")
@@ -174,7 +169,7 @@ class SFGridStrategy:
self.tradeTarget.current_position = int(self.tradeTarget.current_position) - trade.traded_volume # type: ignore
self.tradeTarget.last_trade_price = float(trade.traded_price) # type: ignore
self.tradeTarget.grid_index = int(self.tradeTarget.grid_index) - 1 # type: ignore
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 上涨 卖单已成交 订单ID: {self.tradeTarget.current_order_no} Price: {sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)]} Volume: {sfgrid_constants.grid_volume} 手续费: {trade.commission}\n") # type: ignore
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 上涨 卖单已成交 订单ID: {self.tradeTarget.current_order_no} Price: {config.grid_price[int(self.tradeTarget.grid_index)]} Volume: {config.grid_volume} 手续费: {trade.commission}\n") # type: ignore
print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
print(f' 当前持仓: {self.tradeTarget.current_position}')
print(f' 网格坐标: {self.tradeTarget.grid_index}')
@@ -183,7 +178,7 @@ class SFGridStrategy:
self.tradeTarget.current_position = int(self.tradeTarget.current_position) + trade.traded_volume # type: ignore
self.tradeTarget.last_trade_price = float(trade.traded_price) # type: ignore
self.tradeTarget.grid_index = int(self.tradeTarget.grid_index) + 1 # type: ignore
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 下跌 买单已成交 订单ID: {self.tradeTarget.current_order_no} Price: {trade.traded_price} Volume: {sfgrid_constants.grid_volume} 手续费: {trade.commission}")
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 下跌 买单已成交 订单ID: {self.tradeTarget.current_order_no} Price: {trade.traded_price} Volume: {config.grid_volume} 手续费: {trade.commission}")
print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
print(f' 当前持仓: {self.tradeTarget.current_position}')
print(f' 网格坐标: {self.tradeTarget.grid_index}')
@@ -201,11 +196,11 @@ class SFGridStrategy:
buyIdx: int = self.tradeTarget.grid_index + 1 # pyright: ignore[reportAssignmentType]
sellIdx: int = self.tradeTarget.grid_index - 1
if self.tradeTarget.grid_index > 0: # 可以下空单
self.tradeTarget.plan_sell_price = float(sfgrid_constants.grid_price[sellIdx]) # pyright: ignore[reportAttributeAccessIssue]
self.tradeTarget.plan_sell_price = float(config.grid_price[sellIdx]) # pyright: ignore[reportAttributeAccessIssue]
else:
self.tradeTarget.plan_sell_price = -1.0 # type: ignore
if self.tradeTarget.grid_index < len(sfgrid_constants.grid_price) - 1:
self.tradeTarget.plan_buy_price = float(sfgrid_constants.grid_price[buyIdx]) # pyright: ignore[reportAttributeAccessIssue]
if self.tradeTarget.grid_index < len(config.grid_price) - 1:
self.tradeTarget.plan_buy_price = float(config.grid_price[buyIdx]) # pyright: ignore[reportAttributeAccessIssue]
else:
self.tradeTarget.plan_buy_price = -1.0 # pyright: ignore[reportAttributeAccessIssue]
else:
+312 -301
View File
@@ -1,26 +1,30 @@
from typing import Any
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from datetime import datetime
import threading
import time
import core.eventbus as eBus
from core.logger import LogData, LogLevel
from core.strategy_db import TradeTarget
from core.logger import LogLevel, PrintLog
from core.sfgrid.bus_events import ActionEventAddTradeTarget, ActionEventDeleteTradeTarget, ActionEventDisableTrade, ActionEventEnableTrade, ActionEventGridFix, EventTradeTargetUpdate, ResultEventTradeTargetAdded, ResultEventTradeTargetDeleted
from core.sfgrid.model import SFGridTradeTarget
import configparser
import sfgrid_constants
from core.objects import GridFixData
from core.util import getInstrumentName
import config
from core.sfgrid.objects import GridFixData
from core.qmt import qmtv
from core.sfgrid.sfgrid_strategy import SFGridStrategy
class TradeTargetUI:
def __init__(self):
self.tradeTargetData:dict[int, TradeTarget] = {}
self.market_data_enabled = False # 添加市场数据监听状态变量
self.ui_refresh_enabled = False # 添加UI刷新线程状态变量
self.registerEventHandler()
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.listening_stock = []
self.init_trade_target_pool()
eBus.event_bus.subscribe(eBus.MarketDataUpdate, self.onMarketDataUpdated)
# 创建刷新线程标志
self.refresh_thread_running = False # 默认不启动刷新线程
@@ -28,23 +32,68 @@ class TradeTargetUI:
# 市场监控数据
self.marketData: dict[str, Any] = {} # 存储市场数据 {stock_code: {stock_name, last_price, time}}
self.root = tk.Tk()
self.root.title("三疯交易系统")
self.root.geometry("1400x700")
# 市场监控窗口显示状态
self.market_monitor_visible = True
# 创建界面
self.create_ui()
# 不再自动启动刷新线程,由市场数据开关控制
self.start_ui_refresh()
def registerEventHandler(self):
eBus.event_bus.subscribe(eBus.EventTradeTargetUpdate, self.onTradeTargetUpdated)
eBus.event_bus.subscribe(eBus.MarketDataUpdate, self.onMarketDataUpdated)
eBus.event_bus.subscribe(eBus.ResultEventTradeEnabled, self.onTradeEnabled)
eBus.event_bus.subscribe(eBus.ResultEventTradeDisabled, self.onTradeDisabled)
eBus.event_bus.subscribe(eBus.MarketDataEnabled, self.onMarketDataToggled)
eBus.event_bus.subscribe(eBus.MarketDataDisabled, self.onMarketDataToggled)
eBus.event_bus.subscribe(eBus.ResultEventTradeTargetDeleted, self.onTradeTargetDeleted)
eBus.event_bus.subscribe(eBus.EventPrintLog, self.onLog)
def onMarketDataUpdated(self, data):
# 收集所有市场数据用于市场监控
for stock_code, tickData in data.items():
if stock_code in self.stockCodeIdMap:
id:int = self.stockCodeIdMap[stock_code]
tradeTarget = self.tradeTargetData[id]
lastPrice = float("{:.3f}".format(tickData['lastPrice']))
tradeTarget.market_price = lastPrice # type: ignore
self.updateTradeTarget(tradeTarget)
stock_controller: SFGridStrategy = self.strategy_ctrl[id]
stock_controller.onDataUpdate(tradeTarget)
else:
# 非目标交易,发布市场数据更新事件用于市场监控
lastPrice = tickData['lastPrice']
if lastPrice == 10 or stock_code in self.listening_stock:
# 发布市场数据更新事件用于市场监控
market_target = SFGridTradeTarget()
market_target.stock_code = stock_code
market_target.stock_name = qmtv.getInstrumentName(stock_code) # type: ignore
market_target.market_price = lastPrice # type: ignore
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 refresh_targets(self):
# 更新标的池
results = SFGridTradeTarget.select()
for temp in results:
result :SFGridTradeTarget = temp
self.tradeTargetData[result.get_id()] = result
self.stockCodeIdMap[str(result.stock_code)] = result.get_id()
def init_trade_target_pool(self):
self.refresh_targets()
for id, tradeTarget in self.tradeTargetData.items():
status = "新建" if tradeTarget.status == 0 else "已建初始仓"
PrintLog(LogLevel.INFO, f' [序号-{id}] 股票代码: {tradeTarget.stock_code}-{tradeTarget.stock_name} 当前持仓: {qmtv.getStockPosition(tradeTarget.stock_code)} 网格索引: {tradeTarget.grid_index} 基准价格 {config.grid_price[tradeTarget.grid_index]} 状态: {status} 启用交易线程: {'自动交易中' if tradeTarget.enabled else '交易已停止'}') # type: ignore
tradeTarget.current_position = qmtv.getStockPosition(tradeTarget.stock_code) # type: ignore
result = tradeTarget.save()
PrintLog(LogLevel.INFO, f' |- 同步[{tradeTarget.stock_code}-{tradeTarget.stock_name}]持仓信息[{'成功' if result == 1 else '失败'}]')
stockTradeController = SFGridStrategy(tradeTarget) # type: ignore
self.strategy_ctrl[id] = stockTradeController # pyright: ignore[reportArgumentType]
self.updateTradeTarget(tradeTarget)
PrintLog(LogLevel.INFO, f'- [成功]交易标的信息初始化, 共 {len(self.tradeTargetData)} 个标的')
def start_refresh_thread(self):
"""启动刷新线程"""
@@ -56,59 +105,30 @@ class TradeTargetUI:
"""刷新循环"""
while self.refresh_thread_running:
# 在主线程中更新UI
if hasattr(self, 'root') and self.root:
self.root.after(0, self.refresh_table)
time.sleep(0.5) # 每0.5秒刷新一次
self.after(0, self.refresh_table)
self.after(0, self.populate_market_table)
time.sleep(0.2) # 每0.5秒刷新一次
def stop_refresh_thread(self):
"""停止刷新线程"""
self.refresh_thread_running = False
def onTradeTargetDeleted(self, id: int):
"""处理交易标的删除完成事件"""
# 从本地数据中删除
if id in self.tradeTargetData:
del self.tradeTargetData[id]
# 添加日志
self.add_log(LogLevel.INFO, f"交易标的已删除,ID: {id}")
def onMarketDataToggled(self, data:bool):
self.market_data_enabled = self.market_data_switch_var.get()
self.add_log(LogLevel.INFO, "市场数据监听已" + ("启用" if data else "禁用"))
# 同步UI刷新线程状态
if data:
self.start_ui_refresh()
else:
self.stop_ui_refresh()
def onTradeEnabled(self, target:SFGridTradeTarget):
PrintLog(LogLevel.INFO, f"交易启用: {target.stock_code} - {target.stock_name}")
def onTradeEnabled(self, target:TradeTarget):
self.add_log(LogLevel.INFO, f"交易用: {target.stock_code} - {target.stock_name}")
def onTradeDisabled(self, target:SFGridTradeTarget):
PrintLog(LogLevel.INFO, f"交易用: {target.stock_code} - {target.stock_name}")
def onTradeDisabled(self, target:TradeTarget):
self.add_log(LogLevel.INFO, f"交易禁用: {target.stock_code} - {target.stock_name}")
def onTradeTargetUpdated(self, target: TradeTarget):
def updateTradeTarget(self, target: SFGridTradeTarget):
# 更新或添加数据到本地缓存
self.tradeTargetData[target.get_id()] = target
def onMarketDataUpdated(self, target: TradeTarget):
# 更新市场监控数据
current_time = datetime.now().strftime("%H:%M:%S")
self.marketData[str(target.stock_code)] = {
'stock_name': target.stock_name,
'last_price': target.market_price if target.market_price is not None else 0.0,
'time': current_time
}
self.stockCodeIdMap[target.stock_code] = target.get_id() # type: ignore
def create_ui(self):
"""创建UI界面"""
# 创建菜单栏
self.create_menu_bar()
# 主框架
main_frame = ttk.Frame(self.root)
# 主框架(使用self作为父容器)
main_frame = ttk.Frame(self)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建工具栏
@@ -127,77 +147,28 @@ class TradeTargetUI:
ttk.Button(toolbar_frame, text="🛠 网格修正",
command=self.grid_correction, width=12).pack(side=tk.LEFT, padx=2)
# 添加分隔符
ttk.Separator(toolbar_frame, orient='vertical').pack(side=tk.LEFT, fill=tk.Y, padx=10)
# 市场数据监听开关
self.market_data_switch_var = tk.BooleanVar(value=False)
# self.market_data_switch = ttk.Checkbutton(
# toolbar_frame,
# text="📊 市场数据",
# variable=self.market_data_switch_var,
# command=self.toggle_market_data,
# width=12
# )
# self.market_data_switch.pack(side=tk.LEFT, padx=2)
# 日志显示/隐藏按钮
self.log_toggle_btn = ttk.Button(toolbar_frame, text="📋 显示日志",
command=self.toggle_log_panel, width=12)
self.log_toggle_btn.pack(side=tk.LEFT, padx=2)
# 添加清空日志按钮
ttk.Button(toolbar_frame, text="🗑 清空日志",
command=self.clear_logs, width=12).pack(side=tk.LEFT, padx=2)
# 添加抽屉按钮到工具栏最右侧
self.toggle_market_monitor_btn = ttk.Button(toolbar_frame, text="◀▶",
command=self.toggle_market_monitor, width=3)
self.toggle_market_monitor_btn.pack(side=tk.RIGHT, padx=2)
# 表格区域
self.create_tables_area(main_frame)
def toggle_market_data(self):
"""切换市场数据监听状态"""
print(f'市场数据监听开关')
self.market_data_enabled = self.market_data_switch_var.get()
if self.market_data_enabled:
eBus.event_bus.publish(eBus.ActionEnableMarketData, True)
# 同步开启UI刷新线程
self.start_ui_refresh()
else:
eBus.event_bus.publish(eBus.ActionDisableMarketData, True)
# 同步关闭UI刷新线程
self.stop_ui_refresh()
def start_ui_refresh(self):
"""启动UI刷新线程"""
if not self.refresh_thread_running:
self.refresh_thread_running = True
self.start_refresh_thread()
self.add_log(LogLevel.INFO, "UI刷新线程已启动")
PrintLog(LogLevel.INFO, "UI刷新线程已启动")
def stop_ui_refresh(self):
"""停止UI刷新线程"""
if self.refresh_thread_running:
self.stop_refresh_thread()
self.refresh_thread_running = False
self.add_log(LogLevel.INFO, "UI刷新线程已停止")
PrintLog(LogLevel.INFO, "UI刷新线程已停止")
def create_menu_bar(self):
"""创建菜单栏"""
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
# 系统菜单
system_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="系统", menu=system_menu)
system_menu.add_command(label="系统设置", command=self.system_settings)
system_menu.add_separator()
system_menu.add_command(label="退出", command=self.on_exit)
def on_exit(self):
"""退出程序"""
# 停止刷新线程
self.stop_refresh_thread()
# 关闭窗口
self.root.destroy()
def create_tables_area(self, parent):
"""创建表格区域"""
@@ -213,28 +184,19 @@ class TradeTargetUI:
self.create_trade_target_table(trade_frame)
# 右侧市场监控区域
market_frame = ttk.LabelFrame(tables_frame, text="市场监控", padding=10)
market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
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(market_frame)
# 下方操作日志区域(默认隐藏)
self.log_frame = ttk.LabelFrame(parent, text="操作日志", padding=10)
# 默认不显示,通过工具栏按钮控制
# self.log_frame.pack(fill=tk.X, pady=(5, 0))
self.log_visible = False # 日志区域可见性标志
# 创建操作日志表格
self.create_log_table(self.log_frame)
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)
@@ -243,18 +205,16 @@ class TradeTargetUI:
column_configs = {
"ID": (50, tk.CENTER),
"股票代码": (90, tk.CENTER),
"股票名称": (80, tk.CENTER),
"市场价": (70, tk.CENTER),
"持仓数量": (80, tk.CENTER),
"网格索引": (80, tk.CENTER),
"最新成交": (90, tk.CENTER),
"计划买入": (90, tk.CENTER),
"计划卖出": (90, tk.CENTER),
"当前订单": (90, tk.CENTER),
"当前订单": (90, tk.CENTER),
"当前订单类型": (90, tk.CENTER),
"启用状态": (80, tk.CENTER),
"交易状态": (80, tk.CENTER)
"股票名称": (80, tk.E),
"市场价": (70, tk.E),
"持仓数量": (80, tk.E),
"最新成交价": (90, tk.E),
"计划买入": (90, tk.E),
"计划卖出": (90, tk.E),
"当前订单": (90, tk.E),
"当前订单": (90, tk.E),
"当前订单类型": (90, tk.E),
"交易状态": (80, tk.E)
}
for col in columns:
@@ -277,16 +237,15 @@ class TradeTargetUI:
def create_market_monitor_table(self, parent):
"""创建市场监控表格"""
columns = ("时间", "股票代码", "股票名称", "最新价格")
columns = ("时间", "股票名称", "最新价格")
self.market_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
# 列配置
column_configs = {
"时间": (120, "center"),
"股票代码": (90, "center"),
"股票名称": (80, "center"),
"最新价格": (80, "center")
"时间": (50, tk.CENTER),
"股票名称": (80, tk.CENTER),
"最新价格": (80, tk.CENTER)
}
for col in columns:
@@ -309,7 +268,14 @@ class TradeTargetUI:
def populate_market_table(self):
"""填充市场监控表格数据"""
pass
# 保存当前选中的项
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)
@@ -317,14 +283,36 @@ class TradeTargetUI:
# 填充市场数据
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 = [
data['time'],
stock_code,
data['stock_name'],
time_str,
data['stock_name']+f"{stock_code}",
f"{data['last_price']:.3f}"
]
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()
@@ -351,19 +339,7 @@ class TradeTargetUI:
if result:
# 发布事件通知主控制器添加标的
eBus.event_bus.publish(eBus.ActionEventAddTradeTarget, stock_code)
self.add_log(LogLevel.INFO, f"已发送添加请求: {stock_code} - {stock_name}")
def get_status_indicator(self, target: TradeTarget) -> str:
"""获取状态指示器(带颜色色块的文本)"""
if target.status == 1:
# 绿色圆点表示交易中
return "🟢 已建仓"
elif target.status == 0:
# 黄色圆点表示暂停
return "🟡 未建仓"
else:
return "🔴 错误状态"
self.onAddTradeTarget(stock_code)
def get_trade_enabled_indicator(self, enabled: bool) -> str:
"""获取交易状态指示器"""
@@ -374,58 +350,24 @@ class TradeTargetUI:
def populate_trade_table(self):
"""填充交易标的表格数据"""
for temp in self.tradeTargetData:
target: TradeTarget = self.tradeTargetData[temp]
for temp, target in self.tradeTargetData.items():
values = [
target.id, # type: ignore
target.stock_code,
target.stock_name,
"-" if target.market_price is None else f"{target.market_price:.3f}",
target.current_position,
target.grid_index,
'-' if target.last_trade_price is None else f"{target.last_trade_price:.3f}",
'-' if target.plan_buy_price is None else f"{target.plan_buy_price:.3f}",
'-' if target.plan_sell_price is None else f"{target.plan_sell_price:.3f}",
'-' if target.current_order_price is None else f"{target.current_order_price:.3f}",
target.current_order_no,
target.current_order_type,
self.get_status_indicator(target),
self.get_trade_enabled_indicator(target.enabled) # type: ignore
]
self.trade_table.insert('', tk.END, values=values)
def create_log_table(self, parent):
"""创建操作日志表格"""
columns = ("timestamp", "level", "message")
self.log_table = ttk.Treeview(parent, columns=columns, show='headings', height=8)
log_column_configs = {
"timestamp": ("时间", 100),
"level": ("级别", 50),
"message": ("消息", 850)
}
for col in columns:
title, width = log_column_configs[col]
self.log_table.heading(col, text=title)
self.log_table.column(col, width=width, anchor=tk.W)
# 填充示例日志
sample_logs = [
("2024-01-15 10:30:15", "INFO", "系统启动成功"),
]
for log in sample_logs:
self.log_table.insert('', tk.END, values=log)
# 滚动条
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.log_table.yview)
self.log_table.configure(yscrollcommand=scrollbar.set)
self.log_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
def get_status_text(self, status):
"""获取状态文本"""
@@ -441,7 +383,7 @@ class TradeTargetUI:
if selected:
item = selected[0]
values = self.trade_table.item(item)['values']
self.add_log(LogLevel.DEBUG, f"双击查看详情: {values[0]} - {values[1]}")
PrintLog(LogLevel.DEBUG, f"双击查看详情: {values[0]} - {values[1]}")
def get_selected_target(self):
"""获取选中的交易标的"""
@@ -480,13 +422,16 @@ class TradeTargetUI:
)
if result:
PrintLog(LogLevel.INFO, f'启动标的交易 {target.targetName()}')
target.enabled = True # type: ignore
eBus.event_bus.publish(eBus.ActionEventEnableTrade, target.get_id())
# self.add_log("INFO", f"已启动交易: {target.stock_code} - {target.stock_name}")
# messagebox.showinfo("启动成功", f"已启动 {target.stock_code} ({target.stock_name}) 的交易")
def on_trade_enabled(self, target: TradeTarget):
eBus.event_bus.publish(eBus.ActionEventEnableTrade, target)
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 pause_selected_trade(self):
"""暂停选中的交易"""
@@ -506,8 +451,18 @@ class TradeTargetUI:
)
if result:
target.enabled = False # type: ignore
eBus.event_bus.publish(eBus.ActionEventDisableTrade, target.get_id())
PrintLog(LogLevel.INFO, f'暂停标的交易 {target.targetName()}')
id = target.get_id()
if id in self.strategy_ctrl:
tradeController: SFGridStrategy = self.strategy_ctrl[target.get_id()]
tradeTarget = tradeController.enabledTrading(False)
orders = qmtv.queryPendingOrder(target.stock_code, tradeController.getName()) # type: ignore
for order in orders:
qmtv.xttrader.cancel_order_stock_async(qmtv.account, order.order_id)
print(f'取消未成交订单 {len(orders)}')
self.tradeTargetData[id] = tradeTarget
else:
print(f"标的交易控制器不存在 {target.stock_code} {target.stock_name}\n")
# self.add_log("INFO", f"已暂停交易: {target.stock_code} - {target.stock_name}")
# messagebox.showinfo("暂停成功", f"已暂停 {target.stock_code} ({target.stock_name}) 的交易")
@@ -526,27 +481,41 @@ class TradeTargetUI:
icon='warning'
)
id = target.get_id()
if result:
# 通过事件总线发出删除动作
eBus.event_bus.publish(eBus.ActionEventDeleteTradeTarget, target.get_id())
self.add_log(LogLevel.INFO, f"已发送删除请求: {target.stock_code} - {target.stock_name}")
try:
# 从数据库中删除
target.delete_instance()
del self.tradeTargetData[id]
del self.strategy_ctrl[id]
del self.stockCodeIdMap[str(target.stock_code)]
# 添加日志
PrintLog(LogLevel.INFO, f"交易标的已删除,ID: {id} {target.targetName()}")
except Exception as e:
PrintLog(LogLevel.ERROR, f"删除交易标的失败 ID {id}: {str(e)}")
PrintLog(LogLevel.INFO, f"已发送删除请求: {target.stock_code} - {target.stock_name}")
def add_trade_target(self):
"""添加新的交易标的"""
# 获取顶层窗口
root = self.winfo_toplevel()
# 创建顶层窗口
add_window = tk.Toplevel(self.root)
add_window = tk.Toplevel(root)
add_window.title("添加交易标的")
add_window.geometry("400x150")
add_window.resizable(False, False)
# 设置窗口模态
add_window.transient(self.root)
add_window.transient(root)
add_window.grab_set()
# 居中显示
self.root.update_idletasks()
x = self.root.winfo_x() + (self.root.winfo_width() // 2) - 200
y = self.root.winfo_y() + (self.root.winfo_height() // 2) - 75
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}")
# 创建输入框架
@@ -570,7 +539,7 @@ class TradeTargetUI:
return
# 发布事件通知主控制器添加标的
eBus.event_bus.publish(eBus.ActionEventAddTradeTarget, stock_code)
eBus.event_bus.publish(ActionEventAddTradeTarget, stock_code)
add_window.destroy()
def cancel_add():
@@ -583,20 +552,7 @@ class TradeTargetUI:
# 绑定回车键确认
stock_code_entry.bind('<Return>', lambda event: confirm_add())
self.add_log(LogLevel.INFO, "点击添加交易标的按钮")
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(fill=tk.X, pady=(5, 0))
self.log_visible = True
self.log_toggle_btn.config(text="📋 隐藏日志")
PrintLog(LogLevel.INFO, "点击添加交易标的按钮")
def refresh_table(self):
"""刷新表格数据"""
@@ -625,24 +581,12 @@ class TradeTargetUI:
# 刷新市场监控表格
self.populate_market_table()
def onLog(self, data:LogData):
self.add_log(data.level, data.message)
def add_log(self, level:LogLevel, message):
"""添加日志记录"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_table.insert('', 0, values=(timestamp, level.value, message))
def clear_logs(self):
"""清空日志记录"""
# 删除所有日志项
for item in self.log_table.get_children():
self.log_table.delete(item)
self.add_log(LogLevel.DEBUG, "日志已清空")
def system_settings(self):
"""系统设置"""
settings_window = tk.Toplevel(self.root)
# 获取顶层窗口
root = self.winfo_toplevel()
settings_window = tk.Toplevel(root)
settings_window.title("网格交易系统配置")
# 设置窗口大小
@@ -650,17 +594,17 @@ class TradeTargetUI:
window_height = 550
# 先设置为模态窗口
settings_window.transient(self.root)
settings_window.transient(root)
# 确保主窗口完全初始化
self.root.update_idletasks()
root.update_idletasks()
# 获取主窗口的实际大小(包括边框)
# 使用winfo_rootx/rooty获取窗口在屏幕上的绝对位置
main_x = self.root.winfo_rootx()
main_y = self.root.winfo_rooty()
main_width = self.root.winfo_width()
main_height = self.root.winfo_height()
main_x = root.winfo_rootx()
main_y = root.winfo_rooty()
main_width = root.winfo_width()
main_height = root.winfo_height()
# 计算设置窗口相对于主窗口的居中位置
x = main_x + (main_width - window_width) // 2
@@ -691,7 +635,7 @@ class TradeTargetUI:
# 读取当前配置
config = configparser.ConfigParser()
config_path = sfgrid_constants.get_config_path()
config_path = config.get('config', 'config_path')
config.read(config_path, encoding='utf-8')
# 创建输入框字典用于保存引用
@@ -902,40 +846,42 @@ class TradeTargetUI:
# 定义保存和取消按钮的功能(button_frame已在上方创建)
def save_settings():
"""保存配置"""
try:
# 计算网格价格序列
grid_prices = calculate_grid_prices()
if not grid_prices:
messagebox.showerror("错误", "网格价格参数有误,请检查输入!")
return
# try:
# # 计算网格价格序列
# grid_prices = calculate_grid_prices()
# if not grid_prices:
# messagebox.showerror("错误", "网格价格参数有误,请检查输入!")
# return
grid_price_str = ",".join([str(p) for p in grid_prices])
# grid_price_str = ",".join([str(p) for p in grid_prices])
# 更新配置对象
config.set('config', 'miniQMTPath', entries['miniQMTPath'].get())
config.set('config', 'grid_price', grid_price_str)
config.set('config', 'grid_volume', entries['grid_volume'].get())
config.set('config', 'account_no', entries['account_no'].get())
# # 更新配置对象
# config.set('config', 'miniQMTPath', entries['miniQMTPath'].get())
# config.set('config', 'grid_price', grid_price_str)
# config.set('config', 'grid_volume', entries['grid_volume'].get())
# config.set('config', 'account_no', entries['account_no'].get())
# 写入配置文件
config_path = sfgrid_constants.get_config_path()
with open(config_path, 'w', encoding='utf-8') as configfile:
config.write(configfile)
# # 写入配置文件
# config_path = config.get_config_path()
# with open(config_path, 'w', encoding='utf-8') as configfile:
# config.write(configfile)
# 重新加载配置到内存中
sfgrid_constants.initConfig()
# # 重新加载配置到内存中
# config.initConfig()
messagebox.showinfo("成功", f"配置已保存!\n网格价格序列: {grid_price_str}\n部分配置可能需要重启程序后生效。")
self.add_log(LogLevel.INFO, f"系统配置已更新 - 网格数量: {len(grid_prices)}")
settings_window.destroy()
# messagebox.showinfo("成功", f"配置已保存!\n网格价格序列: {grid_price_str}\n部分配置可能需要重启程序后生效。")
# self.add_log(LogLevel.INFO, f"系统配置已更新 - 网格数量: {len(grid_prices)}")
# settings_window.destroy()
except Exception as e:
messagebox.showerror("错误", f"保存配置失败:{str(e)}")
self.add_log(LogLevel.ERROR, f"保存配置失败: {str(e)}")
# except Exception as e:
# messagebox.showerror("错误", f"保存配置失败:{str(e)}")
# self.add_log(LogLevel.ERROR, f"保存配置失败: {str(e)}")
pass
def cancel_settings():
"""取消设置"""
settings_window.destroy()
# settings_window.destroy()
pass
# 在button_frame中添加按钮
ttk.Button(button_frame, text="💾 保存配置", command=save_settings, width=15).pack(side=tk.LEFT, padx=5)
@@ -950,22 +896,25 @@ class TradeTargetUI:
# 创建网格修正窗口
self.create_grid_correction_window(target)
def create_grid_correction_window(self, target: TradeTarget):
def create_grid_correction_window(self, target: SFGridTradeTarget):
"""创建网格修正窗口"""
# 获取顶层窗口
root = self.winfo_toplevel()
# 创建顶层窗口
correction_window = tk.Toplevel(self.root)
correction_window = tk.Toplevel(root)
correction_window.title(f"网格修正 - {target.stock_code} ({target.stock_name})")
correction_window.geometry("500x400")
correction_window.resizable(False, False)
# 设置窗口模态
correction_window.transient(self.root)
correction_window.transient(root)
correction_window.grab_set()
# 居中显示
self.root.update_idletasks()
x = self.root.winfo_x() + (self.root.winfo_width() // 2) - 250
y = self.root.winfo_y() + (self.root.winfo_height() // 2) - 200
root.update_idletasks()
x = root.winfo_x() + (root.winfo_width() // 2) - 250
y = root.winfo_y() + (root.winfo_height() // 2) - 200
correction_window.geometry(f"500x400+{x}+{y}")
# 创建主框架
@@ -1005,7 +954,7 @@ class TradeTargetUI:
required_position_frame.pack(fill=tk.X, pady=5)
grid_index_value = getattr(target, 'grid_index')
required_position = grid_index_value * sfgrid_constants.grid_volume
required_position = grid_index_value * config.grid_volume
ttk.Label(required_position_frame, text="需求持仓量:", width=12).pack(side=tk.LEFT)
required_position_label = ttk.Label(required_position_frame, text=str(required_position), width=10, anchor=tk.CENTER)
required_position_label.pack(side=tk.LEFT, padx=5)
@@ -1030,7 +979,7 @@ class TradeTargetUI:
# 增加按钮
ttk.Button(grid_index_frame, text="+", width=3,
command=lambda: self.increase_grid_index(grid_index_var, len(sfgrid_constants.grid_price)-1, target, required_position_label, position_status_label)).pack(side=tk.LEFT, padx=(5, 0))
command=lambda: self.increase_grid_index(grid_index_var, len(config.grid_price)-1, target, required_position_label, position_status_label)).pack(side=tk.LEFT, padx=(5, 0))
# 当前价格(实时更新)
price_frame = ttk.Frame(options_frame)
@@ -1054,7 +1003,7 @@ class TradeTargetUI:
command=correction_window.destroy).pack(side=tk.RIGHT, padx=5)
# 监听市场数据更新
def on_market_data_update(updated_target: TradeTarget):
def on_market_data_update(updated_target: SFGridTradeTarget):
if updated_target.get_id() == target.get_id():
current_price_var.set(f"{updated_target.market_price:.3f}" if updated_target.market_price else "-")
@@ -1081,13 +1030,13 @@ class TradeTargetUI:
shortage = required_position - current_position
status_label.config(text=f"还需补充 {shortage} 手仓位", foreground="red")
def save_grid_correction(self, window, target: TradeTarget, new_grid_index: int):
def save_grid_correction(self, window, target: SFGridTradeTarget, new_grid_index: int):
"""保存网格修正"""
# 更新网格序号
setattr(target, 'grid_index', new_grid_index)
# 重新计算需求持仓量
required_position = new_grid_index * sfgrid_constants.grid_volume
required_position = new_grid_index * config.grid_volume
# 检查持仓量是否满足要求
current_position = getattr(target, 'current_position')
@@ -1103,19 +1052,17 @@ class TradeTargetUI:
# 发布网格修正事件,传递GridFixData对象
grid_fix_data = GridFixData(new_grid_index, target)
eBus.event_bus.publish(eBus.ActionEventGridFix, grid_fix_data)
eBus.event_bus.publish(ActionEventGridFix, grid_fix_data)
# 关闭窗口
window.destroy()
# 添加日志
self.add_log(LogLevel.INFO, f"网格修正已保存: {target.stock_code} - {target.stock_name}, 网格序号: {new_grid_index}")
PrintLog(LogLevel.INFO, f"网格修正已保存: {target.stock_code} - {target.stock_name}, 网格序号: {new_grid_index}")
def run(self):
"""运行程序"""
self.root.mainloop()
def decrease_grid_index(self, grid_index_var: tk.IntVar, target: TradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
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:
@@ -1123,7 +1070,7 @@ class TradeTargetUI:
# 同步更新需求持仓量和持仓状态
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: TradeTarget, required_position_label: ttk.Label, position_status_label: ttk.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:
@@ -1131,12 +1078,76 @@ class TradeTargetUI:
# 同步更新需求持仓量和持仓状态
self.update_required_position_and_status(grid_index_var.get(), target, required_position_label, position_status_label)
def update_required_position_and_status(self, grid_index: int, target: TradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
def update_required_position_and_status(self, grid_index: int, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
"""更新需求持仓量和持仓状态"""
# 计算需求持仓量
required_position = grid_index * sfgrid_constants.grid_volume
required_position = grid_index * config.grid_volume
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 onAddTradeTarget(self, stock_code: str):
"""处理添加交易标的事件"""
try:
stock_name = qmtv.getInstrumentName(stock_code)
if not stock_name:
PrintLog(LogLevel.ERROR, f'无法获取股票代码 {stock_code} 的名称,请检查代码是否正确')
return
# 检查是否已存在该标的
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,
market_price=0.0,
current_position=pos,
grid_index=0,
last_trade_price=0.0,
plan_buy_price=0.0,
plan_sell_price=0.0,
current_order_price=0.0,
current_order_no='',
current_order_type=''
)
new_target.save()
# 更新标的池
self.updateTradeTarget(new_target)
except Exception as e:
PrintLog(LogLevel.ERROR, f'新增交易标的失败 {stock_code} {e}')
def update_trade_target_grid(self, data: GridFixData):
"""更新交易标的网格信息"""
try:
target = data.tradeTarget
grid_index = data.grid_index
# 更新数据库中的网格索引
target.grid_index = grid_index
target.save()
PrintLog(LogLevel.INFO, f"网格修正已应用: {target.stock_code} - {target.stock_name}, 网格索引: {grid_index}")
except Exception as e:
PrintLog(LogLevel.ERROR, f"网格修正更新失败: {str(e)}")
def toggle_market_monitor(self):
"""切换市场监控窗口显示/隐藏"""
if self.market_monitor_visible:
# 隐藏市场监控窗口
self.market_frame.pack_forget()
self.toggle_market_monitor_btn.config(text="")
self.market_monitor_visible = False
else:
# 显示市场监控窗口
self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
self.toggle_market_monitor_btn.config(text="◀▶")
self.market_monitor_visible = True
View File
+2 -9
View File
@@ -1,4 +1,4 @@
import sfgrid_constants
import config
import xtquant.xtconstant as xtconstant
from xtquant import xtdata, xttrader
from xtquant.xttype import StockAccount, XtOrder, XtPosition
@@ -37,13 +37,6 @@ def is_trading_time():
return False
def getInstrumentName(stock_code):
# print(f"getInstrumentName: 获取标的名称 {stock_code}")
detail = xtdata.get_instrument_detail(stock_code, False)
if detail is None:
return "UnNamed"
return detail['InstrumentName']
def getStockPosition(stock_code: str, xt_trader: xttrader.XtQuantTrader, account: StockAccount):
volume = 0
@@ -58,7 +51,7 @@ def getStockPosition(stock_code: str, xt_trader: xttrader.XtQuantTrader, account
return volume
def minPosition(gridIndex:int):
return sfgrid_constants.grid_volume * gridIndex
return config.grid_volume * gridIndex
def queryPendingOrder(stock_code:str, tag: str, xt_trader: xttrader.XtQuantTrader, account: StockAccount) -> list[XtOrder]:
if stock_code == None or tag == None:
BIN
View File
Binary file not shown.
+8 -16
View File
@@ -1,20 +1,12 @@
# coding:utf-8
from core import strategy_db
from core.main_controller import SFGridController
from core.logger import LogLevel, PrintLog
import sfgrid_constants as sdConstants
def startTrade(index: int):
ctrl.start_stock_trade(index)
def pauseTrade(index: int):
ctrl.pause_stock_trade(index)
from core.database import db
from core.main_ui import MainWindow
import config as sdConstants
from core.qmt import qmtv
if __name__ == '__main__':
sdConstants.initConfig()
strategy_db.db.connect()
strategy_db.db.create_tables([strategy_db.TradeTarget])
PrintLog(LogLevel.INFO, '- [成功]数据库模块初始化')
ctrl: SFGridController = SFGridController(sdConstants.account_no, sdConstants.miniQMTPath)
ctrl.hold()
qmtv.init_qmtv()
qmtv.connect()
window = MainWindow()
window.run()
+1 -1
View File
@@ -28,7 +28,7 @@ exe = EXE(
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,