Compare commits
56 Commits
UI
..
new_structure
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b916b5c44 | |||
| 5a26f5f7b3 | |||
| 66768cb359 | |||
| 988947aa1a | |||
| b435f12c49 | |||
| c59d29d52e | |||
| 6b3b1a1f76 | |||
| 60af627806 | |||
| 4787011177 | |||
| 81da3fe013 | |||
| c12e394b8e | |||
| 3ee47e95cf | |||
| e5b1d80139 | |||
| 5c40d5d64f | |||
| 1618cad5a0 | |||
| 64bdddde79 | |||
| 4c4c8730f2 | |||
| 0262bfc71b | |||
| d5fef7c0c1 | |||
| fcadcb86d2 | |||
| 59a88c4365 | |||
| c69b5bc1ae | |||
| f626545897 | |||
| f499d9a413 | |||
| 6cae413956 | |||
| 6810b55cbb | |||
| cd67c9ad60 | |||
| a18c7be7eb | |||
| f5d37eaa7e | |||
| 550126d060 | |||
| c03a4adb53 | |||
| 81d0131a7b | |||
| 1193dc2f69 | |||
| 8ab5d83b1a | |||
| 91d1cac384 | |||
| 2ca0ab65f0 | |||
| 5e64e93172 | |||
| 0d54f8b05a | |||
| ba9cd9a700 | |||
| 0dbd8e8dde | |||
| 54fd7c9545 | |||
| 662a1ea7c1 | |||
| c42648d1b4 | |||
| 7cfb433aaf | |||
| 1ec27bb52f | |||
| 7f0a73381d | |||
| c2f34d09e9 | |||
| 20a7453e8b | |||
| 3a137b6aee | |||
| 7733d6df32 | |||
| b7f9d29c54 | |||
| d988f5eb48 | |||
| 1ee8f0426e | |||
| 88bd0b17c9 | |||
| df0e9ecb22 | |||
| c77ff1c0ae |
@@ -1 +1,8 @@
|
||||
__pycache__/
|
||||
dist/
|
||||
example.db
|
||||
xtquant/
|
||||
starter.dist/starter.dll
|
||||
build/
|
||||
.vscode/
|
||||
example.db.bak
|
||||
|
||||
+2
-4
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1,6 @@
|
||||
import xtquant.xtconstant as xtconstant
|
||||
|
||||
OrderTypeBuy = f'{xtconstant.STOCK_BUY}' # 买
|
||||
OrderTypeSell = f'{xtconstant.STOCK_SELL}' # 卖
|
||||
OrderTypeInit = "0" # 建仓
|
||||
OrderTypeNone = "None"
|
||||
@@ -0,0 +1,4 @@
|
||||
import xtquant.xtconstant as xtconstant
|
||||
|
||||
HeatTypeUpStop = "UpStop" # 涨停
|
||||
HeatTypeDragonTiger = "DragonTiger" # 龙虎榜
|
||||
@@ -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])
|
||||
@@ -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
|
||||
@@ -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'})
|
||||
@@ -0,0 +1,7 @@
|
||||
from .eventbus import EventBus
|
||||
|
||||
# Pring Log
|
||||
EventPrintLog = "print_log" # 打印日志
|
||||
|
||||
# 创建事件总线实例
|
||||
loggerEBus = EventBus()
|
||||
@@ -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()
|
||||
@@ -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}')
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -0,0 +1,8 @@
|
||||
from qmt import QmtV
|
||||
from eventbus import marketDataEventBus
|
||||
|
||||
qmtv:QmtV = None
|
||||
|
||||
def init_qmtv():
|
||||
global qmtv
|
||||
qmtv = QmtV()
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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: 应用核心程序目录
|
||||
@@ -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')
|
||||
@@ -0,0 +1,3 @@
|
||||
# 删除交易标的事件
|
||||
EventTradeTargetUpdate = "trade_target_update"
|
||||
EventTradeTargetDeleted = "trade_target_deleted"
|
||||
@@ -0,0 +1,44 @@
|
||||
from peewee import CharField, IntegerField, FloatField, BooleanField
|
||||
|
||||
from core.database import BaseModel, db
|
||||
|
||||
|
||||
# 定义Target类,对应targets表
|
||||
class SFGridTradeTarget(BaseModel):
|
||||
stock_code = CharField(unique=True)
|
||||
stock_name = CharField()
|
||||
current_position = IntegerField()
|
||||
grid_index = IntegerField(default=0)
|
||||
init_price = FloatField(null=True) # 建仓成本
|
||||
grid_match_count = IntegerField(default=0)
|
||||
grid_total_profit = FloatField(default=0.0)
|
||||
status = IntegerField(default=0) # -1表示新标的,未完成交易配置,0表示新标的,已完成交易配置,1表示已建初始仓,正常交易中
|
||||
enabled = BooleanField(default=False) # 是否启动交易线程
|
||||
|
||||
grid_start_price = FloatField(default=10.0) # 基线价格
|
||||
grid_size = FloatField(default=0.1) # 网格价位差
|
||||
grid_volume = IntegerField(default=100) # 网格交易量
|
||||
grid_upper_count = IntegerField(default=1) # 基线价格上方网格数
|
||||
grid_lower_count = IntegerField(default=10) # 基线价格下方网格数
|
||||
|
||||
def targetName(self):
|
||||
return f'{self.stock_code}-{self.stock_name}'
|
||||
|
||||
def getPriceGrid(self) -> list:
|
||||
self.priceGrid: list = []
|
||||
# 网格大小,数量
|
||||
if self.priceGrid is None or len(self.priceGrid) == 0:
|
||||
for i in range(self.grid_upper_count): # type: ignore
|
||||
upperPrice = self.grid_start_price + (self.grid_upper_count - i) * self.grid_size
|
||||
self.priceGrid.append(round(upperPrice, 3))
|
||||
|
||||
self.priceGrid.append(self.grid_start_price)
|
||||
|
||||
for i in range(self.grid_lower_count): # type: ignore 5
|
||||
lowerPrice = self.grid_start_price - (i + 1) * self.grid_size
|
||||
self.priceGrid.append(round(lowerPrice, 3))
|
||||
|
||||
return self.priceGrid
|
||||
|
||||
|
||||
db.create_tables([SFGridTradeTarget])
|
||||
@@ -0,0 +1,223 @@
|
||||
from core.logger import LogLevel, PrintLog
|
||||
from core.qmt import qmtv
|
||||
from core.sfgrid import bus_events
|
||||
from core.sfgrid.bus_events import EventTradeTargetUpdate
|
||||
import core.sfgrid.model as model
|
||||
from core.eventbus import event_bus
|
||||
from core.constants import OrderTypeBuy, OrderTypeSell, OrderTypeInit
|
||||
|
||||
from xtquant import xtconstant
|
||||
from xtquant.xttype import XtOrderResponse, XtTrade
|
||||
import threading
|
||||
import core.eventbus as eBus
|
||||
|
||||
|
||||
class SFGridStrategy:
|
||||
|
||||
def __init__(self, tradeTarget: model.SFGridTradeTarget):
|
||||
self.tradeTarget:model.SFGridTradeTarget = tradeTarget
|
||||
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
|
||||
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
|
||||
self.todayUpStopPrice=qmtv.dailyUpStop(tradeTarget.stock_code) # type: ignore
|
||||
self.todayDownStopPrice=qmtv.dailyDownStop(tradeTarget.stock_code) # type: ignore
|
||||
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}初始化: 停涨价 {self.todayUpStopPrice:.3f}, 停跌价 {self.todayDownStopPrice:.3f}')
|
||||
self.orderGrid = {} # grid index, order_seq | order_id
|
||||
self.loadExistOrders()
|
||||
self.enabledTrading(tradeTarget.enabled) # type: ignore
|
||||
self.dataUpdateLock = threading.Lock()
|
||||
|
||||
def loadExistOrders(self):
|
||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||
for order in orders:
|
||||
if order.strategy_name != self.getName():
|
||||
continue
|
||||
gridIdx = int(order.order_remark.split(',')[1])
|
||||
self.orderGrid[gridIdx] = order.order_id
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 初始化: 加载现有订单, grid-{gridIdx} order_id:{self.orderGrid[gridIdx]}')
|
||||
|
||||
def printPendingOrder(self):
|
||||
for idx, order_id in self.orderGrid.items():
|
||||
PrintLog(LogLevel.DEBUG, f" {idx} : {order_id}")
|
||||
|
||||
def onMarketActiveSwitch(self, isActive: bool):
|
||||
if isActive and self.tradeTarget.enabled:
|
||||
self.refreshGridOrder()
|
||||
|
||||
def refreshGridOrder(self): # 下网格单
|
||||
if not qmtv.isMarketActive or not self.tradeTarget.enabled:
|
||||
PrintLog(LogLevel.INFO, f'|- 市场 {qmtv.isMarketActive}, 策略 {self.getName()} {self.tradeTarget.enabled}, 不下单')
|
||||
return
|
||||
|
||||
currentIdx:int = 0
|
||||
|
||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||
|
||||
if self.tradeTarget.status == 0 and len([order for order in orders if order.order_remark == f'{OrderTypeInit},1,{self.tradeTarget.stock_code}']) == 0: # status == 0 表示已配置好交易参数,且不存在执行中的建仓单
|
||||
price = self.tradeTarget.getPriceGrid()[0]
|
||||
remark = f'{OrderTypeInit},1,{self.tradeTarget.stock_code}'
|
||||
tmpOrderSeq = qmtv.orderAsync(
|
||||
str(self.tradeTarget.stock_code),
|
||||
self.tradeTarget.grid_volume,
|
||||
xtconstant.STOCK_BUY,
|
||||
price,
|
||||
xtconstant.FIX_PRICE,
|
||||
remark, # remark # type: ignore
|
||||
self.getName(), # strategy_name
|
||||
)
|
||||
self.orderGrid[1] = tmpOrderSeq # seq
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 初始化: 建仓单,建仓价: {price:.3f}')
|
||||
elif self.tradeTarget.status == 1: # 下网格单
|
||||
currentIdx = self.tradeTarget.grid_index # type: ignore
|
||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||
|
||||
# 向上下一单,向下下一单
|
||||
if currentIdx > 0: # 可以下空单
|
||||
sellIdx = currentIdx - 1
|
||||
sellPrice = self.tradeTarget.getPriceGrid()[sellIdx]
|
||||
remark = f'{OrderTypeSell},{sellIdx},{self.tradeTarget.stock_code}'
|
||||
if len([order for order in orders if order.order_remark == remark]) == 0: # 网格节点没有卖单,下单
|
||||
# 不存在策略内同价位订单,下单
|
||||
tmpOrderSeq = qmtv.orderAsync(
|
||||
str(self.tradeTarget.stock_code),
|
||||
self.tradeTarget.grid_volume,
|
||||
xtconstant.STOCK_SELL,
|
||||
sellPrice,
|
||||
xtconstant.FIX_PRICE,
|
||||
remark, # remark # type: ignore
|
||||
self.getName(), # strategy_name
|
||||
)
|
||||
self.orderGrid[sellIdx] = tmpOrderSeq # seq
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 下空单,价格: {sellPrice:.3f}')
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已存在同价位空单,跳过下单')
|
||||
if currentIdx < len(self.tradeTarget.getPriceGrid()) - 1: # 可以下多单
|
||||
print(f'length: {len(self.tradeTarget.getPriceGrid())}, currentIdx = {currentIdx}')
|
||||
buyIdx = currentIdx + 1
|
||||
buyPrice = self.tradeTarget.getPriceGrid()[buyIdx]
|
||||
remark = f'{OrderTypeBuy},{buyIdx},{self.tradeTarget.stock_code}'
|
||||
if len([order for order in orders if order.order_type == xtconstant.STOCK_BUY and order.price == buyPrice]) == 0:
|
||||
tmpOrderSeq = qmtv.orderAsync(
|
||||
str(self.tradeTarget.stock_code),
|
||||
self.tradeTarget.grid_volume,
|
||||
xtconstant.STOCK_BUY,
|
||||
buyPrice,
|
||||
xtconstant.FIX_PRICE,
|
||||
remark, # remark # type: ignore
|
||||
self.getName(), # strategy_name
|
||||
)
|
||||
self.orderGrid[buyIdx] = tmpOrderSeq # seq
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 下多单,价格: {buyPrice:.3f}')
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已存在同价位多单,跳过下单')
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已过下边界,停止多单交易')
|
||||
|
||||
def deleteTradeTarget(self, tradeTarget:model.SFGridTradeTarget):
|
||||
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}信息删除: START')
|
||||
self.dataUpdateLock.acquire()
|
||||
try:
|
||||
tradeTarget.delete_instance()
|
||||
event_bus.publish(bus_events.EventTradeTargetDeleted, tradeTarget)
|
||||
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}信息删除: END')
|
||||
finally:
|
||||
self.dataUpdateLock.release()
|
||||
|
||||
def enabledTrading(self, enabled: bool) -> model.SFGridTradeTarget:
|
||||
self.tradeTarget.enabled = enabled # type: ignore
|
||||
|
||||
if enabled:
|
||||
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易启动, 持仓量:{self.tradeTarget.current_position}")
|
||||
if self.tradeTarget.status == 0: # 未建仓
|
||||
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}初始状态, 设置网格序号 1,")
|
||||
self.tradeTarget.grid_index = 1 # pyright: ignore[reportAttributeAccessIssue]
|
||||
else: # 已建仓
|
||||
# 交易阶段,检查仓位,检查现有订单
|
||||
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 无需建初始仓 当前仓位: {self.tradeTarget.current_position} 状态: {self.tradeTarget.status}")
|
||||
minRequirePosition:int = self.tradeTarget.grid_volume * int(self.tradeTarget.grid_index) # type: ignore
|
||||
if minRequirePosition <= int(self.tradeTarget.current_position): # type: ignore
|
||||
PrintLog(LogLevel.INFO, f' |- 仓位检查: 持仓需求充足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}')
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f' |- 仓位检查: 持仓需求不足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}, 交易启动失败')
|
||||
self.tradeTarget.enabled = False # type: ignore
|
||||
self.refreshGridOrder()
|
||||
else:
|
||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||
for order in orders:
|
||||
qmtv.xttrader.cancel_order_stock_async(qmtv.account, order.order_id)
|
||||
if len(orders) > 0:
|
||||
PrintLog(LogLevel.INFO, f' |- 取消未成交订单 {len(orders)}')
|
||||
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易监控暂停")
|
||||
|
||||
self.saveProxy()
|
||||
return self.tradeTarget
|
||||
|
||||
def isEnabled(self) -> bool:
|
||||
print(f'|- 检查交易状态[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - {self.tradeTarget.enabled}')
|
||||
return bool(self.tradeTarget.enabled) # 修复返回类型问题
|
||||
|
||||
def onOrderCreateAsync(self, response:XtOrderResponse): # 下单成功回调,更新orderID到 self.orderGrid
|
||||
remark = response.order_remark.split(',')
|
||||
stockCode = remark[2] # 从remark中获取stockCode
|
||||
if response.strategy_name != self.getName() or len(remark) < 3 or self.tradeTarget.stock_code != stockCode:
|
||||
return
|
||||
self.dataUpdateLock.acquire()
|
||||
try:
|
||||
gridIdx = remark[1] # 从remark中获取gridIdx
|
||||
PrintLog(LogLevel.INFO, f"委托创建通知 onOrderCreateAsync[{self.tradeTarget.targetName()}]: {response.order_id}")
|
||||
self.orderGrid[gridIdx] = response.order_id
|
||||
PrintLog(LogLevel.INFO, f"委托创建通知 onOrderCreateAsync 更新 grid-{gridIdx} seq:{response.seq} -> order_id:{response.order_id}")
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f"|- 委托创建通知 onOrderCreateAsync[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}]: {response.order_id} - {str(e)}")
|
||||
finally:
|
||||
self.dataUpdateLock.release()
|
||||
|
||||
def onOrderTrade(self, trade:XtTrade): # TODO 委托成交通知,处理成交后网格切换
|
||||
remark = trade.order_remark.split(',')
|
||||
if trade.strategy_name != self.getName() or len(remark) < 3 or self.tradeTarget.stock_code != trade.stock_code:
|
||||
return
|
||||
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}] : {trade.order_id}')
|
||||
|
||||
self.dataUpdateLock.acquire()
|
||||
try:
|
||||
orderType = trade.order_remark.split(',')[0]
|
||||
gridIdx = trade.order_remark.split(',')[1] # 从remark中获取gridIdx
|
||||
type:str = ""
|
||||
if orderType == OrderTypeInit:
|
||||
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] - 建仓单成交')
|
||||
self.tradeTarget.status = 1 # type: ignore
|
||||
self.tradeTarget.init_price = trade.traded_price # type: ignore
|
||||
self.tradeTarget.grid_index = 1 # type: ignore
|
||||
type = "建仓单"
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] - 网格单成交')
|
||||
oriIdx = self.tradeTarget.grid_index
|
||||
if gridIdx > self.tradeTarget.grid_index:
|
||||
type = "下移一格"
|
||||
self.tradeTarget.grid_index +=1
|
||||
elif gridIdx < self.tradeTarget.grid_index:
|
||||
type = "上移一格"
|
||||
self.tradeTarget.grid_match_count += 1
|
||||
self.tradeTarget.grid_total_profit += self.tradeTarget.grid_size * trade.traded_volume
|
||||
self.tradeTarget.grid_index -= 1
|
||||
else:
|
||||
type = "保持格, 理论上不应该输出"
|
||||
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} - 原网格位置 {oriIdx}, 现网格位置 {self.tradeTarget.grid_index}')
|
||||
|
||||
self.saveProxy()
|
||||
del self.orderGrid[gridIdx]
|
||||
PrintLog(LogLevel.INFO, f"|- 成交报告[{self.tradeTarget.targetName()}] : ====================================")
|
||||
PrintLog(LogLevel.INFO, f"|- 标的[{self.tradeTarget.targetName()}] {type}-单号{trade.order_id}已成交 ")
|
||||
PrintLog(LogLevel.INFO, f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
|
||||
PrintLog(LogLevel.INFO, f' 手续费 : {trade.commission:.3f}')
|
||||
self.refreshGridOrder() # 更新网格订单
|
||||
finally:
|
||||
self.dataUpdateLock.release()
|
||||
|
||||
|
||||
def getName(self):
|
||||
return "SFGRID"
|
||||
|
||||
def saveProxy(self):
|
||||
rc = self.tradeTarget.save()
|
||||
event_bus.publish(EventTradeTargetUpdate, self.tradeTarget)
|
||||
return rc
|
||||
@@ -0,0 +1,985 @@
|
||||
from typing import Any
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
from datetime import datetime
|
||||
import threading
|
||||
import time
|
||||
import core.eventbus as eBus
|
||||
from core.logger import LogLevel, PrintLog
|
||||
from core.sfgrid import bus_events
|
||||
from core.sfgrid.model import SFGridTradeTarget
|
||||
from core.qmt import qmtv
|
||||
from core.sfgrid.sfgrid_strategy import SFGridStrategy
|
||||
|
||||
|
||||
class TradeTargetUI(ttk.Frame):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.tradeTargetData:dict[int, SFGridTradeTarget] = {} # id->trade_target
|
||||
self.stockCodeIdMap:dict[str, int] = {}
|
||||
self.strategy_ctrl:dict[int, SFGridStrategy] = {} # stock_code->trade_target
|
||||
self.targetMarketPrice: dict[int, float] = {}
|
||||
self.targetAvgPrice: dict[int, float] = {}
|
||||
self.listening_stock = []
|
||||
# 监控价格,默认值为10
|
||||
self.monitor_price = 10.0
|
||||
|
||||
self.init_trade_target_pool()
|
||||
|
||||
# 市场监控数据
|
||||
self.marketData: dict[str, Any] = {} # 存储市场数据 {stock_code: {stock_name, last_price, time}}
|
||||
|
||||
# 市场监控窗口显示状态
|
||||
self.market_monitor_visible = True
|
||||
|
||||
# 创建界面
|
||||
self.create_ui()
|
||||
eBus.event_bus.subscribe(eBus.MarketDataUpdate, self.onMarketDataUpdated)
|
||||
|
||||
eBus.event_bus.subscribe(bus_events.EventTradeTargetUpdate, self.onStrategyUpdate)
|
||||
eBus.event_bus.subscribe(bus_events.EventTradeTargetDeleted, self.onTradeTargetDeleted)
|
||||
|
||||
|
||||
def init_trade_target_pool(self):
|
||||
results = SFGridTradeTarget.select()
|
||||
for temp in results:
|
||||
tradeTarget:SFGridTradeTarget = temp
|
||||
pos = qmtv.getStockPosition(tradeTarget.stock_code)
|
||||
tradeTarget.current_position = 0 if pos is None else pos.volume # type: ignore
|
||||
if pos is None:
|
||||
self.targetAvgPrice[tradeTarget.get_id()] = 0.0
|
||||
else:
|
||||
self.targetAvgPrice[tradeTarget.get_id()] = pos.avg_price
|
||||
PrintLog(LogLevel.INFO, f'- [成功]获取持仓信息: {tradeTarget.stock_code} {tradeTarget.targetName()} {tradeTarget.current_position} {pos.avg_price}')
|
||||
|
||||
self.updateTradeTarget(tradeTarget, True) # 初始化的时候
|
||||
|
||||
PrintLog(LogLevel.INFO, f'- [成功]交易标的信息初始化, 共 {len(self.tradeTargetData)} 个标的')
|
||||
|
||||
|
||||
# 收集所有市场数据用于市场监控
|
||||
def onMarketDataUpdated(self, data):
|
||||
for stock_code, tickData in data.items():
|
||||
if stock_code in self.stockCodeIdMap:
|
||||
id:int = self.stockCodeIdMap[stock_code]
|
||||
self.targetMarketPrice[id] = tickData['lastPrice']
|
||||
tradeTarget = self.tradeTargetData[id]
|
||||
# timeStr = datetime.fromtimestamp(tickData['time']/1000)
|
||||
lastPrice = float("{:.3f}".format(tickData['lastPrice']))
|
||||
tradeTarget.market_price = lastPrice # type: ignore
|
||||
# PrintLog(LogLevel.INFO, f'|- 市价更新[{tradeTarget.targetName()}] - {timeStr.strftime("%H:%M:%S")} 市价更新: {lastPrice}======================{id}')
|
||||
self.updateTradeTarget(tradeTarget, False) # 市价更新
|
||||
else:
|
||||
# 非目标交易,发布市场数据更新事件用于市场监控
|
||||
lastPrice = tickData['lastPrice']
|
||||
# 使用用户设置的监控价格替代硬编码的10
|
||||
if lastPrice == self.monitor_price or stock_code in self.listening_stock:
|
||||
# 发布市场数据更新事件用于市场监控
|
||||
if stock_code not in self.listening_stock:
|
||||
self.listening_stock.append(stock_code)
|
||||
# 更新市场监控数据用于UI显示
|
||||
current_time = datetime.now().strftime("%H:%M:%S")
|
||||
self.marketData[str(stock_code)] = {
|
||||
'stock_name': qmtv.getInstrumentName(stock_code),
|
||||
'last_price': tickData['lastPrice'],
|
||||
'time': current_time
|
||||
}
|
||||
|
||||
|
||||
# 来自策略的数据更新
|
||||
def onStrategyUpdate(self, target: SFGridTradeTarget):
|
||||
id = target.get_id()
|
||||
self.tradeTargetData[id] = target
|
||||
|
||||
|
||||
# priceChange 用于控制是否对更新价格数据,进行交易判断
|
||||
def updateTradeTarget(self, target: SFGridTradeTarget, save: bool = True):
|
||||
if save:
|
||||
target.save()
|
||||
|
||||
id = target.get_id()
|
||||
# PrintLog(LogLevel.INFO, f' [序号-{id}] 股票代码: {target.stock_code}-{target.stock_name}: {target.plan_buy_price} {target.plan_sell_price}') # type: ignore
|
||||
# 更新或添加数据到本地缓存
|
||||
self.tradeTargetData[id] = target
|
||||
|
||||
if id not in self.strategy_ctrl:
|
||||
self.stockCodeIdMap[target.stock_code] = id # type: ignore
|
||||
self.strategy_ctrl[id] = SFGridStrategy(target) # pyright: ignore[reportArgumentType]
|
||||
if id in self.targetAvgPrice:
|
||||
pos = qmtv.getStockPosition(target.stock_code)
|
||||
if pos is not None:
|
||||
self.targetAvgPrice[id] = pos.avg_price
|
||||
|
||||
# UI CREATE
|
||||
def create_ui(self):
|
||||
"""创建UI界面"""
|
||||
# 主框架(使用self作为父容器)
|
||||
main_frame = ttk.Frame(self)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# 创建工具栏
|
||||
toolbar_frame = ttk.Frame(main_frame)
|
||||
toolbar_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# 工具栏按钮
|
||||
ttk.Button(toolbar_frame, text="➕ 添加标的",
|
||||
command=self.btnHandlerAddTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(toolbar_frame, text="🗑 删除标的",
|
||||
command=self.btnHandlerDelSelectedTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(toolbar_frame, text="▶️ 启动交易",
|
||||
command=self.btnHandlerStartSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(toolbar_frame, text="⏸ 暂停交易",
|
||||
command=self.btnHandlerStopSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(toolbar_frame, text="🛠 交易设置",
|
||||
command=self.btnHandlerTradeSettings, width=12).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
ttk.Button(toolbar_frame, text="▣ 边栏",
|
||||
command=self.btnHandlerToggleMarketMonitor, width=8).pack(side=tk.RIGHT, padx=2)
|
||||
# 添加价格监控输入字段和确认按钮
|
||||
ttk.Button(toolbar_frame, text="确认",
|
||||
command=self.btnHandlerSetMonitorPrice, width=8).pack(side=tk.RIGHT, padx=2)
|
||||
self.monitor_price_entry = ttk.Entry(toolbar_frame, width=8)
|
||||
self.monitor_price_entry.insert(0, str(self.monitor_price))
|
||||
self.monitor_price_entry.pack(side=tk.RIGHT, padx=2)
|
||||
ttk.Label(toolbar_frame, text="价格").pack(side=tk.RIGHT, padx=(20, 2))
|
||||
ttk.Label(toolbar_frame, text="监控配置").pack(side=tk.RIGHT, padx=(20, 2))
|
||||
|
||||
|
||||
# 表格区域
|
||||
self.create_tables_area(main_frame)
|
||||
|
||||
# 启动刷新线程
|
||||
self.refresh_thread = threading.Thread(target=self.refresh_loop, daemon=True)
|
||||
self.refresh_thread.start()
|
||||
|
||||
|
||||
def refresh_loop(self):
|
||||
"""刷新循环"""
|
||||
while True:
|
||||
self.after(0, self.refresh_table)
|
||||
self.after(0, self.populate_market_table)
|
||||
time.sleep(0.5) # 每0.5秒刷新一次
|
||||
|
||||
|
||||
def create_tables_area(self, parent):
|
||||
"""创建表格区域"""
|
||||
# 创建主表格框架(水平排列)
|
||||
tables_frame = ttk.Frame(parent)
|
||||
tables_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
|
||||
|
||||
# 左侧交易标的区域
|
||||
trade_frame = ttk.LabelFrame(tables_frame, text="交易标的详情", padding=10)
|
||||
trade_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
|
||||
|
||||
# 创建交易标的表格
|
||||
self.create_trade_target_table(trade_frame)
|
||||
|
||||
# 右侧市场监控区域
|
||||
self.market_frame = ttk.LabelFrame(tables_frame, text="市场监控", padding=10)
|
||||
self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
|
||||
|
||||
# 创建市场监控表格
|
||||
self.create_market_monitor_table(self.market_frame)
|
||||
|
||||
|
||||
def create_trade_target_table(self, parent):
|
||||
"""创建交易标的表格"""
|
||||
|
||||
columns = ("ID",
|
||||
"股票代码", "股票名称", "市场价", "当前持仓", "建仓成本",
|
||||
"平均成本", "网格匹配次数", "网格收益", "交易状态"
|
||||
)
|
||||
|
||||
self.trade_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
|
||||
|
||||
# 专业化的列配置
|
||||
column_configs = {
|
||||
"ID": (50, tk.CENTER),
|
||||
"股票代码": (80, tk.CENTER),
|
||||
"股票名称": (80, tk.E),
|
||||
"市场价": (70, tk.E),
|
||||
"当前持仓": (80, tk.E),
|
||||
"建仓成本": (60, tk.E),
|
||||
"平均成本": (60, tk.E),
|
||||
"网格匹配次数": (60, tk.E),
|
||||
"网格收益": (60, tk.E),
|
||||
"交易状态": (80, tk.CENTER)
|
||||
}
|
||||
|
||||
for col in columns:
|
||||
width, anchor = column_configs[col]
|
||||
self.trade_table.heading(col, text=col)
|
||||
self.trade_table.column(col, width=width, anchor=anchor) # type: ignore
|
||||
|
||||
# 填充数据
|
||||
self.populate_trade_table()
|
||||
|
||||
# 滚动条
|
||||
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.trade_table.yview)
|
||||
self.trade_table.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.trade_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# 绑定双击事件
|
||||
self.trade_table.bind("<Double-1>", self.on_table_double_click)
|
||||
|
||||
def create_market_monitor_table(self, parent):
|
||||
"""创建市场监控表格"""
|
||||
columns = ("时间", "股票名称", "最新价格")
|
||||
|
||||
self.market_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
|
||||
|
||||
# 列配置
|
||||
column_configs = {
|
||||
"时间": (50, tk.CENTER),
|
||||
"股票名称": (80, tk.CENTER),
|
||||
"最新价格": (50, tk.CENTER)
|
||||
}
|
||||
|
||||
for col in columns:
|
||||
width, anchor = column_configs[col]
|
||||
self.market_table.heading(col, text=col)
|
||||
self.market_table.column(col, width=width, anchor=anchor) # type: ignore
|
||||
|
||||
# 滚动条
|
||||
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.market_table.yview)
|
||||
self.market_table.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.market_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# 绑定双击事件
|
||||
self.market_table.bind("<Double-1>", self.on_market_table_double_click)
|
||||
|
||||
# 填充初始数据
|
||||
self.populate_market_table()
|
||||
|
||||
def populate_market_table(self):
|
||||
"""填充市场监控表格数据"""
|
||||
# 保存当前选中的项
|
||||
selected_items = self.market_table.selection()
|
||||
selected_values = []
|
||||
for item in selected_items:
|
||||
values = self.market_table.item(item)['values']
|
||||
if values:
|
||||
selected_values.append(values[1]) # 保存股票代码
|
||||
|
||||
# 清空现有数据
|
||||
for item in self.market_table.get_children():
|
||||
self.market_table.delete(item)
|
||||
|
||||
# 填充市场数据
|
||||
tmp = self.marketData.copy()
|
||||
for stock_code, data in tmp.items():
|
||||
# 处理时间格式,仅显示 hh:mm:ss
|
||||
time_str = data['time']
|
||||
# 如果时间字符串包含空格,说明包含日期和时间,只取时间部分
|
||||
if ' ' in time_str:
|
||||
time_str = time_str.split(' ')[1]
|
||||
|
||||
# 确保时间格式为 hh:mm:ss,如果只有 hh:mm 则补充 :00
|
||||
if ':' in time_str:
|
||||
time_components = time_str.split(':')
|
||||
if len(time_components) == 2:
|
||||
# 只有小时和分钟,补充秒
|
||||
time_str = f"{time_components[0]}:{time_components[1]}:00"
|
||||
elif len(time_components) >= 3:
|
||||
# 有小时、分钟和秒,只取前三个部分
|
||||
time_str = f"{time_components[0]}:{time_components[1]}:{time_components[2]}"
|
||||
|
||||
values = [
|
||||
time_str,
|
||||
data['stock_name']+f"-{stock_code}",
|
||||
f"{data['last_price']:.3f}",
|
||||
stock_code
|
||||
]
|
||||
self.market_table.insert('', tk.END, values=values)
|
||||
|
||||
# 恢复之前选中的项
|
||||
if selected_values:
|
||||
for item in self.market_table.get_children():
|
||||
values = self.market_table.item(item)['values']
|
||||
if values and values[1] in selected_values: # 比较股票代码
|
||||
self.market_table.selection_add(item)
|
||||
|
||||
def on_market_table_double_click(self, event):
|
||||
"""市场监控表格双击事件"""
|
||||
selected = self.market_table.selection()
|
||||
if selected:
|
||||
item = selected[0]
|
||||
values = self.market_table.item(item)['values']
|
||||
print(values)
|
||||
stock_name = values[1]
|
||||
last_price = values[2]
|
||||
stock_code = values[3]
|
||||
|
||||
# 检查是否已在交易池中
|
||||
is_in_trade_pool = any(target.stock_code == stock_code for target in self.tradeTargetData.values())
|
||||
|
||||
if is_in_trade_pool:
|
||||
messagebox.showinfo("提示", f"{stock_code} ({stock_name}) 已在交易池中")
|
||||
else:
|
||||
result = messagebox.askyesno(
|
||||
"添加交易标的",
|
||||
f"确定要将以下股票添加到交易池吗?\n\n"
|
||||
f"股票代码: {stock_code}\n"
|
||||
f"股票名称: {stock_name}\n"
|
||||
f"最新价格: {last_price}"
|
||||
)
|
||||
|
||||
if result:
|
||||
# 发布事件通知主控制器添加标的
|
||||
self.addTradeTarget(stock_code)
|
||||
|
||||
def get_trade_enabled_indicator(self, target: SFGridTradeTarget) -> str:
|
||||
"""获取交易状态指示器"""
|
||||
if target.status == -1:
|
||||
return "请做交易设置"
|
||||
elif target.status >= 0:
|
||||
if target.enabled:
|
||||
return "▶ 运行中"
|
||||
else:
|
||||
return "⏸ 已停止"
|
||||
|
||||
|
||||
def populate_trade_table(self):
|
||||
"""填充交易标的表格数据"""
|
||||
for id, target in self.tradeTargetData.items():
|
||||
values = [
|
||||
id,
|
||||
target.stock_code, # "股票代码"
|
||||
target.stock_name, # "股票名称"
|
||||
f"{self.targetMarketPrice[id]:.3f}" if id in self.targetMarketPrice else '-', # "市场价"
|
||||
target.current_position, # "当前持仓"
|
||||
'-' if target.init_price is None else f"{target.init_price:.3f}", # "建仓成本"
|
||||
f"{self.targetAvgPrice[id]:.3f}", # "平均成本"
|
||||
target.grid_match_count, # "网格匹配次数"
|
||||
f"{target.grid_total_profit:.3f}", # "网格收益"
|
||||
self.get_trade_enabled_indicator(target) # type: ignore
|
||||
]
|
||||
|
||||
self.trade_table.insert('', tk.END, values=values)
|
||||
|
||||
|
||||
def on_table_double_click(self, event):
|
||||
"""表格双击事件"""
|
||||
selected = self.trade_table.selection()
|
||||
if selected:
|
||||
item = selected[0]
|
||||
values = self.trade_table.item(item)['values']
|
||||
ctrl = self.strategy_ctrl[values[0]]
|
||||
PrintLog(LogLevel.DEBUG, f"双击查看详情: {values[0]} - {values[1]}")
|
||||
PrintLog(LogLevel.DEBUG, f"双击查看详情 - 订单网格")
|
||||
ctrl.printPendingOrder()
|
||||
|
||||
def get_selected_target(self):
|
||||
"""获取选中的交易标的"""
|
||||
selected = self.trade_table.selection()
|
||||
if not selected:
|
||||
messagebox.showwarning("未选中", "请先选择一个交易标的")
|
||||
return None
|
||||
|
||||
# 获取选中行的ID
|
||||
item = selected[0]
|
||||
values = self.trade_table.item(item)['values']
|
||||
target_id = values[0]
|
||||
|
||||
# 从列表中找到对应的target对象
|
||||
for id in self.tradeTargetData:
|
||||
if int(target_id) == id: # type: ignore
|
||||
return self.tradeTargetData[id]
|
||||
|
||||
return None
|
||||
|
||||
def refresh_table(self):
|
||||
"""刷新表格数据"""
|
||||
# 保存当前选中的项
|
||||
selected_items = self.trade_table.selection()
|
||||
selected_values = []
|
||||
for item in selected_items:
|
||||
values = self.trade_table.item(item)['values']
|
||||
if values:
|
||||
selected_values.append(values[0]) # 保存ID
|
||||
|
||||
# 清空表格
|
||||
for item in self.trade_table.get_children():
|
||||
self.trade_table.delete(item)
|
||||
|
||||
# 重新填充
|
||||
self.populate_trade_table()
|
||||
|
||||
# 恢复之前选中的项
|
||||
if selected_values:
|
||||
for item in self.trade_table.get_children():
|
||||
values = self.trade_table.item(item)['values']
|
||||
if values and values[0] in selected_values:
|
||||
self.trade_table.selection_add(item)
|
||||
|
||||
# 刷新市场监控表格
|
||||
self.populate_market_table()
|
||||
|
||||
def create_grid_view_window(self, target: SFGridTradeTarget):
|
||||
"""创建网格配置查看窗口(只读)"""
|
||||
# 获取顶层窗口
|
||||
root = self.winfo_toplevel()
|
||||
|
||||
# 创建顶层窗口
|
||||
view_window = tk.Toplevel(root)
|
||||
view_window.title(f"网格配置查看 - {target.stock_code} ({target.stock_name})")
|
||||
view_window.geometry("500x450")
|
||||
view_window.resizable(False, False)
|
||||
|
||||
# 设置窗口模态
|
||||
view_window.transient(root)
|
||||
view_window.grab_set()
|
||||
|
||||
# 居中显示
|
||||
root.update_idletasks()
|
||||
x = root.winfo_x() + (root.winfo_width() // 2) - 250
|
||||
y = root.winfo_y() + (root.winfo_height() // 2) - 225
|
||||
view_window.geometry(f"500x450+{x}+{y}")
|
||||
|
||||
# 创建主框架
|
||||
main_frame = ttk.Frame(view_window, padding=20)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 显示股票信息
|
||||
info_frame = ttk.LabelFrame(main_frame, text="标的详情", padding=10)
|
||||
info_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
ttk.Label(info_frame, text=f"股票代码: {target.stock_code}").grid(row=0, column=0, sticky=tk.W, pady=2)
|
||||
ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2)
|
||||
ttk.Label(info_frame, text=f"状态: 已建初始仓(仅查看模式)").grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=2)
|
||||
|
||||
# 创建网格配置查看框架
|
||||
config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=10)
|
||||
config_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# 基准价格
|
||||
base_price_frame = ttk.Frame(config_frame)
|
||||
base_price_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(base_price_frame, text="基准价格:", width=15).pack(side=tk.LEFT)
|
||||
ttk.Label(base_price_frame, text=f"{target.grid_start_price:.3f}", width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(base_price_frame, text="元", foreground='gray').pack(side=tk.LEFT)
|
||||
|
||||
# 网格大小
|
||||
grid_size_frame = ttk.Frame(config_frame)
|
||||
grid_size_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(grid_size_frame, text="网格大小:", width=15).pack(side=tk.LEFT)
|
||||
ttk.Label(grid_size_frame, text=f"{target.grid_size:.3f}", width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(grid_size_frame, text="元", foreground='gray').pack(side=tk.LEFT)
|
||||
|
||||
# 网格交易量
|
||||
grid_volume_frame = ttk.Frame(config_frame)
|
||||
grid_volume_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(grid_volume_frame, text="网格交易量:", width=15).pack(side=tk.LEFT)
|
||||
ttk.Label(grid_volume_frame, text=str(target.grid_volume), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(grid_volume_frame, text="股", foreground='gray').pack(side=tk.LEFT)
|
||||
|
||||
# 上方网格数量
|
||||
upper_count_frame = ttk.Frame(config_frame)
|
||||
upper_count_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(upper_count_frame, text="上方网格数量:", width=15).pack(side=tk.LEFT)
|
||||
ttk.Label(upper_count_frame, text=str(target.grid_upper_count), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(upper_count_frame, text="格", foreground='gray').pack(side=tk.LEFT)
|
||||
|
||||
# 下方网格数量
|
||||
lower_count_frame = ttk.Frame(config_frame)
|
||||
lower_count_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(lower_count_frame, text="下方网格数量:", width=15).pack(side=tk.LEFT)
|
||||
ttk.Label(lower_count_frame, text=str(target.grid_lower_count), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(lower_count_frame, text="格", foreground='gray').pack(side=tk.LEFT)
|
||||
|
||||
# 生成网格价格序列
|
||||
price_grid_frame = ttk.LabelFrame(main_frame, text="网格价格序列", padding=10)
|
||||
price_grid_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# 计算并显示网格价格序列
|
||||
price_list = target.getPriceGrid()
|
||||
price_text = ", ".join([f"{price:.3f}" for price in price_list])
|
||||
|
||||
# 创建文本框显示网格价格序列
|
||||
text_frame = ttk.Frame(price_grid_frame)
|
||||
text_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
text_widget = tk.Text(text_frame, height=4, wrap=tk.WORD)
|
||||
text_widget.insert(tk.END, price_text)
|
||||
text_widget.config(state=tk.DISABLED) # 只读
|
||||
|
||||
scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_widget.yview)
|
||||
text_widget.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# 关闭按钮
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(fill=tk.X, pady=(10, 0))
|
||||
ttk.Button(button_frame, text="关闭", command=view_window.destroy).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
def create_grid_config_window(self, target: SFGridTradeTarget):
|
||||
"""创建网格配置窗口(可编辑)"""
|
||||
# 获取顶层窗口
|
||||
root = self.winfo_toplevel()
|
||||
|
||||
# 创建顶层窗口
|
||||
config_window = tk.Toplevel(root)
|
||||
config_window.title(f"网格配置 - {target.stock_code} ({target.stock_name})")
|
||||
config_window.geometry("550x550")
|
||||
config_window.resizable(False, False)
|
||||
|
||||
# 设置窗口模态
|
||||
config_window.transient(root)
|
||||
config_window.grab_set()
|
||||
|
||||
# 居中显示
|
||||
root.update_idletasks()
|
||||
x = root.winfo_x() + (root.winfo_width() // 2) - 275
|
||||
y = root.winfo_y() + (root.winfo_height() // 2) - 275
|
||||
config_window.geometry(f"550x550+{x}+{y}")
|
||||
|
||||
# 创建主框架
|
||||
main_frame = ttk.Frame(config_window, padding=20)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 显示股票信息
|
||||
info_frame = ttk.LabelFrame(main_frame, text="标的详情", padding=10)
|
||||
info_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
ttk.Label(info_frame, text=f"股票代码: {target.stock_code}").grid(row=0, column=0, sticky=tk.W, pady=2)
|
||||
ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2)
|
||||
ttk.Label(info_frame, text=f"状态: 新标的(可配置模式)").grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=2)
|
||||
|
||||
# 创建网格配置框架
|
||||
config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=15)
|
||||
config_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# 创建输入框字典用于保存引用
|
||||
entries = {}
|
||||
|
||||
# 基准价格
|
||||
base_price_frame = ttk.Frame(config_frame)
|
||||
base_price_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(base_price_frame, text="基准价格:", width=15).pack(side=tk.LEFT)
|
||||
base_price_entry = ttk.Entry(base_price_frame, width=15)
|
||||
base_price_entry.insert(0, str(target.grid_start_price))
|
||||
base_price_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(base_price_frame, text="元", foreground='gray').pack(side=tk.LEFT)
|
||||
entries['grid_start_price'] = base_price_entry
|
||||
|
||||
# 网格大小
|
||||
grid_size_frame = ttk.Frame(config_frame)
|
||||
grid_size_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(grid_size_frame, text="网格大小:", width=15).pack(side=tk.LEFT)
|
||||
grid_size_entry = ttk.Entry(grid_size_frame, width=15)
|
||||
grid_size_entry.insert(0, str(target.grid_size))
|
||||
grid_size_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(grid_size_frame, text="元", foreground='gray').pack(side=tk.LEFT)
|
||||
entries['grid_size'] = grid_size_entry
|
||||
|
||||
# 网格交易量
|
||||
grid_volume_frame = ttk.Frame(config_frame)
|
||||
grid_volume_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(grid_volume_frame, text="网格交易量:", width=15).pack(side=tk.LEFT)
|
||||
grid_volume_entry = ttk.Entry(grid_volume_frame, width=15)
|
||||
grid_volume_entry.insert(0, str(target.grid_volume))
|
||||
grid_volume_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(grid_volume_frame, text="手", foreground='gray').pack(side=tk.LEFT)
|
||||
entries['grid_volume'] = grid_volume_entry
|
||||
|
||||
# 上方网格数量
|
||||
upper_count_frame = ttk.Frame(config_frame)
|
||||
upper_count_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(upper_count_frame, text="上方网格数量:", width=15).pack(side=tk.LEFT)
|
||||
upper_count_entry = ttk.Entry(upper_count_frame, width=15)
|
||||
upper_count_entry.insert(0, str(target.grid_upper_count))
|
||||
upper_count_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(upper_count_frame, text="格", foreground='gray').pack(side=tk.LEFT)
|
||||
entries['grid_upper_count'] = upper_count_entry
|
||||
|
||||
# 下方网格数量
|
||||
lower_count_frame = ttk.Frame(config_frame)
|
||||
lower_count_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(lower_count_frame, text="下方网格数量:", width=15).pack(side=tk.LEFT)
|
||||
lower_count_entry = ttk.Entry(lower_count_frame, width=15)
|
||||
lower_count_entry.insert(0, str(target.grid_lower_count))
|
||||
lower_count_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(lower_count_frame, text="格", foreground='gray').pack(side=tk.LEFT)
|
||||
entries['grid_lower_count'] = lower_count_entry
|
||||
|
||||
# 预览按钮和结果显示
|
||||
preview_frame = ttk.LabelFrame(main_frame, text="网格价格序列预览", padding=10)
|
||||
preview_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
preview_result = tk.StringVar(value="点击'预览'查看生成的网格价格序列")
|
||||
|
||||
def calculate_grid_prices():
|
||||
"""计算网格价格序列"""
|
||||
try:
|
||||
base_price = float(base_price_entry.get())
|
||||
grid_size = float(grid_size_entry.get())
|
||||
upper_count = int(upper_count_entry.get())
|
||||
lower_count = int(lower_count_entry.get())
|
||||
|
||||
prices = []
|
||||
|
||||
# 计算上方网格价格
|
||||
for i in range(upper_count, 0, -1):
|
||||
price = base_price + grid_size * i
|
||||
prices.append(round(price, 3))
|
||||
|
||||
# 添加基准价格
|
||||
prices.append(base_price)
|
||||
|
||||
# 计算下方网格价格
|
||||
for i in range(1, lower_count + 1):
|
||||
price = base_price - grid_size * i
|
||||
# 确保价格不为负
|
||||
if price >= 0:
|
||||
prices.append(round(price, 3))
|
||||
else:
|
||||
break
|
||||
|
||||
return prices
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def update_preview():
|
||||
"""更新网格价格序列预览"""
|
||||
prices = calculate_grid_prices()
|
||||
if prices:
|
||||
price_str = ", ".join([str(p) for p in prices])
|
||||
preview_result.set(f"网格价格序列: {price_str}")
|
||||
else:
|
||||
preview_result.set("参数错误,请检查输入!")
|
||||
|
||||
# 绑定输入变化自动预览
|
||||
for entry_widget in entries.values():
|
||||
entry_widget.bind("<KeyRelease>", lambda e: update_preview())
|
||||
entry_widget.bind("<FocusOut>", lambda e: update_preview())
|
||||
|
||||
# 预览按钮
|
||||
preview_button_frame = ttk.Frame(preview_frame)
|
||||
preview_button_frame.pack(fill=tk.X, pady=5)
|
||||
# ttk.Button(preview_button_frame, text="预览", command=update_preview).pack(side=tk.LEFT)
|
||||
|
||||
# 预览结果显示
|
||||
preview_label = ttk.Label(preview_button_frame, textvariable=preview_result, foreground='blue')
|
||||
preview_label.pack(side=tk.LEFT, padx=10)
|
||||
|
||||
# 初始预览
|
||||
update_preview()
|
||||
|
||||
# 按钮框架
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(fill=tk.X, pady=(10, 0))
|
||||
|
||||
def save_config():
|
||||
"""保存配置"""
|
||||
try:
|
||||
# 获取输入值
|
||||
grid_start_price = float(base_price_entry.get())
|
||||
grid_size = float(grid_size_entry.get())
|
||||
grid_volume = int(grid_volume_entry.get())
|
||||
grid_upper_count = int(upper_count_entry.get())
|
||||
grid_lower_count = int(lower_count_entry.get())
|
||||
|
||||
# 更新target对象(使用setattr来正确设置Peewee字段的值)
|
||||
setattr(target, 'grid_start_price', grid_start_price)
|
||||
setattr(target, 'grid_size', grid_size)
|
||||
setattr(target, 'grid_volume', grid_volume)
|
||||
setattr(target, 'grid_upper_count', grid_upper_count)
|
||||
setattr(target, 'grid_lower_count', grid_lower_count)
|
||||
setattr(target, 'status', 0)
|
||||
|
||||
# 更新策略控制器
|
||||
self.updateTradeTarget(target, True) # 网格配置变更
|
||||
|
||||
# 关闭窗口
|
||||
config_window.destroy()
|
||||
|
||||
# 添加日志
|
||||
PrintLog(LogLevel.INFO, f"网格配置已保存: {target.stock_code} - {target.stock_name}")
|
||||
messagebox.showinfo("成功", "网格配置已保存!")
|
||||
|
||||
except ValueError:
|
||||
messagebox.showerror("错误", "输入参数有误,请检查!")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"保存配置失败:{str(e)}")
|
||||
PrintLog(LogLevel.ERROR, f"保存网格配置失败: {str(e)}")
|
||||
|
||||
# 保存和取消按钮
|
||||
ttk.Button(button_frame, text="保存", command=save_config).pack(side=tk.RIGHT, padx=5)
|
||||
ttk.Button(button_frame, text="取消", command=config_window.destroy).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
def decrease_grid_index(self, grid_index_var: tk.IntVar, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
|
||||
"""减少网格序号"""
|
||||
current_value = grid_index_var.get()
|
||||
if current_value > 0:
|
||||
grid_index_var.set(current_value - 1)
|
||||
# 同步更新需求持仓量和持仓状态
|
||||
self.update_required_position_and_status(grid_index_var.get(), target, required_position_label, position_status_label)
|
||||
|
||||
def increase_grid_index(self, grid_index_var: tk.IntVar, max_index: int, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
|
||||
"""增加网格序号"""
|
||||
current_value = grid_index_var.get()
|
||||
if current_value < max_index:
|
||||
grid_index_var.set(current_value + 1)
|
||||
# 同步更新需求持仓量和持仓状态
|
||||
self.update_required_position_and_status(grid_index_var.get(), target, required_position_label, position_status_label)
|
||||
|
||||
def update_position_status(self, current_position: int, required_position: int, status_label: ttk.Label):
|
||||
"""更新持仓量状态提示"""
|
||||
if current_position >= required_position:
|
||||
status_label.config(text="持仓量充足", foreground="green")
|
||||
else:
|
||||
shortage = required_position - current_position
|
||||
status_label.config(text=f"还需补充 {shortage} 手仓位", foreground="red")
|
||||
|
||||
|
||||
def update_required_position_and_status(self, grid_index: int, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
|
||||
"""更新需求持仓量和持仓状态"""
|
||||
# 计算需求持仓量
|
||||
required_position:int = grid_index * target.grid_volume # type: ignore
|
||||
required_position_label.config(text=str(required_position))
|
||||
|
||||
# 更新持仓量状态
|
||||
current_position = getattr(target, 'current_position')
|
||||
self.update_position_status(current_position, required_position, position_status_label)
|
||||
|
||||
|
||||
# 交易池管理
|
||||
def addTradeTarget(self, stock_code: str, gridIndex: int = 1): # 新增
|
||||
"""处理添加交易标的事件"""
|
||||
try:
|
||||
stock_name = qmtv.getInstrumentName(stock_code)
|
||||
if not stock_name:
|
||||
PrintLog(LogLevel.ERROR, f'无法获取股票代码 {stock_code} 的名称,请检查代码是否正确')
|
||||
return
|
||||
PrintLog(LogLevel.DEBUG, f'添加交易标的: {stock_code} {stock_name}')
|
||||
|
||||
# 检查是否已存在该标的
|
||||
existing_target = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == stock_code)
|
||||
if existing_target:
|
||||
PrintLog(LogLevel.INFO, f'交易标的 {stock_code} {stock_name} 已存在')
|
||||
return
|
||||
|
||||
# 刷新标的持仓
|
||||
pos = qmtv.getStockPosition(stock_code) # type: ignore
|
||||
new_target = SFGridTradeTarget.create(
|
||||
stock_name=stock_name,
|
||||
stock_code=stock_code,
|
||||
current_position="0" if pos is None else str(pos.volume),
|
||||
grid_index=gridIndex,
|
||||
init_price=0.0,
|
||||
status=-1
|
||||
)
|
||||
# 更新标的池
|
||||
self.updateTradeTarget(new_target, True) # 新增标的,相当于也是初始化
|
||||
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'新增交易标的失败 {stock_code} {e}')
|
||||
|
||||
# button handlers =============================================================================================
|
||||
def btnHandlerGridCorrect(self):
|
||||
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
self.create_grid_correction_window(target)
|
||||
|
||||
|
||||
def btnHandlerToggleMarketMonitor(self):
|
||||
"""切换市场监控窗口显示/隐藏"""
|
||||
if self.market_monitor_visible:
|
||||
# 隐藏市场监控窗口
|
||||
self.market_frame.pack_forget()
|
||||
self.market_monitor_visible = False
|
||||
else:
|
||||
# 显示市场监控窗口
|
||||
self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
|
||||
self.market_monitor_visible = True
|
||||
|
||||
def btnHandlerTradeSettings(self):
|
||||
"""网格配置功能"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
# 检查标的的状态,status为1时仅可查看
|
||||
if target.status == -1 or target.status == 0:
|
||||
self.create_grid_config_window(target)
|
||||
else:
|
||||
# 创建只读的网格配置查看窗口
|
||||
self.create_grid_view_window(target)
|
||||
|
||||
def btnHandlerStartSelectedTrade(self):
|
||||
"""启动选中的交易"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
if target.status < 0:
|
||||
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 未配置交易参数, 请做交易设置。")
|
||||
return
|
||||
|
||||
if target.enabled: # type: ignore
|
||||
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经在运行中")
|
||||
return
|
||||
|
||||
result = messagebox.askyesno(
|
||||
"确认启动",
|
||||
f"确定要启动以下交易标的吗?\n\n"
|
||||
f"股票代码: {target.stock_code}\n"
|
||||
f"股票名称: {target.stock_name}"
|
||||
)
|
||||
|
||||
if result:
|
||||
PrintLog(LogLevel.INFO, f'启动标的交易 {target.targetName()}')
|
||||
target.enabled = True # type: ignore
|
||||
|
||||
id = target.get_id()
|
||||
if id in self.strategy_ctrl:
|
||||
tradeController: SFGridStrategy = self.strategy_ctrl[target.get_id()]
|
||||
tradeTarget = tradeController.enabledTrading(True)
|
||||
self.tradeTargetData[id] = tradeTarget
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f"\t创建标的交易控制器 {target.targetName()}")
|
||||
|
||||
def btnHandlerStopSelectedTrade(self):
|
||||
"""暂停选中的交易"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
if not target.enabled: # type: ignore
|
||||
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经是暂停状态")
|
||||
return
|
||||
|
||||
result = messagebox.askyesno(
|
||||
"确认暂停",
|
||||
f"确定要暂停以下交易标的吗?\n\n"
|
||||
f"股票代码: {target.stock_code}\n"
|
||||
f"股票名称: {target.stock_name}"
|
||||
)
|
||||
|
||||
if result:
|
||||
PrintLog(LogLevel.INFO, f'暂停标的交易 {target.targetName()}')
|
||||
id = target.get_id()
|
||||
if id in self.strategy_ctrl:
|
||||
tradeController: SFGridStrategy = self.strategy_ctrl[target.get_id()]
|
||||
tradeController.enabledTrading(False)
|
||||
else:
|
||||
print(f"标的交易控制器不存在 {target.stock_code} {target.stock_name}\n")
|
||||
|
||||
def btnHandlerDelSelectedTradeTarget(self):
|
||||
"""删除选中的交易标的"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
result = messagebox.askyesno(
|
||||
"确认删除",
|
||||
f"确定要删除以下交易标的吗?\n\n"
|
||||
f"股票代码: {target.stock_code}\n"
|
||||
f"股票名称: {target.stock_name}\n\n"
|
||||
f"⚠️ 此操作不可恢复!",
|
||||
icon='warning'
|
||||
)
|
||||
|
||||
if result:
|
||||
id = target.get_id()
|
||||
# try:
|
||||
if id in self.strategy_ctrl:
|
||||
ctrl = self.strategy_ctrl[id]
|
||||
ctrl.deleteTradeTarget(target)
|
||||
else:
|
||||
self.onTradeTargetDeleted(target)
|
||||
PrintLog(LogLevel.INFO, f"已发送删除请求: {target.stock_code} - {target.stock_name}")
|
||||
|
||||
def onTradeTargetDeleted(self, target: SFGridTradeTarget):
|
||||
id = target.get_id()
|
||||
del self.tradeTargetData[id]
|
||||
del self.strategy_ctrl[id]
|
||||
del self.stockCodeIdMap[target.stock_code] # type: ignore
|
||||
|
||||
def btnHandlerAddTradeTarget(self):
|
||||
"""添加新的交易标的"""
|
||||
# 获取顶层窗口
|
||||
root = self.winfo_toplevel()
|
||||
|
||||
# 创建顶层窗口
|
||||
add_window = tk.Toplevel(root)
|
||||
add_window.title("添加交易标的")
|
||||
add_window.geometry("400x150")
|
||||
add_window.resizable(False, False)
|
||||
|
||||
# 设置窗口模态
|
||||
add_window.transient(root)
|
||||
add_window.grab_set()
|
||||
|
||||
# 居中显示
|
||||
root.update_idletasks()
|
||||
x = root.winfo_x() + (root.winfo_width() // 2) - 200
|
||||
y = root.winfo_y() + (root.winfo_height() // 2) - 75
|
||||
add_window.geometry(f"400x150+{x}+{y}")
|
||||
|
||||
# 创建输入框架
|
||||
input_frame = ttk.Frame(add_window, padding=20)
|
||||
input_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 股票代码输入
|
||||
ttk.Label(input_frame, text="股票代码:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
stock_code_entry = ttk.Entry(input_frame, width=30)
|
||||
stock_code_entry.grid(row=0, column=1, pady=5, padx=(10, 0))
|
||||
stock_code_entry.focus()
|
||||
|
||||
# 按钮框架
|
||||
button_frame = ttk.Frame(input_frame)
|
||||
button_frame.grid(row=1, column=0, columnspan=2, pady=20)
|
||||
|
||||
def confirm_add():
|
||||
stock_code = stock_code_entry.get().strip()
|
||||
if not stock_code:
|
||||
messagebox.showwarning("输入错误", "请输入股票代码")
|
||||
return
|
||||
|
||||
# 发布事件通知主控制器添加标的
|
||||
self.addTradeTarget(stock_code)
|
||||
add_window.destroy()
|
||||
|
||||
def cancel_add():
|
||||
add_window.destroy()
|
||||
|
||||
# 确认和取消按钮
|
||||
ttk.Button(button_frame, text="确认", command=confirm_add, width=10).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="取消", command=cancel_add, width=10).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 绑定回车键确认
|
||||
stock_code_entry.bind('<Return>', lambda event: confirm_add())
|
||||
|
||||
PrintLog(LogLevel.INFO, "点击添加交易标的按钮")
|
||||
|
||||
def btnHandlerSetMonitorPrice(self):
|
||||
"""设置监控价格"""
|
||||
try:
|
||||
# 获取输入的价格
|
||||
price_str = self.monitor_price_entry.get()
|
||||
new_price = float(price_str)
|
||||
|
||||
# 更新监控价格
|
||||
self.monitor_price = new_price
|
||||
|
||||
# 清空当前监控的数据
|
||||
self.marketData.clear()
|
||||
self.listening_stock.clear()
|
||||
|
||||
# 清空市场监控表格
|
||||
for item in self.market_table.get_children():
|
||||
self.market_table.delete(item)
|
||||
|
||||
PrintLog(LogLevel.INFO, f"监控价格已更新为: {new_price}")
|
||||
except ValueError:
|
||||
messagebox.showerror("错误", "请输入有效的数字")
|
||||
@@ -0,0 +1,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()
|
||||
@@ -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
@@ -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
Binary file not shown.
+259
@@ -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
|
||||
@@ -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
@@ -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()
|
||||
@@ -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' # 添加图标文件
|
||||
)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user