56 Commits

Author SHA1 Message Date
kyugao 0b916b5c44 Merge branch 'new_structure' of ssh://git.gogao.top:2222/sfgrid into new_structure
# Conflicts:
#	config.py
2026-01-04 17:48:33 +08:00
kyugao 5a26f5f7b3 update 2026-01-04 17:46:48 +08:00
kyugao 66768cb359 update config 2025-12-08 18:08:43 +08:00
kyugao 988947aa1a 适配macos中对pytray支持不好的情况。使用系统菜单。 2025-12-06 00:12:44 +08:00
kyugao b435f12c49 update 2025-12-05 18:06:39 +08:00
kyugao c59d29d52e init new structure 2025-12-05 17:43:13 +08:00
kyugao 6b3b1a1f76 代码整理 2025-11-25 17:27:58 +08:00
kyugao 60af627806 日志模块更新 2025-11-25 17:20:23 +08:00
kyugao 4787011177 完善日志系统 2025-11-25 17:13:40 +08:00
kyugao 81da3fe013 update 2025-11-24 16:54:54 +08:00
kyugao c12e394b8e update 2025-11-24 16:47:40 +08:00
kyugao 3ee47e95cf 完善部分逻辑 2025-11-24 16:30:53 +08:00
kyugao e5b1d80139 过滤一条日志 2025-11-20 15:47:09 +08:00
kyugao 5c40d5d64f update 2025-11-20 14:20:18 +08:00
kyugao 1618cad5a0 删除部分无用代码,添加当前订单查询功能 2025-11-19 16:28:04 +08:00
kyugao 64bdddde79 修复一些逻辑问题 2025-11-19 15:55:23 +08:00
kyugao 4c4c8730f2 初步完成网格交易统计重构 2025-11-19 14:14:37 +08:00
kyugao 0262bfc71b 完成预下单优化 2025-11-19 10:29:58 +08:00
kyugao d5fef7c0c1 update 2025-11-18 18:06:15 +08:00
kyugao fcadcb86d2 日志更新 2025-11-18 13:34:44 +08:00
kyugao 59a88c4365 移除不需要的控制台日志 2025-11-18 10:55:13 +08:00
kyugao c69b5bc1ae 更新控制台日志输出格式 2025-11-18 10:43:54 +08:00
kyugao f626545897 release 2025-11-17 18:10:41 +08:00
kyugao f499d9a413 update for first release 2025-11-17 16:36:09 +08:00
kyugao 6cae413956 调整一些日志,基本上OK了 2025-11-17 11:50:51 +08:00
kyugao 6810b55cbb 第一个Alpha版本 2025-11-14 18:09:20 +08:00
kyugao cd67c9ad60 update 2025-11-14 15:52:03 +08:00
kyugao a18c7be7eb 代码测试与整理 2025-11-14 11:45:42 +08:00
kyugao f5d37eaa7e update 2025-11-13 17:42:00 +08:00
kyugao 550126d060 update 2025-11-13 17:32:10 +08:00
kyugao c03a4adb53 update 2025-11-12 18:14:56 +08:00
kyugao 81d0131a7b update 2025-11-12 17:57:45 +08:00
kyugao 1193dc2f69 独立配置 2025-11-12 15:08:56 +08:00
kyugao 8ab5d83b1a update 2025-11-12 12:50:22 +08:00
kyugao 91d1cac384 update 2025-11-12 12:49:22 +08:00
kyugao 2ca0ab65f0 Merge branch 'new_structure'
# Conflicts:
#	config.ini
#	config.py
2025-11-12 11:53:35 +08:00
kyugao 5e64e93172 update 2025-11-12 11:51:40 +08:00
kyugao 0d54f8b05a update 2025-11-12 10:48:43 +08:00
kyugao ba9cd9a700 update 2025-11-12 10:15:14 +08:00
kyugao 0dbd8e8dde 完成添加,删除,开启,停止方法的重构 2025-11-11 18:09:42 +08:00
kyugao 54fd7c9545 new update 2025-11-11 17:45:56 +08:00
kyugao 662a1ea7c1 update 2025-11-11 16:47:55 +08:00
kyugao c42648d1b4 update for restructure 2025-11-11 12:15:40 +08:00
kyugao 7cfb433aaf update 2025-11-10 17:16:06 +08:00
kyugao 1ec27bb52f 去掉不用的配置 2025-11-10 17:15:35 +08:00
kyugao 7f0a73381d release 2025-11-10 17:05:43 +08:00
kyugao c2f34d09e9 去掉不用的配置项 2025-11-10 16:19:04 +08:00
kyugao 20a7453e8b 程序整理 2025-11-10 14:49:35 +08:00
kyugao 3a137b6aee 市场数据跟踪 2025-11-10 14:43:14 +08:00
kyugao 7733d6df32 网格修正 2025-11-10 10:57:21 +08:00
kyugao b7f9d29c54 调试 2025-11-07 11:00:41 +08:00
kyugao d988f5eb48 完善UI操作逻辑 2025-11-06 18:13:10 +08:00
kyugao 1ee8f0426e 初步测试通过 2025-11-06 14:39:09 +08:00
kyugao 88bd0b17c9 新增功能,网格修正 2025-11-05 18:09:00 +08:00
kyugao df0e9ecb22 添加编译文件、打包瘦身、UI逻辑调整 2025-11-05 18:04:52 +08:00
kyugao c77ff1c0ae 合并UI 2025-11-05 10:34:30 +08:00
35 changed files with 2658 additions and 1331 deletions
+7
View File
@@ -1 +1,8 @@
__pycache__/
dist/
example.db
xtquant/
starter.dist/starter.dll
build/
.vscode/
example.db.bak
+2 -4
View File
@@ -1,7 +1,5 @@
[config]
miniqmtpath = /Users/gao/Workspace/quant
grid_price = 10.9,10.0,9.1,8.2,7.3,6.4,5.5,4.6,3.7,2.8,1.9,1.0
grid_volume = 200
miniqmtpath = D:/Programs/DTQMT/userdata_mini
account_no = 99082560
max_enabled_targets = 10
log_level = INFO
+64
View File
@@ -0,0 +1,64 @@
import configparser
from pathlib import Path
import sys
from typing import Any
miniQMTPath = r'D:\\Programs\\DTQMT\\userdata_mini' # miniQMT软件的安装路径
# miniQMTPath = ''
account_no:str = '99082560'
console_log = True
log_level = "INFO"
config : Any
def get_config_path() -> Path:
"""获取配置文件的正确路径(兼容开发环境和打包后的可执行文件)"""
if getattr(sys, 'frozen', False):
# 打包后的可执行文件环境
# sys._MEIPASS是PyInstaller解压临时文件的目录
# 配置文件应该放在可执行文件同目录下
base_path = Path(sys.executable).parent
else:
# 开发环境
base_path = Path(__file__).resolve().parent
return base_path / 'config.ini'
def get_config(section:str, key:str):
pass
def save_config(miniQmtPath:str, account_no:str):
"""创建默认配置文件"""
config = configparser.ConfigParser()
config['config'] = {
'miniQMTPath': miniQmtPath,
'account_no': account_no
}
config_path = get_config_path()
with open(config_path, 'w') as configfile:
config.write(configfile)
print(f'已创建默认配置文件: {config_path}')
def exist_config() -> bool:
"""检查配置文件是否存在"""
config_path = get_config_path()
return config_path.exists()
def initConfig() -> bool:
global miniQMTPath, account_no, log_level
# 获取配置文件路径
config_path = get_config_path()
config = configparser.ConfigParser()
config.read(config_path, encoding='utf-8')
miniQMTPath = config.get('config','miniQMTPath')
account_no = config.get('config','account_no')
log_level = config.get('config','log_level')
# 判断miniQMTPath是否为空,并且目录是否存在
if not miniQMTPath or not Path(miniQMTPath).exists():
print('请先配置miniQMTPath')
return False
else:
return True
+94
View File
@@ -0,0 +1,94 @@
# Global configuration variables
# Define these BEFORE imports to avoid circular dependency issues with logger
console_log = True
miniQMTPath = None
miniQMTAccount = None
log_level = "1"
from pathlib import Path
from core.config.config_model import ConfigModel, CfgKeyLogLevel, CfgKeyMiniQmtPath, CfgKeyMiniQmtAccount, CfgKeyConsoleLog
from core.database import db
def initConfig() -> bool:
"""Initialize configuration from database"""
global miniQMTPath, miniQMTAccount, log_level, console_log
# Ensure connection and tables
db.connect(reuse_if_open=True)
if not db.table_exists(ConfigModel._meta.table_name):
db.create_tables([ConfigModel])
# Check and initialize keys
_init_key(CfgKeyLogLevel, "1")
_init_key(CfgKeyConsoleLog, "True")
_init_key(CfgKeyMiniQmtPath, None)
_init_key(CfgKeyMiniQmtAccount, None)
# Load values
try:
miniQMTPath = _get_value(CfgKeyMiniQmtPath)
miniQMTAccount = _get_value(CfgKeyMiniQmtAccount)
log_level = _get_value(CfgKeyLogLevel) or "1"
console_log = _get_value(CfgKeyConsoleLog) or "True"
console_log = console_log.lower() == "true"
# console_log is not in DB currently, keeping default True or could add to DB
except Exception as e:
print(f"Error loading config: {e}")
return False
# Validate path
if not miniQMTPath or not Path(miniQMTPath).exists():
print('请先配置miniQMTPath')
return False
return True
def _init_key(key: str, default_value: str | None):
"""Helper to initialize a key if it doesn't exist"""
try:
ConfigModel.get(ConfigModel.key == key)
except ConfigModel.DoesNotExist:
ConfigModel.create(key=key, value=default_value)
def _get_value(key: str) -> str | None:
"""Helper to get value safely"""
try:
return ConfigModel.get(ConfigModel.key == key).value
except ConfigModel.DoesNotExist:
return None
def save_config(key: str, value: str):
"""Save configuration to database"""
_update_key(key, value)
print(f'配置已更新: {key}={value}')
def _update_key(key: str, value: str):
try:
record = ConfigModel.get(ConfigModel.key == key)
record.value = value
record.save()
except ConfigModel.DoesNotExist:
ConfigModel.create(key=key, value=value)
def exist_config() -> bool:
"""Check if essential config exists"""
path = _get_value(CfgKeyMiniQmtPath)
account = _get_value(CfgKeyMiniQmtAccount)
return bool(path and account)
def getLogLevel() -> str:
"""获取配置中的日志级别"""
return log_level
def getConsoleLog() -> bool:
"""获取配置中的控制台日志设置"""
return console_log
def getMiniQMTPath() -> str | None:
"""获取配置中的miniQMT路径"""
return miniQMTPath
def getMiniQMTAccount() -> str | None:
"""获取配置的miniQMT账号"""
return miniQMTAccount
+11
View File
@@ -0,0 +1,11 @@
from peewee import CharField
from core.database import BaseModel, db
CfgKeyLogLevel = "log_level"
CfgKeyConsoleLog = "console_log"
CfgKeyMiniQmtPath = "miniQMTPath"
CfgKeyMiniQmtAccount = "miniQMTAccount"
class ConfigModel(BaseModel):
key = CharField(unique=True)
value = CharField(null=True)
+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"
+4
View File
@@ -0,0 +1,4 @@
import xtquant.xtconstant as xtconstant
HeatTypeUpStop = "UpStop" # 涨停
HeatTypeDragonTiger = "DragonTiger" # 龙虎榜
+11
View File
@@ -0,0 +1,11 @@
from peewee import CharField, DateField
from core.database import BaseModel, db
class HeatStock(BaseModel):
stock_code = CharField(unique=True)
stock_name = CharField()
heat_type = CharField()
date = DateField()
db.create_tables([HeatStock])
+11
View File
@@ -0,0 +1,11 @@
from peewee import SqliteDatabase, Model
# 连接到SQLite数据库
db: SqliteDatabase = SqliteDatabase('example.db')
db.connect()
print("Database connected")
# 定义基础模型类
class BaseModel(Model):
class Meta:
database: SqliteDatabase = db
+17
View File
@@ -0,0 +1,17 @@
class EventBus:
def __init__(self):
self.listeners = {} # 管理各种event的订阅情况
def subscribe(self, event_type, listener):
if event_type not in self.listeners:
self.listeners[event_type] = []
self.listeners[event_type].append(listener)
def publish(self, event_type, data):
if event_type in self.listeners:
for listener in self.listeners[event_type]:
listener(data)
# 订阅与发布事件示例
# event_bus.subscribe('my_event', handle_event)
# event_bus.publish('my_event', {'key': 'value'})
+7
View File
@@ -0,0 +1,7 @@
from .eventbus import EventBus
# Pring Log
EventPrintLog = "print_log" # 打印日志
# 创建事件总线实例
loggerEBus = EventBus()
+10
View File
@@ -0,0 +1,10 @@
from eventbus import EventBus
# 市场数据监听控制事件
EventMarketActiveSwitch = "market_active_switch" # 市场数据状态变更
MarketDataUpdate = "market_data_update" # 市价更新
MarketOrderCreated = "market_order_created" # 市价单创建
MarketOrderTraded = "market_order_traded" # 市价单成交
# 创建事件总线实例
marketDataEventBus = EventBus()
+26
View File
@@ -0,0 +1,26 @@
from enum import Enum
from core.ebus.logger_ebus import EventPrintLog, loggerEBus
from core.config import config as config
class LogLevel(Enum):
DEBUG = 0
INFO = 1
WARNING = 2
ERROR = 3
CRITICAL = 4
def __le__(self, other):
return self.value <= other.value
class LogData:
def __init__(self, level:LogLevel, message:str):
self.level = level
self.message = message
def PrintLog(level:LogLevel, message:str):
data = LogData(level, message)
loggerEBus.publish(EventPrintLog, data)
if config.getConsoleLog():
print(f'{level.name} {message}')
-310
View File
@@ -1,310 +0,0 @@
# coding:utf-8
from xtquant.xttrader import XtQuantTrader
import time, sys
from peewee import ModelSelect
import xtquant.xtconstant as xtconstant
sys.stdout.reconfigure(encoding='utf-8') # 设置标准输出编码为UTF-8 # type: ignore
import core.strategy_db as strategy_db
import sfgrid_constants
from core.sfgrid_strategy import SFGridStrategy
from core.util import getInstrumentName, getStockPosition
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
# 量化核心控制对象
class SFGridController(XtQuantTraderCallback):
def __init__(self, account_no: str, miniQmtPath: str):
super().__init__()
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()
connect_result = self.xt_trader.connect()
print(f'- [{'成功' if self.xt_trader.connected else '失败'}]市场交易连接{connect_result}--: {miniQmtPath}')
if self.xt_trader.connected == False:
self.inited: bool = False
return
else:
self.inited = True
self.account= StockAccount(account_no, 'STOCK')
print(f'- [成功]交易账号对象初始化完成, 账号: {self.account.account_id}') # pyright: ignore[reportAttributeAccessIssue]
subscribe_result = self.xt_trader.subscribe(self.account)
print(f'- [{'成功' if subscribe_result == 0 else '失败'}:{subscribe_result}]交易状态订阅')
if subscribe_result == 0:
self.inited = True
else:
self.inited = False
return
self.stock_trade_ctrl = {}
self.init_instrument_pool(self.xt_trader, self.account) # type: ignore
self.seq = None
print('- [成功]三疯交易系统初始化完成')
self.startMarketData()
def startMarketData(self):
print('- 启动市场数据订阅')
self.seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], callback=self.onDataUpdate)
if self.seq == -1:
print('- 市场数据订阅失败')
else:
print(f'- 市场数据订阅成功, 订阅号={self.seq}')
def stopMarketData(self):
print('- 停止市场数据订阅')
if self.seq is not None and self.seq > 0:
xtdata.unsubscribe_quote(self.seq)
def add_trade_target(self, stock_code: str):
try:
stock_name = getInstrumentName(stock_code)
new_target = strategy_db.TradeTarget.create(
stock_name=stock_name,
stock_code=stock_code,
current_position=0,
grid_index=0,
last_trade_price=0.0,
current_buy_price=0.0,
current_buy_order_no='',
current_sell_price=0.0,
current_sell_order_no=''
)
new_target.save()
print(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, new_target.enabled) # type: ignore
self.stock_trade_ctrl[stock_code] = stockTradeController
except Exception as e:
print(f'新增交易标的失败 {stock_code} {e}')
def del_trade_target(self, index:int):
target: strategy_db.TradeTarget = self.instrument_pool[index]
# self.stock_trade_ctrl.
del self.stock_trade_ctrl[target.stock_code]
target.delete_instance()
self.refresh_targets()
def init_instrument_pool(self, xtTrader:XtQuantTrader, account:StockAccount):
self.refresh_targets()
for temp in self.instrument_pool:
tradeTarget:strategy_db.TradeTarget = temp
tradeTarget.current_position = getStockPosition(tradeTarget.stock_code, xtTrader, account) # type: ignore
result = tradeTarget.save()
print(f' |- 同步当前持仓信息 {tradeTarget.stock_code}, {tradeTarget.current_position}, result = {result}')
stockTradeController = SFGridStrategy(tradeTarget, self.xt_trader, self.account, tradeTarget.enabled) # type: ignore
self.stock_trade_ctrl[tradeTarget.stock_code] = stockTradeController
print(f'- [成功]交易标的信息初始化, 共 {len(self.instrument_pool)} 个标的')
def refresh_targets(self):
# 更新标的池
self.instrument_pool:ModelSelect = strategy_db.TradeTarget.select()
self.print_pool()
def print_pool(self):
print("- [信息]标的池信息")
for i in range(len(self.instrument_pool)):
target: strategy_db.TradeTarget = self.instrument_pool[i]
status = "新建" if target.status == 0 else "已建初始仓"
print(f' [序号-{i}] 股票代码: {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
def print_position_info(self):
positions:list[XtPosition] = self.xt_trader.query_stock_positions(self.account)
if positions:
print("\n- 持仓信息")
for temp in positions:
pos : XtPosition = temp
if pos.volume <=0:
continue
print(f"股票代码: {pos.stock_code}-{getInstrumentName(pos.stock_code)}")
print(f"总持仓: {pos.volume}")
print(f"可用持仓: {pos.can_use_volume}")
print(f"持仓成本: {pos.avg_price}")
print("---")
else:
print("\n当前无持仓")
def print_account_info(self):
temp = self.xt_trader.query_stock_asset(self.account)
asset: XtAsset = temp # type: ignore
print(f"=== 账户信息 {self.account.account_id} ===") # type: ignore
print(f"可用资金: {asset.cash}")
print(f"总资产: {asset.total_asset}")
print(f"证券市值: {asset.market_value}")
def print_stock_orders(self):
orders = self.xt_trader.query_stock_orders(self.account, cancelable_only=True)
if orders:
print("\n=== 委托信息 ===")
for order in orders:
print(f"委托编号: {order.order_id}")
print(f"股票代码: {order.stock_code} {getInstrumentName(order.stock_code)}")
print(f"委托方向: {order.offset_flag} ")
print(f"委托价格: {order.price}")
print(f"委托数量: {order.order_volume}")
print(f"已成交数量: {order.traded_volume}")
print(f"委托状态: {order.order_status} ")
print("---")
else:
print("\n当前无委托记录")
# 初始化指定标的交易控制器
def start_stock_trade(self, index: int):
tradeTarget = self.instrument_pool[index]
# check existing thread
if tradeTarget.stock_code in self.stock_trade_ctrl:
tradeController: SFGridStrategy = self.stock_trade_ctrl[tradeTarget.stock_code]
if tradeController.isEnabled():
print(f"标的交易控制器已存在且正在运行 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n")
else:
print(f"标的交易控制器已存在但未运行,重新启动 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n")
tradeController.enabledTrading(True)
else:
stockTradeController = SFGridStrategy(tradeTarget, self.xt_trader, self.account, tradeTarget.enabled) # type: ignore
self.stock_trade_ctrl[tradeTarget.stock_code] = stockTradeController
print(f"\t创建标的交易控制器 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}")
def pause_stock_trade(self, index: int):
tradeTarget = self.instrument_pool[index]
if tradeTarget.stock_code in self.stock_trade_ctrl:
tradeController: SFGridStrategy = self.stock_trade_ctrl[tradeTarget.stock_code]
if tradeController.isEnabled():
print(f"暂停标的交易 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n")
tradeController.enabledTrading(False)
else:
print(f"标的交易已暂停 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n")
else:
print(f"标的交易控制器不存在 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n")
# ====== 市场回调方法 -- 以下方法由XtQuantData调用 ======
def onDataUpdate(self, data):
if sfgrid_constants.max_enabled_targets <= 0: # 全推
for stock_code, tickData in data.items():
lastPrice = tickData['lastPrice']
if lastPrice == 10.0 and stock_code not in self.stock_trade_ctrl:
print(f'New trade target = {stock_code} - {getInstrumentName(stock_code)} {tickData['lastPrice']}')
self.add_trade_target(stock_code)
self.stock_trade_ctrl[stock_code].enabledTrading(True)
else: # 指定目标 当前主要使用这种模式
for target in self.instrument_pool:
stock_code = target.stock_code
# 如果存在对应的StockTradeController,则调用其onDataUpdate方法
if stock_code not in self.stock_trade_ctrl or stock_code not in data:
# print(f"股票代码 {stock_code} 未在交易控制器中找到,跳过处理。\n")
continue
stock_controller: SFGridStrategy = self.stock_trade_ctrl[stock_code]
stock_controller.onDataUpdate(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:
"""
stockCode = order.stock_code
ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# 如果存在对应的StockTradeController,则调用其onDataUpdate方法
if ctrl is not None and order.strategy_name == ctrl.getName():
ctrl.onOrderTrade(trade=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 test_sim_trade(self, index: int, orderType: int):
tradeTarget:strategy_db.TradeTarget = self.instrument_pool[index]
ctrl:SFGridStrategy = self.stock_trade_ctrl[tradeTarget.stock_code]
trade: XtTrade = None # type: ignore
if orderType == xtconstant.STOCK_BUY:
trade = XtTrade(
sfgrid_constants.account_no,
'300083.SZ',
xtconstant.STOCK_BUY,
1, 1, tradeTarget.current_buy_price, sfgrid_constants.grid_volume, 1000,
tradeTarget.current_buy_order_no,
None, ctrl.getName(), None, None, None, None, None, tradeTarget.stock_name)
else:
trade = XtTrade(sfgrid_constants.account_no, '300083.SZ', xtconstant.STOCK_SELL, 1, 1, price, sfgrid_constants.grid_volume, 1000, tradeTarget.current_sell_order_no, None, ctrl.getName(), None, None, None, None, None, tradeTarget.stock_name) # type: ignore
self.on_stock_trade(trade)
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(), sys._getframe().f_code.co_name)
+179
View File
@@ -0,0 +1,179 @@
# coding:utf-8
# MainEntry 负责应用主窗口与菜单的统一构建:
# - 通过 build_menu_model 定义跨平台统一的菜单数据结构
# - 在 macOS 上使用 Tk 菜单栏;在非 macOS 上使用 pystray 系统托盘
# - 所有菜单项均绑定到同名处理函数,切换平台无需改动业务逻辑
import tkinter as tk
from core.logger import LogLevel, PrintLog
import threading
import sys
class MainEntry:
def __init__(self, master):
# 初始化 Tk 窗口属性与基础状态
self.master = master
self.master.title("Main Board")
self.master.geometry("800x600")
self.master.configure(bg="#f0f0f0")
self.master.resizable(False, False)
self.master.protocol("WM_DELETE_WINDOW", self.hide_window)
# QMT 开关状态用于动态更新菜单文案
self.qmt_enabled = False
self.icon = None
# 非 macOS 使用系统托盘(pystray);macOS 使用原生菜单栏
self.systray_supported = sys.platform != "darwin"
# 主内容容器
self.main_frame = tk.Frame(self.master, bg="#f0f0f0")
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
# 首次进入根据平台构建菜单
self.create_menu()
self.create_dashboard()
def build_menu_model(self):
# 菜单模型统一描述所有菜单:
# - 每个分组包含 label 与 items
# - item 支持:label 文案、action 处理函数名、enabled 启用状态、default 默认项、separator 分隔符
# - 文案可根据状态动态生成(如 QMT 开关)
qmt_label = "QMT (已开启)" if self.qmt_enabled else "QMT (已关闭)"
return [
{
"label": "-- 交易大师 --",
"items": [
{"label": "交易复盘", "action": "handler", "enabled": True},
{"label": "市场数据", "action": "handler", "enabled": True},
{"label": "快速下单", "action": "handler", "enabled": True},
],
},
{
"label": "-- 策略交易 --",
"items": [
{"label": "交易看板", "action": "handler", "enabled": True},
{"label": "策略中心", "action": None, "enabled": False},
{"label": "策略定制", "action": None, "enabled": False},
],
},
{
"label": "-- 实时数据 --",
"items": [
{"label": qmt_label, "action": "marketDataSwitch", "enabled": True},
],
},
{
"label": "-- 系统 --",
"items": [
{"label": "控制台", "action": "show_window", "enabled": True, "default": True},
{"label": "设置", "action": "marketDataSwitch", "enabled": True},
{"separator": True},
{"label": "退出", "action": "quit_window", "enabled": True},
],
},
]
def create_dashboard(self):
# 根据菜单模型构建主窗口按钮面板
for widget in self.main_frame.winfo_children():
widget.destroy()
model = self.build_menu_model()
for group in model:
# 为每个分组创建 LabelFrame
group_frame = tk.LabelFrame(self.main_frame, text=group["label"], bg="#f0f0f0", padx=10, pady=10)
group_frame.pack(fill=tk.X, pady=10, padx=10)
for it in group["items"]:
if it.get("separator"):
continue
fn = getattr(self, it["action"]) if it.get("action") else None
state = tk.NORMAL if it.get("enabled", True) else tk.DISABLED
# 创建按钮
btn = tk.Button(group_frame, text=it["label"], command=fn, state=state)
btn.pack(side=tk.LEFT, padx=5)
def create_menu(self):
# 根据统一菜单模型与平台类型,渲染到系统托盘或 Tk 菜单栏
model = self.build_menu_model()
if self.systray_supported:
# 非 macOS:延迟导入 pystray 与 PIL,避免在 macOS 上引入不兼容依赖
from PIL import Image
import pystray
image = Image.open("logo.png")
items = []
for group in model:
# 分组标题作为禁用的头部项
items.append(pystray.MenuItem(group["label"], None, enabled=False))
for it in group["items"]:
if it.get("separator"):
items.append(pystray.Menu.SEPARATOR)
else:
fn = getattr(self, it["action"]) if it.get("action") else None
items.append(pystray.MenuItem(it["label"], fn, default=it.get("default", False), enabled=it.get("enabled", True)))
menu = tuple(items)
if self.icon:
# 已存在托盘图标:更新菜单
self.icon.menu = menu
self.icon.update_menu()
else:
# 首次创建托盘图标并在后台线程运行
self.icon = pystray.Icon("name", image, "标题", menu)
self.trayThread = threading.Thread(target=self.icon.run, daemon=True)
self.trayThread.start()
else:
# macOS:使用 Tk 菜单栏
menu_bar = tk.Menu(self.master)
for group in model:
m = tk.Menu(menu_bar, tearoff=0)
for it in group["items"]:
if it.get("separator"):
m.add_separator()
else:
fn = getattr(self, it["action"]) if it.get("action") else None
if it.get("enabled", True) and fn:
m.add_command(label=it["label"], command=fn)
else:
m.add_command(label=it["label"], state="disabled")
menu_bar.add_cascade(label=group["label"], menu=m)
self.master.config(menu=menu_bar)
def marketDataSwitch(self):
# 切换 QMT 开关,并触发菜单重建以更新文案
if self.qmt_enabled:
self.qmt_enabled = False
PrintLog(LogLevel.INFO, "QMT 市场数据已关闭")
else:
self.qmt_enabled = True
PrintLog(LogLevel.INFO, "QMT 市场数据已开启")
self.create_menu()
self.create_dashboard()
def handler(self):
# 通用占位处理:当前仅记录点击行为,后续可替换为具体业务逻辑
PrintLog(LogLevel.INFO, f"点击了")
def hide_window(self):
# 关闭窗口事件:隐藏但不退出应用
PrintLog(LogLevel.INFO, "隐藏主窗口")
self.master.withdraw() # 隐藏主窗口
def show_window(self):
# 显示主窗口;在非 macOS 平台同步让托盘图标可见
if self.icon:
self.icon.visible = True
PrintLog(LogLevel.INFO, "显示主窗口")
self.master.deiconify() # 显示主窗口
def quit_window(self, icon=None):
# 退出应用;在非 macOS 平台时关闭托盘图标
if icon:
icon.stop()
PrintLog(LogLevel.INFO, "退出应用")
self.master.quit()
self.master.destroy()
def run(self):
# 主事件循环入口
self.master.mainloop()
+262
View File
@@ -0,0 +1,262 @@
import tkinter as tk
from tkinter import ttk
from core.logger import LogLevel, LogData, PrintLog
from core.sfgrid.sfgrid_ui import TradeTargetUI
from tkinter import ttk
from core.eventbus import EventPrintLog
from core.eventbus import event_bus as eBus
class MainWindow:
def __init__(self, configLogLevel:str):
self.root = tk.Tk()
self.root.title("神之一手 - 交易系统")
self.root.geometry("1400x700")
self.logLevel = LogLevel[configLogLevel]
PrintLog(LogLevel.DEBUG, f"系统启动成功 {self.logLevel.name}")
# 当前选中的策略Tab索引
self.current_strategy_index = 0
# 存储各个Frame的引用
self.strategy_frames = {}
# 日志面板可见性标志
self.log_visible = False
self.create_ui()
eBus.subscribe(EventPrintLog, self.on_log_event)
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))
# 创建自定义样式
self.create_custom_styles()
# 创建Tab按钮(垂直排列,文字垂直显示)
self.tab_buttons = []
strategy_names = ["网格", "复盘"]
for idx, name in enumerate(strategy_names):
btn = ttk.Button(
tab_bar_frame,
text=name,
command=lambda i=idx: self.switch_strategy_tab(i),
width=4,
style='Bookmark.TButton' # 使用自定义书签样式
)
btn.pack(side=tk.TOP, pady=2, fill=tk.X)
self.tab_buttons.append(btn)
# 在Tab按钮下方添加退出按钮和日志按钮(底部对齐)
# 使用一个填充Frame将按钮推到底部
spacer = ttk.Frame(tab_bar_frame)
spacer.pack(side=tk.TOP, fill=tk.X, ipady=10)
# 清空日志按钮(底部第三个)
clear_log_btn = ttk.Button(
tab_bar_frame,
text="🗑", # 垃圾桶图标
command=self.clear_logs,
width=3
)
clear_log_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
# 日志显示按钮(退出按钮上方)
self.log_toggle_btn = ttk.Button(
tab_bar_frame,
text="📋", # 日志图标
command=self.toggle_log_panel,
width=3
)
self.log_toggle_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
# 退出按钮(最底部)
exit_btn = ttk.Button(
tab_bar_frame,
text="", # 电源图标
command=self.on_exit,
width=3
)
exit_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
# 添加垂直分隔线
separator = ttk.Separator(content_area, orient='vertical')
separator.pack(side=tk.LEFT, fill=tk.Y, padx=1)
# 右侧内容区域容器(用于放置不同策略的Frame)
self.content_container = ttk.Frame(content_area)
self.content_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 创建各个策略的Frame
self.create_strategy_frames(strategy_names)
# 创建全局日志面板(默认隐藏)
self.create_global_log_panel(main_container)
# 默认显示第一个策略
self.switch_strategy_tab(0)
def create_custom_styles(self):
"""创建自定义样式"""
style = ttk.Style()
# 创建书签样式
style.configure(
'Bookmark.TButton',
relief='flat',
borderwidth=1,
padding=(5, 10),
foreground='black',
background='#FFE599', # 浅黄色背景,类似便签纸
font=('Arial', 10, 'bold')
)
# 设置焦点样式(选中状态)
style.map(
'Bookmark.TButton',
background=[('active', '#F1C232'), ('pressed', '#F1C232')],
relief=[('pressed', 'sunken')]
)
# 创建选中状态的书签样式
style.configure(
'SelectedBookmark.TButton',
relief='flat',
borderwidth=1,
padding=(5, 10),
background='#3D85C6', # 蓝色背景表示选中状态
font=('Arial', 10, 'bold')
)
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 on_log_event(self, event:LogData):
if self.logLevel.value <= event.level.value:
self.add_log(event.level, event.message)
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.name, message))
def clear_logs(self):
"""清空日志记录"""
# 删除所有日志项
for item in self.log_table.get_children():
self.log_table.delete(item)
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按钮的样式以显示选中状态"""
# 重置所有按钮为普通书签样式
for i, btn in enumerate(self.tab_buttons):
if i == self.current_strategy_index:
btn.configure(style='SelectedBookmark.TButton') # 选中状态
else:
btn.configure(style='Bookmark.TButton') # 普通状态
def toggle_log_panel(self):
"""切换日志面板的显示/隐藏"""
if self.log_visible:
# 隐藏日志面板
self.log_frame.pack_forget()
self.log_visible = False
self.log_toggle_btn.config(text="📋") # 日志图标
else:
# 显示日志面板
self.log_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0))
self.log_visible = True
self.log_toggle_btn.config(text="🔽") # 使用不同图标表示隐藏
def on_exit(self):
"""退出程序"""
from tkinter import messagebox
result = messagebox.askyesno("确认退出", "确定要退出系统吗?")
if result:
self.root.destroy()
def run(self):
"""运行程序"""
self.root.mainloop()
+8
View File
@@ -0,0 +1,8 @@
from qmt import QmtV
from eventbus import marketDataEventBus
qmtv:QmtV = None
def init_qmtv():
global qmtv
qmtv = QmtV()
+209
View File
@@ -0,0 +1,209 @@
import datetime
import threading
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
from eventbus import marketDataEventBus, EventMarketActiveSwitch, MarketDataUpdate, MarketOrderCreated, MarketOrderTraded
class QmtV(XtQuantTraderCallback):
def __init__(self) -> None:
self.xttrader: XtQuantTrader
self.inited: bool = False
self.details = {}
self.lastMarketDataUpdateTimestamp = time.time()
self.isMarketActive = False
self.refresh_thread = threading.Thread(target=self.marketStatusNotifier, daemon=True)
self.refresh_thread.start()
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) -> bool:
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 self.inited
else:
self.inited = True
self.account = StockAccount(config.miniQMTAccount, 'STOCK') # pyright: ignore[reportAssignmentType, reportAttributeAccessIssue]
PrintLog(LogLevel.INFO, f'- [成功]交易账号对象初始化完成, 账号: {config.miniQMTAccount}') # 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.inited
self.startMarketDataSubscription()
return self.inited
def getStockPosition(self, stock_code: str):
positions = self.xttrader.query_stock_positions(self.account)
if positions:
for temp in positions:
pos:XtPosition = temp
if pos.stock_code == stock_code:
return pos
return None
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 cacheStockDetail(self, stock_code:str):
if stock_code in self.details:
return self.details[stock_code]
else:
self.details[stock_code] = xtdata.get_instrument_detail(stock_code, False)
return self.details[stock_code]
def getInstrumentName(self, stock_code:str):
return self.cacheStockDetail(stock_code)['InstrumentName']
def dailyUpStop(self, stock_code:str):
cacheStock = self.cacheStockDetail(stock_code)
PrintLog(LogLevel.INFO, f'- [成功]获取股票详情: {stock_code} {cacheStock["InstrumentName"]} {cacheStock["UpStopPrice"]}')
return cacheStock['UpStopPrice']
def dailyDownStop(self, stock_code:str):
return self.cacheStockDetail(stock_code)['DownStopPrice']
# ========================================#
def startMarketDataSubscription(self):
try:
self.subscriptionId = xtdata.subscribe_whole_quote(['SH', 'SZ'], self.onDataUpdate)
PrintLog(LogLevel.INFO, f'- [市场数据订阅成功-{self.subscriptionId}]')
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [市场数据订阅失败-{e}]')
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):
# 收集所有市场数据用于市场监控
marketDataEventBus.publish(marketDataEventBus.MarketDataUpdate, data)
now = time.time()
if now - self.lastMarketDataUpdateTimestamp < 5:
self.isMarketActive = True
self.lastMarketDataUpdateTimestamp = now
def marketStatusNotifier(self):
# 市场状态通知器
tmpMarketStatus = False
while True:
tmpTime = time.time()
time.sleep(10)
if tmpMarketStatus != self.isMarketActive and tmpTime - self.lastMarketDataUpdateTimestamp < 5:
tmpMarketStatus = self.isMarketActive
PrintLog(LogLevel.INFO, f'- [市场状态变更] {self.isMarketActive}')
marketDataEventBus.publish(EventMarketActiveSwitch, self.isMarketActive)
if tmpMarketStatus and self.isMarketActive and tmpTime - self.lastMarketDataUpdateTimestamp > 10: # 上次更新市场状态已经超过10秒
self.isMarketActive = False
PrintLog(LogLevel.INFO, f'- [市场状态变更] {self.isMarketActive}')
PrintLog(LogLevel.DEBUG, f'- [市场状态] {self.isMarketActive}') # 市场已 inactive
# ====== 市场回调方法 -- 以下方法由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:
"""
pass
# print(f"委托回调 on_stock_order 投资备注 {order.order_id} {order.strategy_name} {order.order_remark}")
def on_stock_trade(self, trade:XtTrade):
"""
成交变动推送
:param trade: XtTrade对象
:return:
"""
marketDataEventBus.publish(MarketOrderTraded, trade)
# 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"委托回调 on_order_stock_async_response 投资备注 {response.order_id} {response.seq} {response.error_msg}{response.strategy_name} {response.order_remark}")
marketDataEventBus.publish(MarketOrderCreated, response)
# 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)
+7
View File
@@ -0,0 +1,7 @@
from peewee import CharField, DateField
from core.database import BaseModel, db
class StockInfo(BaseModel):
stock_code = CharField(unique=True, primary_key=True)
stock_name = CharField()
+11
View File
@@ -0,0 +1,11 @@
# 软件介绍
软件名称:神之一手交易系统
软件介绍:面向个人的交易管理系统,提供交易记录、复盘工具、持仓管理、资产监控、策略交易等功能。
# 模块介绍
1. /core/daily_review: 每日复盘模块目录
2. /core/market_data: 市场数据模块目录
3. /core/quick_trade: 快速交易模块目录
4. /core/strategy/builder: 策略构建模块目录
5. /core/strategy/trade: 策略交易模块目录
6. /core: 应用核心程序目录
-182
View File
@@ -1,182 +0,0 @@
from re import L
from core import util
from core.strategy_db import TradeTarget
from core.util import queryPendingOrder
from xtquant import xttrader, xtconstant
from xtquant.xttype import StockAccount, XtOrderResponse, XtTrade
import sfgrid_constants
import threading
class SFGridStrategy:
def __init__(self, tradeTarget: TradeTarget, xt_trader: xttrader.XtQuantTrader, account: StockAccount, enabled: bool = False):
self.tradeTarget:TradeTarget = tradeTarget
self.xt_trader: xttrader.XtQuantTrader = xt_trader
self.account:StockAccount = account
self.enabledTrading(enabled)
self.dataUpdateLock = threading.Lock()
def getName(self):
return "SFGRID"
def enabledTrading(self, enabled: bool):
self.tradeTarget.enabled = enabled # type: ignore
self.tradeTarget.save()
pendingOrders = queryPendingOrder(str(self.tradeTarget.stock_code),self.getName(), self.xt_trader,self.account)
if len(pendingOrders) > 0:
print(f' |- 已存在{len(pendingOrders)}订单,全部取消,按需要重下。')
for order in pendingOrders:
self.xt_trader.cancel_order_stock(self.account, order.order_id)
if enabled:
print(f" |- 标的{self.tradeTarget.targetName()}交易启动, position {self.tradeTarget.current_position}")
# 建仓状态检查
if int(self.tradeTarget.current_position) == 0 and int(self.tradeTarget.status) == 0: # type: ignore
self.tradeTarget.grid_index = 1 # type: ignore
self.tradeTarget.save()
self.initBuyOrderId = self.xt_trader.order_stock_async(
self.account,
str(self.tradeTarget.stock_code),
xtconstant.STOCK_BUY,
sfgrid_constants.grid_volume,
xtconstant.FIX_PRICE,
sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)], # type: ignore
'sf_grid', f'{self.tradeTarget.stock_code}_init_buy')
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 建初始仓 买单已发出 InitBuyOrderSeq: {self.initBuyOrderId} Price: {sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)]} Volume: {sfgrid_constants.grid_volume}\n") # type: ignore
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
if minRequirePosition <= int(self.tradeTarget.current_position): # type: ignore
print(f' |- 仓位检查: 持仓需求充足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}')
else:
print(f' |- 仓位检查: 持仓需求不足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}')
def isEnabled(self) -> bool:
return bool(self.tradeTarget.enabled)
def onDataUpdate(self, data):
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:
index = self.tradeTarget.grid_index
price = sfgrid_constants.grid_price[int(index)] # pyright: ignore[reportArgumentType]
lowPrice = sfgrid_constants.grid_price[int(index) + 1] if len(sfgrid_constants.grid_price)>int(index) + 1 else -1.0 # pyright: ignore[reportArgumentType]
highPrice = sfgrid_constants.grid_price[int(index) - 1] # pyright: ignore[reportArgumentType]
lastPrice = float("{:.3f}".format(data[self.tradeTarget.stock_code]['lastPrice']))
print(f"|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - 价格: {lastPrice}, 网格序号: {index}, 网格价格: {price}, 计划多单价: {lowPrice}, 计划空单价: {highPrice}")
if lastPrice <= lowPrice: # 下下方多单
orders = queryPendingOrder(str(self.tradeTarget.stock_code), self.getName(), self.xt_trader, self.account)
if len([order for order in orders if order.order_type == xtconstant.STOCK_BUY and order.price == lowPrice]) > 0:
# 已存在未交易的多单
print(f' |- 已存在未交易的多单,不重复下单')
else:
print(f' |- 下网格多单')
self.tradeTarget.current_buy_order_no = self.xt_trader.order_stock_async(
self.account,
str(self.tradeTarget.stock_code),
xtconstant.STOCK_BUY,
sfgrid_constants.grid_volume,
xtconstant.FIX_PRICE,
lowPrice,
self.getName(), # strategy_name
self.tradeTarget.stock_code # remark # type: ignore
)
self.tradeTarget.current_buy_price = float(lowPrice) # type: ignore
print(f' |- 下网格多单号 {self.tradeTarget.current_buy_order_no}, 网格基准价 {price}, 下单价 {lowPrice}, 下单量 {sfgrid_constants.grid_volume}')
elif lastPrice == highPrice: # 下上方空单
orders = queryPendingOrder(str(self.tradeTarget.stock_code), self.getName(), self.xt_trader, self.account)
if len([order for order in orders if order.order_type == xtconstant.STOCK_SELL and order.price == highPrice]) > 0:
# 已存在未交易的空单
print(f' |- 已存在未交易的空单,不重复下单')
else:
print(f' |- 下网格空单')
self.tradeTarget.current_sell_order_no = self.xt_trader.order_stock_async(
self.account,
str(self.tradeTarget.stock_code),
xtconstant.STOCK_SELL,
sfgrid_constants.grid_volume,
xtconstant.FIX_PRICE,
highPrice,
self.getName(),
self.tradeTarget.stock_code) # type: ignore
self.tradeTarget.current_sell_price = float(highPrice) # type: ignore
print(f' |- 下网格空单号 {self.tradeTarget.current_sell_order_no}, 网格基准价 {price}, 下单价 {highPrice}, 下单量 {sfgrid_constants.grid_volume}')
self.tradeTarget.save()
finally:
print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - release lock')
self.dataUpdateLock.release()
print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - END')
def onAsyncOrderResponse(self, order:XtOrderResponse):
print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]:START')
self.dataUpdateLock.acquire()
print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]:LOCKED')
try:
stockCode = order.order_remark
orderSeq = order.seq
if (self.tradeTarget.status == 1): # 正常交易阶段订单下单成功
if self.tradeTarget.current_buy_order_no == order.seq:
self.tradeTarget.current_buy_order_no = order.order_id
elif self.tradeTarget.current_sell_order_no == order.seq:
self.tradeTarget.current_sell_order_no = order.order_id
else:
print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]: 不在策略监控范围内')
rc = self.tradeTarget.save()
finally:
print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]:release lock')
self.dataUpdateLock.release()
print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]:END')
def onOrderTrade(self, trade:XtTrade):
print(f' |- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}]:START')
self.dataUpdateLock.acquire()
print(f' |- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}]:LOCKED')
try:
if int(self.tradeTarget.status) == 0 and trade.order_id == self.initBuyOrderId : # type: ignore
# 此时为建仓成交
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.status = 1 # type: ignore
self.tradeTarget.save()
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 建初始仓订单ID: {self.initBuyOrderId}已成交 ")
print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
print(f' 当前持仓: {self.tradeTarget.current_position}')
print(f' 网格坐标: {self.tradeTarget.grid_index}')
elif trade.order_id == self.tradeTarget.current_sell_order_no and int(self.tradeTarget.status) == 1: # type: ignore
# 上涨一格:此时空单成交
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
self.tradeTarget.save()
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 上涨 卖单已成交 订单ID: {self.tradeTarget.current_sell_order_no} Price: {sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)]} Volume: {sfgrid_constants.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}')
elif trade.order_id == self.tradeTarget.current_buy_order_no and int(self.tradeTarget.status) == 1: # type: ignore
# 下跌一格:此时多单成交
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
self.tradeTarget.save()
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 下跌 买单已成交 订单ID: {self.tradeTarget.current_buy_order_no} Price: {trade.traded_price} Volume: {sfgrid_constants.grid_volume} 手续费: {trade.commission}")
print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
print(f' 当前持仓: {self.tradeTarget.current_position}')
print(f' 网格坐标: {self.tradeTarget.grid_index}')
else:
# 打印订单信息和订单状态
print(f'|- 非策略内部订单,或订单状态不满足监控条件 {trade.order_id} {trade.stock_code}-{trade.instrument_name} {trade.commission}')
finally:
print(f' |- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}]:release lock')
self.dataUpdateLock.release()
print(f' |- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}]:END')
+3
View File
@@ -0,0 +1,3 @@
# 删除交易标的事件
EventTradeTargetUpdate = "trade_target_update"
EventTradeTargetDeleted = "trade_target_deleted"
+44
View File
@@ -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])
+223
View File
@@ -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
+985
View File
@@ -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("错误", "请输入有效的数字")
+149
View File
@@ -0,0 +1,149 @@
# 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
# 保存配置
try:
sdConstants.save_config(mini_qmt_path.replace('\\', '/'), account_number)
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()
-26
View File
@@ -1,26 +0,0 @@
from peewee import SqliteDatabase, Model, CharField, IntegerField, FloatField, BooleanField
# 连接到SQLite数据库
db = SqliteDatabase('example.db')
# 定义基础模型类
class BaseModel(Model):
class Meta:
database = db
# 定义Target类,对应targets表
class TradeTarget(BaseModel):
stock_code = CharField(unique=True)
stock_name = CharField()
current_position = IntegerField()
grid_index = IntegerField()
last_trade_price = FloatField()
current_buy_price = FloatField()
current_buy_order_no = CharField(default='')
current_sell_price = FloatField()
current_sell_order_no = CharField(default='')
status = IntegerField(default=0) # 0表示新标的,1表示已建初始仓,正常交易中
enabled = BooleanField(default=False) # 是否启动交易线程
def targetName(self):
return f'{self.stock_name}[{self.stock_code}]'
+1 -35
View File
@@ -1,12 +1,5 @@
from typing import Any
import sfgrid_constants
import xtquant.xtconstant as xtconstant
from xtquant import xtdata, xttrader
from xtquant.xttype import StockAccount, XtOrder, XtPosition
import datetime
def is_trading_time():
"""
判断当前时间是否在周一至周五的9:30~11:30或13:00~15:00时间段内
@@ -40,30 +33,3 @@ def is_trading_time():
return False
def getInstrumentName(stock_code):
# print(f"getInstrumentName: 获取标的名称 {stock_code}")
detail = xtdata.get_instrument_detail(stock_code, False)
return detail['InstrumentName']
def getStockPosition(stock_code: str, xt_trader: xttrader.XtQuantTrader, account: StockAccount):
volume = 0
positions = xt_trader.query_stock_positions(account)
if positions:
for temp in positions:
pos:XtPosition = temp
if pos.stock_code == stock_code:
volume = pos.volume
break
return volume
def minPosition(gridIndex:int):
return sfgrid_constants.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:
return []
orders = xt_trader.query_stock_orders(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
BIN
View File
Binary file not shown.
+259
View File
@@ -0,0 +1,259 @@
from kuanke.wizard import *
from jqdata import *
import pandas as pd
import numpy as np
# ==================== 初始化 ====================
def initialize(context):
set_params(context)
# 开启防未来函数
set_option('avoid_future_data', True)
# 用真实价格交易
set_option('use_real_price', True)
# 过滤order中低于error级别的日志
log.set_level('order', 'error')
log.set_level('system', 'error')
log.set_level('strategy', 'debug')
set_benchmark('000001.XSHG')
set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0002, close_commission=0.0002, min_commission=5), type='stock')
set_slippage(FixedSlippage(0.01))
run_daily(before_trading, '9:30')
# -------------------- 参数设置 --------------------
def set_params(context):
context.max_price = 6
context.min_price = 5.01
context.grid_base_min = 1 # 最小价格
context.grid_base_max = 5 # 建仓价格
context.grid_interval = 0.5 # 下跌n元加仓
context.profit_target = 0.5 # 上涨n元清仓
context.min_stocks = 10
context.max_stocks = 25
context.base_max_stocks = 25
context.max_layers = 7
context.base_position_pct = 0.15
context.max_position_pct = 0.15
context.target_usage = 0.98
context.reserve_ratio = 0.02
context.first_round_max = 10
context.add_batch_size = 3
context.add_cash_threshold = 0.4
g.stock_pool = []
g.grid_info = {}
g.monitoring_stocks = set()
g.first_round_done = False
# ==================== 盘前 ====================
def before_trading(context):
january_clear(context)
if context.current_dt.month == 1:
return
stock_pool = get_stock_pool(context)
g.stock_pool = stock_pool
g.monitoring_stocks.update([s for s in stock_pool if s not in g.grid_info])
g.first_round_done = len(g.grid_info) >= context.first_round_max
# -------------------- 股票池 --------------------
def get_stock_pool(context):
# 1. 全部 A 股(不含退市)
df_sec = get_all_securities(types=['stock'], date=context.previous_date)
codes = list(df_sec.index)
# 2. 过滤 ST、科创板、北交所
def is_valid(code):
name = df_sec.loc[code, 'display_name']
if 'ST' in name or '退' in name or 'st' in name:
return False
if code.startswith('688'): # 科创板
return False
if code.startswith('83') or code.startswith('87') or code.startswith('9'): # 北交所
return False
return True
codes = [c for c in codes if is_valid(c)]
if not codes:
return []
# 3. 过滤停牌 & 价格区间
try:
price_df = get_price(codes,
end_date=context.current_dt,
count=1,
fields=['pre_close'],
panel=False)
if price_df is None or price_df.empty:
return []
# 过滤价格区间
price_df = price_df[
(price_df['pre_close'].notna()) &
(price_df['pre_close'] >= context.min_price) &
(price_df['pre_close'] <= context.max_price)
]
valid_codes = price_df['code'].tolist()
except Exception as e:
log.error(f"获取价格数据失败: {e}")
return []
if not valid_codes:
return []
# 4. 过滤停牌(开盘价缺失)
try:
open_df = get_price(valid_codes,
end_date=context.current_dt,
count=1,
fields=['open'],
panel=False)
if open_df is None or open_df.empty:
return []
# 过滤掉开盘价为空的股票
open_df = open_df[open_df['open'].notna()]
final_codes = open_df['code'].tolist()
except Exception as e:
log.error(f"获取开盘价数据失败: {e}")
return []
return final_codes
# -------------------- 一月清仓 --------------------
def january_clear(context):
if context.current_dt.month == 1:
log.info("进入1月,执行年度清仓...")
for stock in list(context.portfolio.positions.keys()):
order_target(stock, 0)
if stock in g.grid_info:
del g.grid_info[stock]
g.monitoring_stocks.add(stock)
# ==================== 盘中 ====================
def handle_data(context, data):
if context.current_dt.month == 1:
return
manage_positions(context, data)
usage = (context.portfolio.total_value - context.portfolio.available_cash) / context.portfolio.total_value
dynamic_max = get_dynamic_max_stocks(context)
if len(g.grid_info) < dynamic_max and usage < context.target_usage:
try_build_new(context, data)
# -------------------- 动态上限 --------------------
def get_dynamic_max_stocks(context):
return context.max_stocks if g.first_round_done else context.first_round_max
# -------------------- 建仓 --------------------
def try_build_new(context, data):
position_pct = context.max_position_pct
dynamic_max = get_dynamic_max_stocks(context)
count = 0
for stock in list(g.monitoring_stocks):
if len(g.grid_info) >= dynamic_max or count >= 3:
break
price = data[stock].close
if context.grid_base_min <= price <= context.grid_base_max:
total_value = context.portfolio.total_value
stock_amount = total_value * position_pct
grid = GridInfo(price, stock_amount, context.max_layers, context.grid_interval, context.profit_target)
layer_amount = grid.get_layer_amount(0)
buy_amount = int(layer_amount / price / 100) * 100
if buy_amount > 0:
order(stock, buy_amount)
grid.add_position(price, buy_amount, 0)
g.grid_info[stock] = grid
g.monitoring_stocks.discard(stock)
count += 1
log.info(f"[建仓] {stock} 价格{price:.2f} 数量{buy_amount}")
# -------------------- 管理持仓 --------------------
def manage_positions(context, data):
for stock, grid in list(g.grid_info.items()):
price = data[stock].close
# 止盈
sellable = grid.get_sellable_positions(price)
if sellable:
for idx, pos in reversed(sellable):
order(stock, -pos['amount'])
grid.remove_position(idx)
profit = (price - pos['price']) * pos['amount']
log.info(f"[止盈] {stock} 盈利{profit:.2f}")
# 加仓
layer = grid.should_add_layer(price)
if layer is not None:
layer_amount = grid.get_layer_amount(layer)
buy_amount = int(layer_amount / price / 100) * 100
if buy_amount > 0:
order(stock, buy_amount)
grid.add_position(price, buy_amount, layer)
log.info(f"[加仓] {stock} 层级{layer} 数量{buy_amount}")
else:
log.info(f"[加仓失败] {stock} 层级{layer} 金额不足")
# 清仓
if len(grid.positions) == 0:
del g.grid_info[stock]
g.monitoring_stocks.add(stock)
log.info(f"[清仓] {stock}")
# ==================== 盘后 ====================
def after_trading_end(context):
log.info(f"持仓数:{len(g.grid_info)},监控数:{len(g.monitoring_stocks)}")
# ==================== 网格类 ====================
class GridInfo:
def __init__(self, base_price, total_amount, max_layers, interval, profit_target):
self.base_price = float(base_price)
self.total_amount = float(total_amount)
self.max_layers = int(max_layers)
self.interval = float(interval)
self.profit_target = float(profit_target)
self.layer_prices = {i: base_price - i * interval for i in range(self.max_layers)}
self.layer_weights = self._calc_weights()
self.positions = []
def _calc_weights(self):
weights = {i: 1.0 + 0.05 * i for i in range(self.max_layers)}
total = sum(list(weights.values()))
return {k: v / total for k, v in weights.items()}
def get_layer_amount(self, layer):
return self.total_amount * self.layer_weights[layer]
def add_position(self, price, amount, layer):
self.positions.append({'price': price, 'amount': amount, 'layer': layer})
def get_sellable_positions(self, current_price):
return [(i, p) for i, p in enumerate(self.positions) if current_price >= p['price'] + self.profit_target]
def remove_position(self, index):
return self.positions.pop(index)
def should_add_layer(self, current_price):
for layer in range(self.max_layers):
target = self.layer_prices[layer]
diff = abs(current_price - target)
# 获取该层级的所有持仓
layer_positions = [p for p in self.positions if p['layer'] == layer]
has_position = len(layer_positions) > 0
if diff <= 0.1 and not has_position:
return layer
return None
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

-27
View File
@@ -1,27 +0,0 @@
from typing import List
import configparser
# miniQMTPath = r'D:\\Programs\\DTQMT_MN\\userdata_mini' # miniQMT软件的安装路径
miniQMTPath = r'D:\\Programs\\DTQMT\\userdata_mini' # miniQMT软件的安装路径
# miniQMTPath = ''
# grid_price = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1] # 网格价格设置,从高到低
grid_price:List[float] = [] # 网格价格设置,从高到低
grid_volume:int = 100 # 每个网格的交易手数
account_no:str = '99082560'
# account_no:str = '89009170' # 交易账号
max_enabled_targets:int = 10
def initConfig():
global miniQMTPath, grid_price, grid_volume, account_no, max_enabled_targets
config = configparser.ConfigParser()
config.read('config.ini')
miniQMTPath = config.get('config','miniQMTPath')
print(f'QMTPath: {miniQMTPath}')
str_list = config.get('config','grid_price').split(',')
grid_price = [float(item) for item in str_list]
print(f'网格设置:{grid_price}')
grid_volume = config.getint('config','grid_volume')
# account_no = config.get('config','account_no')
print(f'账号: {account_no}')
max_enabled_targets = config.getint('config','max_enabled_targets')
print(f'最大启用目标数: {max_enabled_targets}')
+10 -80
View File
@@ -1,82 +1,12 @@
# coding:utf-8
from mimetypes import init
import sys
sys.stdout.reconfigure(encoding='utf-8') # 设置标准输出编码为UTF-8 # type: ignore
from core import strategy_db
# from core.main_controller import SFGridController
# import core.util as util
import sfgrid_constants as sdConstants
# from xtquant import xtdata
import ui
import tkinter as tk
from core.main_entry import MainEntry
def interact():
"""执行后进入repl模式"""
import code
code.InteractiveConsole(locals=globals()).interact()
# def startMarketData():
# ctrl.startMarketData()
# def stopMarketData():
# ctrl.stopMarketData()
# def pool():
# ctrl.print_pool()
# def addTarget(stock_code):
# ctrl.add_trade_target(stock_code)
# def delTarget(index:int):
# ctrl.del_trade_target(index)
# def accountInfo():
# ctrl.print_account_info()
# def positionInfo():
# ctrl.print_position_info()
# def startTrade(index:int):
# ctrl.start_stock_trade(index)
# def pauseTrade(index:int):
# ctrl.pause_stock_trade(index)
# def stockTradeCtrl(index: int):
# return ctrl.stock_trade_ctrl[ctrl.instrument_pool[index].stock_code]
def help():
print("基础指令:")
print(" ===================================================")
print(" startMarketData() - 启动市场数据接收")
print(" stopMarketData() - 停止市场数据接收\n")
print(" pool() - 打印标的池信息")
print(" addTarget(stock_code) - 添加交易标的")
print(" delTarget(index) - 删除交易标的\n")
print(" accountInfo() - 打印账户信息")
print(" positionInfo() - 打印持仓信息\n")
print(" startTrade(index) - 启动标的交易")
print(" pauseTrade(index) - 暂停标的交易")
print(" ===================================================")
print("内部指令:")
print(" stockTradeCtrl(index) - 获取标的交易控制器")
print(" ctrl - 访问控制器实例")
if __name__ == '__main__':
strategy_db.db.connect()
strategy_db.db.create_tables([strategy_db.TradeTarget])
print('- [成功]数据库模块初始化')
targets = strategy_db.TradeTarget.select()
app = ui.TradeTargetUI(trade_targets=targets)
app.run()
# sdConstants.initConfig()
# print(f'{sdConstants.account_no} : {sdConstants.miniQMTPath}')
# ctrl: SFGridController = SFGridController(sdConstants.account_no, sdConstants.miniQMTPath)
# if ctrl.inited:
# interact()
# else:
# print("控制器初始化失败")
# 这是应用的启动入口程序,负责初始化并启动主窗口。
# 它创建一个Tkinter根窗口,实例化主窗口类MainBoardWindow
# 并调用其run方法启动主事件循环。
if __name__ == "__main__":
import tkinter as tk
root = tk.Tk()
app = MainEntry(root)
app.run()
+38
View File
@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['starter.py'],
pathex=[],
binaries=[],
datas=[('config.ini', '.'), ('xtquant/xtdata.ini', 'xtquant')], # 明确包含配置文件和xtdata.ini
hiddenimports=['brotli', 'brotli.encoding'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=['PyQt5', 'PyQt6', 'PySide2', 'PySide6', 'matplotlib', 'numpy', 'pandas', 'jupyter', 'notebook', 'ipython'], # 排除不必要的包
noarchive=False,
optimize=2, # 启用最高级别优化
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='神之一手',
debug=False,
bootloader_ignore_signals=False,
strip=True, # 去除调试符号
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='logo.png' # 添加图标文件
)
-667
View File
@@ -1,667 +0,0 @@
import random
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from typing import List, Optional
from datetime import datetime
from core.strategy_db import TradeTarget
import configparser
import sfgrid_constants
class TradeTargetUI:
def __init__(self, trade_targets: Optional[List[TradeTarget]] = None):
if trade_targets is not None:
self.trade_targets = trade_targets
else:
self.trade_targets = []
self.root = tk.Tk()
self.root.title("三疯交易系统")
self.root.geometry("1200x700")
# 创建界面
self.create_ui()
def create_ui(self):
"""创建UI界面"""
# 创建菜单栏
self.create_menu_bar()
# 主框架
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 表格区域
self.create_tables_area(main_frame)
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.root.quit)
def create_tables_area(self, parent):
"""创建表格区域"""
# 上方交易标的区域
trade_frame = ttk.LabelFrame(parent, text="交易标的详情", padding=10)
trade_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
# 创建交易标的表格
self.create_trade_target_table(trade_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)
def create_trade_target_table(self, parent):
"""创建交易标的表格"""
# 创建工具栏
toolbar_frame = ttk.Frame(parent)
toolbar_frame.pack(fill=tk.X, pady=(0, 10))
# 工具栏按钮
ttk.Button(toolbar_frame, text="▶️ 启动交易",
command=self.start_selected_trade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="⏸ 暂停交易",
command=self.pause_selected_trade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text=" 添加标的",
command=self.add_trade_target, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🗑 删除标的",
command=self.delete_selected_trade, width=12).pack(side=tk.LEFT, padx=2)
# 添加分隔符
ttk.Separator(toolbar_frame, orient='vertical').pack(side=tk.LEFT, fill=tk.Y, padx=10)
# 日志显示/隐藏按钮
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.Separator(parent, orient='horizontal').pack(fill=tk.X, pady=5)
columns = ("ID",
"股票代码", "股票名称", "持仓数量", "网格索引",
"最新成交价", "计划买入价", "买入订单号", "计划卖出价", "卖出订单号",
"启用状态", "交易状态"
)
self.trade_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
# 专业化的列配置
column_configs = {
"ID": (50, tk.CENTER),
"股票代码": (90, tk.CENTER),
"股票名称": (100, tk.CENTER),
"持仓数量": (90, tk.CENTER),
"网格索引": (80, tk.CENTER),
"最新成交价": (100, tk.CENTER),
"计划买入价": (100, tk.CENTER),
"买入订单号": (100, tk.CENTER),
"计划卖出价": (100, tk.CENTER),
"卖出订单号": (100, tk.CENTER),
"启用状态": (80, tk.CENTER),
"交易状态": (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 get_status_indicator(self, target: TradeTarget) -> str:
"""获取状态指示器(带颜色色块的文本)"""
if target.status == 1:
# 绿色圆点表示交易中
return "🟢 已建仓"
elif target.status == 0:
# 黄色圆点表示暂停
return "🟡 未建仓"
else:
return "🔴 错误状态"
def get_trade_status_indicator(self, status: int) -> str:
"""获取交易状态指示器"""
if status == 1:
return "🟢 策略运行"
else:
return "🟡 策略暂停"
def populate_trade_table(self):
"""填充交易标的表格数据"""
for temp in self.trade_targets:
target: TradeTarget = temp
values = [
target.id, # type: ignore
target.stock_code,
target.stock_name,
target.current_position,
target.grid_index,
f"{target.last_trade_price:.2f}",
f"{target.current_buy_price:.2f}",
target.current_buy_order_no,
f"{target.current_sell_price:.2f}",
target.current_sell_order_no,
self.get_status_indicator(target),
self.get_trade_status_indicator(target.status) # 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": ("时间", 120),
"level": ("级别", 60),
"message": ("消息", 200)
}
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", "系统启动成功"),
("2024-01-15 10:31:22", "DEBUG", "加载交易标的: 5个"),
("2024-01-15 10:32:45", "INFO", "000001 - 网格交易线程启动"),
("2024-01-15 10:33:10", "WARNING", "601318 - 未启用交易"),
("2024-01-15 10:34:30", "ERROR", "300750 - 订单提交失败"),
("2024-01-15 10:35:18", "INFO", "600036 - 买入订单创建成功"),
("2024-01-15 10:36:05", "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):
"""获取状态文本"""
status_map = {
0: "新标的",
1: "交易中"
}
return status_map.get(status, "未知")
def on_table_double_click(self, event):
"""表格双击事件"""
selected = self.trade_table.selection()
if selected:
item = selected[0]
values = self.trade_table.item(item)['values']
self.add_log("DEBUG", f"双击查看详情: {values[0]} - {values[1]}")
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 target in self.trade_targets:
if target.id == target_id: # type: ignore
return target
return None
def start_selected_trade(self):
"""启动选中的交易"""
target = self.get_selected_target()
if not target:
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:
target.enabled = True # type: ignore
self.add_log("INFO", f"已启动交易: {target.stock_code} - {target.stock_name}")
self.refresh_table()
messagebox.showinfo("启动成功", f"已启动 {target.stock_code} ({target.stock_name}) 的交易")
def pause_selected_trade(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:
target.enabled = False # type: ignore
self.add_log("INFO", f"已暂停交易: {target.stock_code} - {target.stock_name}")
self.refresh_table()
messagebox.showinfo("暂停成功", f"已暂停 {target.stock_code} ({target.stock_name}) 的交易")
def delete_selected_trade(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:
try:
self.trade_targets.remove(target)
self.add_log("WARNING", f"已删除交易标的: {target.stock_code} - {target.stock_name}")
self.refresh_table()
messagebox.showinfo("删除成功", f"已删除 {target.stock_code} ({target.stock_name})")
except Exception as e:
self.add_log("ERROR", f"删除失败: {str(e)}")
messagebox.showerror("删除失败", f"删除交易标的时出错:{str(e)}")
def add_trade_target(self):
"""添加新的交易标的"""
# TODO: 实现添加交易标的的对话框
messagebox.showinfo("提示", "添加交易标的功能待实现")
self.add_log("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="📋 隐藏日志")
def refresh_table(self):
"""刷新表格数据"""
# 清空表格
for item in self.trade_table.get_children():
self.trade_table.delete(item)
# 重新填充
self.populate_trade_table()
def add_log(self, level, message):
"""添加日志记录"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_table.insert('', 0, values=(timestamp, level, message))
def system_settings(self):
"""系统设置"""
settings_window = tk.Toplevel(self.root)
settings_window.title("网格交易系统配置")
# 设置窗口大小
window_width = 700
window_height = 600
# 先设置为模态窗口
settings_window.transient(self.root)
# 确保主窗口完全初始化
self.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()
# 计算设置窗口相对于主窗口的居中位置
x = main_x + (main_width - window_width) // 2
y = main_y + (main_height - window_height) // 2
# 设置窗口大小和位置
settings_window.geometry(f"{window_width}x{window_height}+{x}+{y}")
settings_window.resizable(False, False)
# 设置为模态窗口
settings_window.grab_set()
# 添加底部按钮区域(先创建,确保固定在底部)
button_frame = ttk.Frame(settings_window)
button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=20, pady=10)
# 创建选项卡(在按钮之后创建,填充剩余空间)
notebook = ttk.Notebook(settings_window)
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=(10, 0))
# 基础设置
basic_frame = ttk.Frame(notebook)
notebook.add(basic_frame, text="基础设置")
# 高级设置
advanced_frame = ttk.Frame(notebook)
notebook.add(advanced_frame, text="高级设置")
# 读取当前配置
config = configparser.ConfigParser()
config.read('config.ini')
# 创建输入框字典用于保存引用
entries = {}
# 网格价格计算参数
grid_params = {}
# 添加网格价格设置(特殊处理)
grid_price_frame = ttk.LabelFrame(basic_frame, text="网格价格设置", padding=15)
grid_price_frame.pack(fill=tk.X, padx=20, pady=10)
# 基准价格
base_price_row = ttk.Frame(grid_price_frame)
base_price_row.pack(fill=tk.X, pady=5)
ttk.Label(base_price_row, text="基准价格:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
base_price_entry = ttk.Entry(base_price_row, width=15, font=('Arial', 10))
base_price_entry.insert(0, "10.0")
base_price_entry.pack(side=tk.LEFT, padx=5)
ttk.Label(base_price_row, text="", foreground='gray', font=('Arial', 9)).pack(side=tk.LEFT)
grid_params['base_price'] = base_price_entry
# 网格类型
grid_type_row = ttk.Frame(grid_price_frame)
grid_type_row.pack(fill=tk.X, pady=5)
ttk.Label(grid_type_row, text="网格类型:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
grid_type_var = tk.StringVar(value="金额差")
ttk.Radiobutton(grid_type_row, text="百分比", variable=grid_type_var,
value="百分比", command=lambda: on_grid_type_change()).pack(side=tk.LEFT, padx=5)
ttk.Radiobutton(grid_type_row, text="金额差", variable=grid_type_var,
value="金额差", command=lambda: on_grid_type_change()).pack(side=tk.LEFT, padx=5)
grid_params['grid_type'] = grid_type_var
# 网格大小
grid_size_row = ttk.Frame(grid_price_frame)
grid_size_row.pack(fill=tk.X, pady=5)
ttk.Label(grid_size_row, text="网格大小:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
grid_size_entry = ttk.Entry(grid_size_row, width=15, font=('Arial', 10))
grid_size_entry.insert(0, "1.0")
grid_size_entry.pack(side=tk.LEFT, padx=5)
grid_size_unit_label = ttk.Label(grid_size_row, text="", foreground='gray', font=('Arial', 9))
grid_size_unit_label.pack(side=tk.LEFT)
grid_params['grid_size'] = grid_size_entry
grid_params['grid_size_unit_label'] = grid_size_unit_label
# 网格类型改变时更新单位
def on_grid_type_change():
if grid_type_var.get() == "百分比":
grid_size_unit_label.config(text="%")
grid_size_entry.delete(0, tk.END)
grid_size_entry.insert(0, "1.0")
else:
grid_size_unit_label.config(text="")
grid_size_entry.delete(0, tk.END)
grid_size_entry.insert(0, "1.0")
update_preview()
# 上方网格数量
upper_grid_row = ttk.Frame(grid_price_frame)
upper_grid_row.pack(fill=tk.X, pady=5)
ttk.Label(upper_grid_row, text="上方网格数量:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
upper_grid_entry = ttk.Entry(upper_grid_row, width=15, font=('Arial', 10))
upper_grid_entry.insert(0, "1")
upper_grid_entry.pack(side=tk.LEFT, padx=5)
ttk.Label(upper_grid_row, text="", foreground='gray', font=('Arial', 9)).pack(side=tk.LEFT)
grid_params['upper_count'] = upper_grid_entry
# 下方网格数量
lower_grid_row = ttk.Frame(grid_price_frame)
lower_grid_row.pack(fill=tk.X, pady=5)
ttk.Label(lower_grid_row, text="下方网格数量:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
lower_grid_entry = ttk.Entry(lower_grid_row, width=15, font=('Arial', 10))
lower_grid_entry.insert(0, "10")
lower_grid_entry.pack(side=tk.LEFT, padx=5)
ttk.Label(lower_grid_row, text="", foreground='gray', font=('Arial', 9)).pack(side=tk.LEFT)
grid_params['lower_count'] = lower_grid_entry
# 预览按钮和结果显示
preview_row = ttk.Frame(grid_price_frame)
preview_row.pack(fill=tk.X, pady=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_grid_entry.get())
lower_count = int(lower_grid_entry.get())
grid_type = grid_type_var.get()
prices = []
# 计算上方网格价格
for i in range(upper_count, 0, -1):
if grid_type == "百分比":
price = base_price * (1 + grid_size / 100 * i)
else: # 金额差
price = base_price + grid_size * i
prices.append(round(price, 3))
# 添加基准价格
prices.append(base_price)
# 计算下方网格价格
for i in range(1, lower_count + 1):
if grid_type == "百分比":
price = base_price * (1 - grid_size / 100 * i)
else: # 金额差
price = base_price - grid_size * i
# 确保价格不为负
if price >= 0:
prices.append(round(price, 3))
else:
break
return prices
except ValueError as e:
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 (base_price_entry, grid_size_entry, upper_grid_entry, lower_grid_entry):
entry_widget.bind("<KeyRelease>", lambda e: update_preview())
entry_widget.bind("<FocusOut>", lambda e: update_preview())
# 初始预览
update_preview()
ttk.Label(preview_row, textvariable=preview_result,
font=('Arial', 10)).pack(side=tk.LEFT, padx=10)
# 添加其他基础配置选项
other_basic_frame = ttk.LabelFrame(basic_frame, text="交易设置", padding=15)
other_basic_frame.pack(fill=tk.X, padx=20, pady=10)
other_basic_settings = [
("网格交易手数", "grid_volume", config.get('config', 'grid_volume'), "每个网格的交易手数"),
("最大启用目标数", "max_enabled_targets", config.get('config', 'max_enabled_targets'), "同时运行的最大标的数量")
]
for i, (label, key, default, tooltip) in enumerate(other_basic_settings):
frame = ttk.Frame(other_basic_frame)
frame.pack(fill=tk.X, pady=5)
label_widget = ttk.Label(frame, text=label + ":", width=15, font=('Arial', 10))
label_widget.pack(side=tk.LEFT)
entry = ttk.Entry(frame, width=15, font=('Arial', 10))
entry.insert(0, default)
entry.pack(side=tk.LEFT, padx=5)
entries[key] = entry
# 添加提示信息
tip_label = ttk.Label(frame, text=tooltip, font=('Arial', 9), foreground='gray')
tip_label.pack(side=tk.LEFT, padx=5)
# 添加高级设置选项
account_frame = ttk.LabelFrame(advanced_frame, text="账号设置", padding=15)
account_frame.pack(fill=tk.X, padx=20, pady=10)
# 交易账号
account_row = ttk.Frame(account_frame)
account_row.pack(fill=tk.X, pady=5)
ttk.Label(account_row, text="交易账号:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
account_entry = ttk.Entry(account_row, width=15, font=('Arial', 10))
account_entry.insert(0, config.get('config', 'account_no'))
account_entry.pack(side=tk.LEFT, padx=5)
entries['account_no'] = account_entry
ttk.Label(account_row, text="QMT交易账号", font=('Arial', 9), foreground='gray').pack(side=tk.LEFT, padx=5)
# QMT路径特殊处理 - 使用文件浏览器
qmt_path_frame = ttk.LabelFrame(advanced_frame, text="软件路径", padding=15)
qmt_path_frame.pack(fill=tk.X, padx=20, pady=10)
qmt_row = ttk.Frame(qmt_path_frame)
qmt_row.pack(fill=tk.X, pady=5)
ttk.Label(qmt_row, text="QMT路径:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
qmt_entry = ttk.Entry(qmt_row, width=30, font=('Arial', 10))
qmt_entry.insert(0, config.get('config', 'miniQMTPath'))
qmt_entry.pack(side=tk.LEFT, padx=5)
entries['miniQMTPath'] = qmt_entry
def browse_qmt_path():
"""打开文件夹浏览器选择QMT路径"""
initial_dir = qmt_entry.get() if qmt_entry.get() else "/"
folder_path = filedialog.askdirectory(
title="选择miniQMT安装路径",
initialdir=initial_dir
)
if folder_path:
qmt_entry.delete(0, tk.END)
qmt_entry.insert(0, folder_path)
ttk.Button(qmt_row, text="📁 浏览...", command=browse_qmt_path, width=10).pack(side=tk.LEFT, padx=5)
ttk.Label(qmt_row, text="miniQMT软件安装路径", font=('Arial', 9), foreground='gray').pack(side=tk.LEFT, padx=5)
# 定义保存和取消按钮的功能(button_frame已在上方创建)
def save_settings():
"""保存配置"""
try:
# 计算网格价格序列
grid_prices = calculate_grid_prices()
if not grid_prices:
messagebox.showerror("错误", "网格价格参数有误,请检查输入!")
return
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', 'max_enabled_targets', entries['max_enabled_targets'].get())
# 写入配置文件
with open('config.ini', 'w') as configfile:
config.write(configfile)
# 重新加载配置到内存中
sfgrid_constants.initConfig()
messagebox.showinfo("成功", f"配置已保存!\n网格价格序列: {grid_price_str}\n部分配置可能需要重启程序后生效。")
self.add_log("INFO", f"系统配置已更新 - 网格数量: {len(grid_prices)}")
settings_window.destroy()
except Exception as e:
messagebox.showerror("错误", f"保存配置失败:{str(e)}")
self.add_log("ERROR", f"保存配置失败: {str(e)}")
def cancel_settings():
"""取消设置"""
settings_window.destroy()
# 在button_frame中添加按钮
ttk.Button(button_frame, text="💾 保存配置", command=save_settings, width=15).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="❌ 取消", command=cancel_settings, width=15).pack(side=tk.LEFT, padx=5)
def run(self):
"""运行程序"""
self.root.mainloop()
# 使用示例
if __name__ == "__main__":
print("交易标的监控系统启动...")
print("功能说明:")
print(" - 左侧表格显示所有交易标的详细信息")
print(" - 右侧表格显示操作日志")
print(" - 底部五个功能按钮提供操作")
# 创建并运行界面
app = TradeTargetUI()
app.run()