6 Commits

Author SHA1 Message Date
kyugao 0b916b5c44 Merge branch 'new_structure' of ssh://git.gogao.top:2222/sfgrid into new_structure
# Conflicts:
#	config.py
2026-01-04 17:48:33 +08:00
kyugao 5a26f5f7b3 update 2026-01-04 17:46:48 +08:00
kyugao 66768cb359 update config 2025-12-08 18:08:43 +08:00
kyugao 988947aa1a 适配macos中对pytray支持不好的情况。使用系统菜单。 2025-12-06 00:12:44 +08:00
kyugao b435f12c49 update 2025-12-05 18:06:39 +08:00
kyugao c59d29d52e init new structure 2025-12-05 17:43:13 +08:00
86 changed files with 2524 additions and 11166 deletions
-4
View File
@@ -6,7 +6,3 @@ starter.dist/starter.dll
build/
.vscode/
example.db.bak
venv/
flet_desktop/
.flet/
sfgrid.log
-233
View File
@@ -1,233 +0,0 @@
# SFGrid 网格交易策略流程图
## 1. 总览:策略生命周期
```mermaid
flowchart TD
A["SFGridStrategy.__init__()"] --> B["订阅事件总线<br/>onOrderCreateAsync / onOrderTrade / onOrderError"]
B --> C["获取涨跌停价<br/>todayUpStopPrice / todayDownStopPrice"]
C --> D["loadExistOrders()<br/>从券商侧恢复未成交订单到 orderGrid"]
D --> E["enabledTrading(enabled)"]
E --> F{"enabled ?"}
F -->|True| G["启用交易流程 → 见 §3"]
F -->|False| H["停用交易流程 → 见 §3"]
G --> I["saveProxy() 持久化"]
H --> I
I --> J["构造完成,进入事件循环<br/>等待 QMT 回调 / UI 操作"]
```
---
## 2. 核心:refreshGridOrder() 网格下单
```mermaid
flowchart TD
START["refreshGridOrder()"] --> CHECK1{"qmtv.isMarketActive<br/>AND<br/>tradeTarget.enabled ?"}
CHECK1 -->|No| SKIP["跳过不下单"]
CHECK1 -->|Yes| QUERY["查询未成交订单<br/>queryPendingOrder()"]
QUERY --> STATUS{"tradeTarget.status ?"}
STATUS -->|"= 0 未建仓"| CHECK_INIT{"已存在建仓单?<br/>remark = 'INIT,1,{code}'"}
CHECK_INIT -->|"No 没有"| PLACE_INIT["下建仓单 (STOCK_BUY)<br/>价格 = getPriceGrid()[0]<br/>remark = 'INIT,1,{code}'"]
CHECK_INIT -->|"Yes 已有"| DONE_INIT["建仓单已在途,跳过"]
STATUS -->|"= 1 已建仓"| GET_IDX["currentIdx = grid_index"]
GET_IDX --> SELL_CHECK{"currentIdx > 0 ?<br/>(grid_index 不是最低点)"}
SELL_CHECK -->|"Yes 可挂卖单"| SELL_EXIST{"已存在同 remark 卖单?<br/>remark='SELL,{idx-1},{code}'"}
SELL_EXIST -->|"No 没有"| SELL_PLACE["下卖出单 (STOCK_SELL)<br/>价格 = grid[sellIdx]<br/>sellIdx = currentIdx - 1"]
SELL_EXIST -->|"Yes 已有"| SELL_SKIP["跳过,避免重复"]
SELL_CHECK -->|"No 价格已最低"| SELL_SKIP2["无卖出空间"]
SELL_PLACE --> BUY_CHECK
SELL_SKIP --> BUY_CHECK
SELL_SKIP2 --> BUY_CHECK
BUY_CHECK{"currentIdx < len(grid)-1 ?<br/>(grid_index 不是最高点)"}
BUY_CHECK -->|"Yes 可挂买单"| BUY_EXIST{"已存在同价同类型买单?<br/>order_type=BUY AND price=buyPrice"}
BUY_EXIST -->|"No 没有"| BUY_PLACE["下买入单 (STOCK_BUY)<br/>价格 = grid[buyIdx]<br/>buyIdx = currentIdx + 1"]
BUY_EXIST -->|"Yes 已有"| BUY_SKIP["跳过,避免重复"]
BUY_CHECK -->|"No 价格已最高"| BUY_SKIP2["无买入空间"]
```
---
## 3. 交易启停:enabledTrading()
```mermaid
flowchart TD
START["enabledTrading(enabled)"] --> SET["self.tradeTarget.enabled = enabled"]
SET --> BRANCH{"enabled ?"}
BRANCH -->|"True 启用"| STATUS{"tradeTarget.status ?"}
STATUS -->|"= 0 未建仓"| INIT_IDX{"grid_index == 0 ?"}
INIT_IDX -->|"Yes"| SET1["grid_index = 1<br/>(默认建仓位置)"]
INIT_IDX -->|"No"| KEEP["保留现有 grid_index"]
SET1 --> REFRESH1["refreshGridOrder()"]
KEEP --> REFRESH1
STATUS -->|"= 1 已建仓"| CALC["计算最小需求仓位<br/>min = grid_volume × grid_index"]
CALC --> CHECK{"current_position >= min ?"}
CHECK -->|"Yes 充足"| REFRESH2["refreshGridOrder()"]
CHECK -->|"No 不足"| DENY["拒绝启用<br/>enabled = False<br/>(风控保护)"]
BRANCH -->|"False 停用"| CANCEL["取消所有未成交订单<br/>cancel_order_stock_async()"]
CANCEL --> LOG["记录取消数量"]
REFRESH1 --> SAVE["saveProxy() 持久化"]
REFRESH2 --> SAVE
DENY --> SAVE
LOG --> SAVE
```
---
## 4. 事件回调链
```mermaid
flowchart TD
subgraph QMT["QMT / xtquant 层"]
OA["orderAsync()<br/>返回 seq"]
PUSH_ERR["C扩展推送<br/>XtOrderError"]
PUSH_RESP["C扩展推送<br/>XtOrderResponse"]
PUSH_TRADE["C扩展推送<br/>XtTrade"]
end
subgraph BUS["事件总线 event_bus"]
EVT_ERR["MarketOrderError"]
EVT_RESP["MarketOrderCreated"]
EVT_TRADE["MarketOrderTraded"]
end
subgraph STG["SFGridStrategy 回调"]
OE["onOrderError()"]
OC["onOrderCreateAsync()"]
OT["onOrderTrade()"]
end
OA --> PUSH_RESP
OA --> PUSH_ERR
PUSH_ERR --> EVT_ERR --> OE
PUSH_RESP --> EVT_RESP --> OC
PUSH_TRADE --> EVT_TRADE --> OT
```
---
## 5. onOrderError() 委托失败处理
```mermaid
flowchart TD
START["onOrderError(order_error)"] --> CHK1{"order_remark 非空 ?"}
CHK1 -->|"No 空"| EXIT1["无法解析,忽略"]
CHK1 -->|"Yes"| PARSE["解析 remark<br/>'{type},{gridIdx},{stockCode}'"]
PARSE --> CHK2{"len(parts) >= 3 ?"}
CHK2 -->|"No"| EXIT1
CHK2 -->|"Yes"| CHK3{"strategy_name == 'SFGRID'<br/>AND<br/>stockCode 匹配本标的 ?"}
CHK3 -->|"No 不匹配"| EXIT1
CHK3 -->|"Yes"| LOCK["获取 dataUpdateLock"]
LOCK --> DEL{"gridIdx in orderGrid ?"}
DEL -->|"Yes"| REMOVE["del orderGrid[gridIdx]<br/>清理孤立条目"]
DEL -->|"No"| LOG_ERR["记录错误日志<br/>error_id / error_msg"]
REMOVE --> LOG_ERR
LOG_ERR --> UNLOCK["释放 dataUpdateLock"]
```
---
## 6. onOrderCreateAsync() 订单确认
```mermaid
flowchart TD
START["onOrderCreateAsync(response)"] --> PARSE["解析 remark<br/>'{type},{gridIdx},{stockCode}'"]
PARSE --> FILTER{"strategy_name == 'SFGRID'<br/>AND len(parts) >= 3<br/>AND stockCode 匹配 ?"}
FILTER -->|"No"| EXIT["忽略"]
FILTER -->|"Yes"| LOCK["获取 dataUpdateLock"]
LOCK --> UPDATE["orderGrid[gridIdx] = response.order_id<br/>seq → order_id 替换"]
UPDATE --> UNLOCK["释放 dataUpdateLock"]
```
---
## 7. onOrderTrade() 成交处理
```mermaid
flowchart TD
START["onOrderTrade(trade)"] --> PARSE["解析 remark<br/>'{type},{gridIdx},{stockCode}'"]
PARSE --> FILTER{"strategy_name == 'SFGRID'<br/>AND len(parts) >= 3<br/>AND stockCode 匹配 ?"}
FILTER -->|"No"| EXIT["忽略"]
FILTER -->|"Yes"| LOCK["获取 dataUpdateLock"]
LOCK --> TYPE{"orderType ?"}
TYPE -->|"INIT 建仓单"| INIT["status = 1<br/>init_price = traded_price<br/>grid_index = 1"]
TYPE -->|"BUY / SELL 网格单"| CMP{"gridIdx vs grid_index ?"}
CMP -->|"gridIdx > grid_index<br/>(买入成交)"| DOWN["grid_index += 1<br/>下移一格"]
CMP -->|"gridIdx < grid_index<br/>(卖出成交)"| UP["grid_index -= 1<br/>上移一格<br/>match_count += 1<br/>total_profit += grid_size × volume"]
CMP -->|"gridIdx == grid_index<br/>(异常)"| SAME["日志: 理论上不应该输出"]
INIT --> POST
DOWN --> POST
UP --> POST
SAME --> POST
POST["成交后处理"] --> SAVE["saveProxy() 持久化状态"]
SAVE --> DEL["del orderGrid[gridIdx]<br/>移除已成交订单"]
DEL --> REPORT["打印成交报告<br/>成交价/量/手续费"]
REPORT --> REFRESH["refreshGridOrder()<br/>在新位置挂新的网格单"]
REFRESH --> UNLOCK["释放 dataUpdateLock"]
```
---
## 8. 网格交易完整状态机
```mermaid
stateDiagram-v2
[*] --> 未建仓: 创建 SFGridStrategy
未建仓 --> 建仓中: enabledTrading(True)<br/>下建仓单 INIT
建仓中 --> 已建仓: onOrderTrade(INIT)<br/>建仓单成交
建仓中 --> 建仓失败: onOrderError(INIT)<br/>委托被拒
建仓失败 --> 建仓中: refreshGridOrder()<br/>重新下建仓单
已建仓 --> 网格运行: refreshGridOrder()<br/>上下各挂一单
网格运行 --> 网格运行: onOrderTrade(SELL)<br/>卖出成交 → 上移<br/>重新挂单
网格运行 --> 网格运行: onOrderTrade(BUY)<br/>买入成交 → 下移<br/>重新挂单
网格运行 --> 单边挂单: onOrderError<br/>某方向委托失败
单边挂单 --> 网格运行: refreshGridOrder()<br/>重新补挂失败方向的单
已建仓 --> 已停用: enabledTrading(False)<br/>取消所有挂单
网格运行 --> 已停用: enabledTrading(False)
单边挂单 --> 已停用: enabledTrading(False)
已停用 --> 已建仓: enabledTrading(True)<br/>仓位检查通过
已停用 --> 已停用: enabledTrading(True)<br/>仓位不足,回退
```
---
## 9. 网格价格示意
```
价格
│ grid[5] = 12.00 ← 最贵(顶部)
│ grid[4] = 11.50
│ grid[3] = 11.00 ← 当前位置 grid_index=3
│ grid[2] = 10.50 上方挂卖单 @10.50 (sellIdx=2, grid_index-1)
│ grid[1] = 10.00 下方挂买单 @10.00 (buyIdx=1, 已成交位置)
│ grid[0] = 9.50 ← 最便宜(底部/建仓价)
└──────────────────────→
grid_index=3 时:
卖单挂在 grid[2] @10.50 → 价格跌到 10.50 卖出(上移一格,赚差价)
买单挂在 grid[4] @11.50 → 价格涨到 11.50 买入(下移一格,补仓)
grid_size = grid[i] - grid[i-1] = 0.50(每格利润空间)
```
+5
View File
@@ -0,0 +1,5 @@
[config]
miniqmtpath = D:/Programs/DTQMT/userdata_mini
account_no = 99082560
log_level = INFO
+57 -18
View File
@@ -1,25 +1,64 @@
"""
运行时配置 — 端口、路径、账号由自动探测设置,无需配置文件。
"""
import os
import sys
import configparser
from pathlib import Path
import sys
from typing import Any
# ---- 自动探测的配置项(默认值仅占位,启动时自动修正) ----
miniQMTPath: str = ''
account_no: str = ''
log_level: str = 'INFO'
console_log: bool = True
use_simulated_qmt: bool = False
miniQMTPath = r'D:\\Programs\\DTQMT\\userdata_mini' # miniQMT软件的安装路径
# miniQMTPath = ''
account_no:str = '99082560'
console_log = True
log_level = "INFO"
config : Any
def app_dir() -> Path:
"""应用根目录(兼容开发环境打包后的 exe"""
def get_config_path() -> Path:
"""获取配置文件的正确路径(兼容开发环境打包后的可执行文件"""
if getattr(sys, 'frozen', False):
return Path(sys.executable).parent
return Path(__file__).resolve().parent
# 打包后的可执行文件环境
# sys._MEIPASS是PyInstaller解压临时文件的目录
# 配置文件应该放在可执行文件同目录下
base_path = Path(sys.executable).parent
else:
# 开发环境
base_path = Path(__file__).resolve().parent
return base_path / 'config.ini'
def log_file_path() -> Path:
"""日志文件路径"""
return app_dir() / 'sfgrid.log'
def get_config(section:str, key:str):
pass
def save_config(miniQmtPath:str, account_no:str):
"""创建默认配置文件"""
config = configparser.ConfigParser()
config['config'] = {
'miniQMTPath': miniQmtPath,
'account_no': account_no
}
config_path = get_config_path()
with open(config_path, 'w') as configfile:
config.write(configfile)
print(f'已创建默认配置文件: {config_path}')
def exist_config() -> bool:
"""检查配置文件是否存在"""
config_path = get_config_path()
return config_path.exists()
def initConfig() -> bool:
global miniQMTPath, account_no, log_level
# 获取配置文件路径
config_path = get_config_path()
config = configparser.ConfigParser()
config.read(config_path, encoding='utf-8')
miniQMTPath = config.get('config','miniQMTPath')
account_no = config.get('config','account_no')
log_level = config.get('config','log_level')
# 判断miniQMTPath是否为空,并且目录是否存在
if not miniQMTPath or not Path(miniQMTPath).exists():
print('请先配置miniQMTPath')
return False
else:
return True
+94
View File
@@ -0,0 +1,94 @@
# Global configuration variables
# Define these BEFORE imports to avoid circular dependency issues with logger
console_log = True
miniQMTPath = None
miniQMTAccount = None
log_level = "1"
from pathlib import Path
from core.config.config_model import ConfigModel, CfgKeyLogLevel, CfgKeyMiniQmtPath, CfgKeyMiniQmtAccount, CfgKeyConsoleLog
from core.database import db
def initConfig() -> bool:
"""Initialize configuration from database"""
global miniQMTPath, miniQMTAccount, log_level, console_log
# Ensure connection and tables
db.connect(reuse_if_open=True)
if not db.table_exists(ConfigModel._meta.table_name):
db.create_tables([ConfigModel])
# Check and initialize keys
_init_key(CfgKeyLogLevel, "1")
_init_key(CfgKeyConsoleLog, "True")
_init_key(CfgKeyMiniQmtPath, None)
_init_key(CfgKeyMiniQmtAccount, None)
# Load values
try:
miniQMTPath = _get_value(CfgKeyMiniQmtPath)
miniQMTAccount = _get_value(CfgKeyMiniQmtAccount)
log_level = _get_value(CfgKeyLogLevel) or "1"
console_log = _get_value(CfgKeyConsoleLog) or "True"
console_log = console_log.lower() == "true"
# console_log is not in DB currently, keeping default True or could add to DB
except Exception as e:
print(f"Error loading config: {e}")
return False
# Validate path
if not miniQMTPath or not Path(miniQMTPath).exists():
print('请先配置miniQMTPath')
return False
return True
def _init_key(key: str, default_value: str | None):
"""Helper to initialize a key if it doesn't exist"""
try:
ConfigModel.get(ConfigModel.key == key)
except ConfigModel.DoesNotExist:
ConfigModel.create(key=key, value=default_value)
def _get_value(key: str) -> str | None:
"""Helper to get value safely"""
try:
return ConfigModel.get(ConfigModel.key == key).value
except ConfigModel.DoesNotExist:
return None
def save_config(key: str, value: str):
"""Save configuration to database"""
_update_key(key, value)
print(f'配置已更新: {key}={value}')
def _update_key(key: str, value: str):
try:
record = ConfigModel.get(ConfigModel.key == key)
record.value = value
record.save()
except ConfigModel.DoesNotExist:
ConfigModel.create(key=key, value=value)
def exist_config() -> bool:
"""Check if essential config exists"""
path = _get_value(CfgKeyMiniQmtPath)
account = _get_value(CfgKeyMiniQmtAccount)
return bool(path and account)
def getLogLevel() -> str:
"""获取配置中的日志级别"""
return log_level
def getConsoleLog() -> bool:
"""获取配置中的控制台日志设置"""
return console_log
def getMiniQMTPath() -> str | None:
"""获取配置中的miniQMT路径"""
return miniQMTPath
def getMiniQMTAccount() -> str | None:
"""获取配置的miniQMT账号"""
return miniQMTAccount
+11
View File
@@ -0,0 +1,11 @@
from peewee import CharField
from core.database import BaseModel, db
CfgKeyLogLevel = "log_level"
CfgKeyConsoleLog = "console_log"
CfgKeyMiniQmtPath = "miniQMTPath"
CfgKeyMiniQmtAccount = "miniQMTAccount"
class ConfigModel(BaseModel):
key = CharField(unique=True)
value = CharField(null=True)
+1
View File
@@ -2,4 +2,5 @@ import xtquant.xtconstant as xtconstant
OrderTypeBuy = f'{xtconstant.STOCK_BUY}' # 买
OrderTypeSell = f'{xtconstant.STOCK_SELL}' # 卖
OrderTypeInit = "0" # 建仓
OrderTypeNone = "None"
+1 -2
View File
@@ -1,10 +1,9 @@
from peewee import SqliteDatabase, Model
from core.logger import LogLevel, PrintLog
# 连接到SQLite数据库
db: SqliteDatabase = SqliteDatabase('example.db')
db.connect()
PrintLog(LogLevel.INFO, '- [成功]数据库连接')
print("Database connected")
# 定义基础模型类
class BaseModel(Model):
+17
View File
@@ -0,0 +1,17 @@
class EventBus:
def __init__(self):
self.listeners = {} # 管理各种event的订阅情况
def subscribe(self, event_type, listener):
if event_type not in self.listeners:
self.listeners[event_type] = []
self.listeners[event_type].append(listener)
def publish(self, event_type, data):
if event_type in self.listeners:
for listener in self.listeners[event_type]:
listener(data)
# 订阅与发布事件示例
# event_bus.subscribe('my_event', handle_event)
# event_bus.publish('my_event', {'key': 'value'})
+7
View File
@@ -0,0 +1,7 @@
from .eventbus import EventBus
# Pring Log
EventPrintLog = "print_log" # 打印日志
# 创建事件总线实例
loggerEBus = EventBus()
+10
View File
@@ -0,0 +1,10 @@
from eventbus import EventBus
# 市场数据监听控制事件
EventMarketActiveSwitch = "market_active_switch" # 市场数据状态变更
MarketDataUpdate = "market_data_update" # 市价更新
MarketOrderCreated = "market_order_created" # 市价单创建
MarketOrderTraded = "market_order_traded" # 市价单成交
# 创建事件总线实例
marketDataEventBus = EventBus()
-47
View File
@@ -1,47 +0,0 @@
# 市场数据监听控制事件
EventMarketActiveSwitch = "market_active_switch" # 市场数据状态变更
MarketDataUpdate = "market_data_update" # 市价更新
MarketOrderCreated = "market_order_created" # 市价单创建
MarketOrderTraded = "market_order_traded" # 市价单成交
MarketOrderError = "market_order_error" # 市价单委托失败
# Pring Log
EventPrintLog = "print_log" # 打印日志
class EventBus:
def __init__(self):
self.listeners = {} # 管理各种event的订阅情况
self.last_events = {} # 存储每个事件的最后一次值,用于"重播"给新订阅者
def subscribe(self, event_type, listener, replay=True):
"""订阅事件
Args:
event_type: 事件类型
listener: 回调函数
replay: 是否自动重播最近一次事件状态(默认True)
"""
if event_type not in self.listeners:
self.listeners[event_type] = []
self.listeners[event_type].append(listener)
# 新订阅者自动收到最近一次事件状态(如果存在)
if replay and event_type in self.last_events:
listener(self.last_events[event_type])
def publish(self, event_type, data):
# 存储最后一次事件值
self.last_events[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'})
# 创建事件总线实例
event_bus = EventBus()
+7 -30
View File
@@ -1,9 +1,7 @@
from datetime import datetime
from enum import Enum
import threading
from core.eventbus import EventPrintLog, event_bus
import config
from core.ebus.logger_ebus import EventPrintLog, loggerEBus
from core.config import config as config
class LogLevel(Enum):
@@ -16,34 +14,13 @@ class LogLevel(Enum):
def __le__(self, other):
return self.value <= other.value
class LogData:
def __init__(self, level: LogLevel, message: str):
def __init__(self, level:LogLevel, message:str):
self.level = level
self.message = message
_log_lock = threading.Lock()
def _log_file_path():
"""日志文件路径"""
return str(config.log_file_path())
def PrintLog(level: LogLevel, message: str):
def PrintLog(level:LogLevel, message:str):
data = LogData(level, message)
event_bus.publish(EventPrintLog, data)
line = f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} [{level.name}] {message}'
if config.console_log:
print(line)
# 写入日志文件
try:
with _log_lock:
with open(_log_file_path(), 'a', encoding='utf-8') as f:
f.write(line + '\n')
except Exception:
pass # 写文件失败不阻塞主流程
loggerEBus.publish(EventPrintLog, data)
if config.getConsoleLog():
print(f'{level.name} {message}')
+179
View File
@@ -0,0 +1,179 @@
# coding:utf-8
# MainEntry 负责应用主窗口与菜单的统一构建:
# - 通过 build_menu_model 定义跨平台统一的菜单数据结构
# - 在 macOS 上使用 Tk 菜单栏;在非 macOS 上使用 pystray 系统托盘
# - 所有菜单项均绑定到同名处理函数,切换平台无需改动业务逻辑
import tkinter as tk
from core.logger import LogLevel, PrintLog
import threading
import sys
class MainEntry:
def __init__(self, master):
# 初始化 Tk 窗口属性与基础状态
self.master = master
self.master.title("Main Board")
self.master.geometry("800x600")
self.master.configure(bg="#f0f0f0")
self.master.resizable(False, False)
self.master.protocol("WM_DELETE_WINDOW", self.hide_window)
# QMT 开关状态用于动态更新菜单文案
self.qmt_enabled = False
self.icon = None
# 非 macOS 使用系统托盘(pystray);macOS 使用原生菜单栏
self.systray_supported = sys.platform != "darwin"
# 主内容容器
self.main_frame = tk.Frame(self.master, bg="#f0f0f0")
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
# 首次进入根据平台构建菜单
self.create_menu()
self.create_dashboard()
def build_menu_model(self):
# 菜单模型统一描述所有菜单:
# - 每个分组包含 label 与 items
# - item 支持:label 文案、action 处理函数名、enabled 启用状态、default 默认项、separator 分隔符
# - 文案可根据状态动态生成(如 QMT 开关)
qmt_label = "QMT (已开启)" if self.qmt_enabled else "QMT (已关闭)"
return [
{
"label": "-- 交易大师 --",
"items": [
{"label": "交易复盘", "action": "handler", "enabled": True},
{"label": "市场数据", "action": "handler", "enabled": True},
{"label": "快速下单", "action": "handler", "enabled": True},
],
},
{
"label": "-- 策略交易 --",
"items": [
{"label": "交易看板", "action": "handler", "enabled": True},
{"label": "策略中心", "action": None, "enabled": False},
{"label": "策略定制", "action": None, "enabled": False},
],
},
{
"label": "-- 实时数据 --",
"items": [
{"label": qmt_label, "action": "marketDataSwitch", "enabled": True},
],
},
{
"label": "-- 系统 --",
"items": [
{"label": "控制台", "action": "show_window", "enabled": True, "default": True},
{"label": "设置", "action": "marketDataSwitch", "enabled": True},
{"separator": True},
{"label": "退出", "action": "quit_window", "enabled": True},
],
},
]
def create_dashboard(self):
# 根据菜单模型构建主窗口按钮面板
for widget in self.main_frame.winfo_children():
widget.destroy()
model = self.build_menu_model()
for group in model:
# 为每个分组创建 LabelFrame
group_frame = tk.LabelFrame(self.main_frame, text=group["label"], bg="#f0f0f0", padx=10, pady=10)
group_frame.pack(fill=tk.X, pady=10, padx=10)
for it in group["items"]:
if it.get("separator"):
continue
fn = getattr(self, it["action"]) if it.get("action") else None
state = tk.NORMAL if it.get("enabled", True) else tk.DISABLED
# 创建按钮
btn = tk.Button(group_frame, text=it["label"], command=fn, state=state)
btn.pack(side=tk.LEFT, padx=5)
def create_menu(self):
# 根据统一菜单模型与平台类型,渲染到系统托盘或 Tk 菜单栏
model = self.build_menu_model()
if self.systray_supported:
# 非 macOS:延迟导入 pystray 与 PIL,避免在 macOS 上引入不兼容依赖
from PIL import Image
import pystray
image = Image.open("logo.png")
items = []
for group in model:
# 分组标题作为禁用的头部项
items.append(pystray.MenuItem(group["label"], None, enabled=False))
for it in group["items"]:
if it.get("separator"):
items.append(pystray.Menu.SEPARATOR)
else:
fn = getattr(self, it["action"]) if it.get("action") else None
items.append(pystray.MenuItem(it["label"], fn, default=it.get("default", False), enabled=it.get("enabled", True)))
menu = tuple(items)
if self.icon:
# 已存在托盘图标:更新菜单
self.icon.menu = menu
self.icon.update_menu()
else:
# 首次创建托盘图标并在后台线程运行
self.icon = pystray.Icon("name", image, "标题", menu)
self.trayThread = threading.Thread(target=self.icon.run, daemon=True)
self.trayThread.start()
else:
# macOS:使用 Tk 菜单栏
menu_bar = tk.Menu(self.master)
for group in model:
m = tk.Menu(menu_bar, tearoff=0)
for it in group["items"]:
if it.get("separator"):
m.add_separator()
else:
fn = getattr(self, it["action"]) if it.get("action") else None
if it.get("enabled", True) and fn:
m.add_command(label=it["label"], command=fn)
else:
m.add_command(label=it["label"], state="disabled")
menu_bar.add_cascade(label=group["label"], menu=m)
self.master.config(menu=menu_bar)
def marketDataSwitch(self):
# 切换 QMT 开关,并触发菜单重建以更新文案
if self.qmt_enabled:
self.qmt_enabled = False
PrintLog(LogLevel.INFO, "QMT 市场数据已关闭")
else:
self.qmt_enabled = True
PrintLog(LogLevel.INFO, "QMT 市场数据已开启")
self.create_menu()
self.create_dashboard()
def handler(self):
# 通用占位处理:当前仅记录点击行为,后续可替换为具体业务逻辑
PrintLog(LogLevel.INFO, f"点击了")
def hide_window(self):
# 关闭窗口事件:隐藏但不退出应用
PrintLog(LogLevel.INFO, "隐藏主窗口")
self.master.withdraw() # 隐藏主窗口
def show_window(self):
# 显示主窗口;在非 macOS 平台同步让托盘图标可见
if self.icon:
self.icon.visible = True
PrintLog(LogLevel.INFO, "显示主窗口")
self.master.deiconify() # 显示主窗口
def quit_window(self, icon=None):
# 退出应用;在非 macOS 平台时关闭托盘图标
if icon:
icon.stop()
PrintLog(LogLevel.INFO, "退出应用")
self.master.quit()
self.master.destroy()
def run(self):
# 主事件循环入口
self.master.mainloop()
+262
View File
@@ -0,0 +1,262 @@
import tkinter as tk
from tkinter import ttk
from core.logger import LogLevel, LogData, PrintLog
from core.sfgrid.sfgrid_ui import TradeTargetUI
from tkinter import ttk
from core.eventbus import EventPrintLog
from core.eventbus import event_bus as eBus
class MainWindow:
def __init__(self, configLogLevel:str):
self.root = tk.Tk()
self.root.title("神之一手 - 交易系统")
self.root.geometry("1400x700")
self.logLevel = LogLevel[configLogLevel]
PrintLog(LogLevel.DEBUG, f"系统启动成功 {self.logLevel.name}")
# 当前选中的策略Tab索引
self.current_strategy_index = 0
# 存储各个Frame的引用
self.strategy_frames = {}
# 日志面板可见性标志
self.log_visible = False
self.create_ui()
eBus.subscribe(EventPrintLog, self.on_log_event)
def create_ui(self):
"""创建UI界面"""
# 主容器
main_container = ttk.Frame(self.root)
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 中间主体区域(左右布局)
content_area = ttk.Frame(main_container)
content_area.pack(fill=tk.BOTH, expand=True)
# 左侧Tab按钮栏(垂直排列)
tab_bar_frame = ttk.Frame(content_area)
tab_bar_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
# 创建自定义样式
self.create_custom_styles()
# 创建Tab按钮(垂直排列,文字垂直显示)
self.tab_buttons = []
strategy_names = ["网格", "复盘"]
for idx, name in enumerate(strategy_names):
btn = ttk.Button(
tab_bar_frame,
text=name,
command=lambda i=idx: self.switch_strategy_tab(i),
width=4,
style='Bookmark.TButton' # 使用自定义书签样式
)
btn.pack(side=tk.TOP, pady=2, fill=tk.X)
self.tab_buttons.append(btn)
# 在Tab按钮下方添加退出按钮和日志按钮(底部对齐)
# 使用一个填充Frame将按钮推到底部
spacer = ttk.Frame(tab_bar_frame)
spacer.pack(side=tk.TOP, fill=tk.X, ipady=10)
# 清空日志按钮(底部第三个)
clear_log_btn = ttk.Button(
tab_bar_frame,
text="🗑", # 垃圾桶图标
command=self.clear_logs,
width=3
)
clear_log_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
# 日志显示按钮(退出按钮上方)
self.log_toggle_btn = ttk.Button(
tab_bar_frame,
text="📋", # 日志图标
command=self.toggle_log_panel,
width=3
)
self.log_toggle_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
# 退出按钮(最底部)
exit_btn = ttk.Button(
tab_bar_frame,
text="", # 电源图标
command=self.on_exit,
width=3
)
exit_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
# 添加垂直分隔线
separator = ttk.Separator(content_area, orient='vertical')
separator.pack(side=tk.LEFT, fill=tk.Y, padx=1)
# 右侧内容区域容器(用于放置不同策略的Frame)
self.content_container = ttk.Frame(content_area)
self.content_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 创建各个策略的Frame
self.create_strategy_frames(strategy_names)
# 创建全局日志面板(默认隐藏)
self.create_global_log_panel(main_container)
# 默认显示第一个策略
self.switch_strategy_tab(0)
def create_custom_styles(self):
"""创建自定义样式"""
style = ttk.Style()
# 创建书签样式
style.configure(
'Bookmark.TButton',
relief='flat',
borderwidth=1,
padding=(5, 10),
foreground='black',
background='#FFE599', # 浅黄色背景,类似便签纸
font=('Arial', 10, 'bold')
)
# 设置焦点样式(选中状态)
style.map(
'Bookmark.TButton',
background=[('active', '#F1C232'), ('pressed', '#F1C232')],
relief=[('pressed', 'sunken')]
)
# 创建选中状态的书签样式
style.configure(
'SelectedBookmark.TButton',
relief='flat',
borderwidth=1,
padding=(5, 10),
background='#3D85C6', # 蓝色背景表示选中状态
font=('Arial', 10, 'bold')
)
def create_global_log_panel(self, parent):
"""创建全局日志面板"""
# 日志区域(默认隐藏)
self.log_frame = ttk.LabelFrame(parent, text="操作日志", padding=10)
# 默认不显示,通过工具栏按钮控制
# 创建日志表格
columns = ("timestamp", "level", "message")
self.log_table = ttk.Treeview(self.log_frame, columns=columns, show='headings', height=8)
log_column_configs = {
"timestamp": ("时间", 100),
"level": ("级别", 50),
"message": ("消息", 1150) # 调整宽度适应全局布局
}
for col in columns:
title, width = log_column_configs[col]
self.log_table.heading(col, text=title)
self.log_table.column(col, width=width, anchor=tk.W)
# 添加初始日志
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_table.insert('', tk.END, values=(timestamp, "INFO", "系统启动成功"))
# 滚动条
scrollbar = ttk.Scrollbar(self.log_frame, orient=tk.VERTICAL, command=self.log_table.yview)
self.log_table.configure(yscrollcommand=scrollbar.set)
self.log_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
def on_log_event(self, event:LogData):
if self.logLevel.value <= event.level.value:
self.add_log(event.level, event.message)
def add_log(self, level:LogLevel, message):
"""添加日志记录 - 全局方法"""
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_table.insert('', 0, values=(timestamp, level.name, message))
def clear_logs(self):
"""清空日志记录"""
# 删除所有日志项
for item in self.log_table.get_children():
self.log_table.delete(item)
def create_strategy_frames(self, strategy_names):
"""创建各个策略的Frame"""
for idx, name in enumerate(strategy_names):
if idx == 0:
# 第一个Tab使用TradeTargetUI,传入main_window引用
frame = TradeTargetUI(self.content_container)
self.strategy_frames[idx] = frame
else:
# 其他策略使用占位Frame
frame = ttk.Frame(self.content_container)
self.strategy_frames[idx] = frame
# 添加占位内容
placeholder = ttk.Label(
frame,
text=f"{name} - 策略界面将在此实现",
font=('Arial', 14),
foreground='gray'
)
placeholder.pack(expand=True)
def switch_strategy_tab(self, index):
"""切换策略Tab"""
# 隐藏当前Frame
if self.current_strategy_index in self.strategy_frames:
self.strategy_frames[self.current_strategy_index].pack_forget()
# 更新当前索引
self.current_strategy_index = index
# 显示选中的Frame
if index in self.strategy_frames:
self.strategy_frames[index].pack(fill=tk.BOTH, expand=True)
# 更新Tab按钮样式(可选,用于视觉反馈)
self.update_tab_button_styles()
def update_tab_button_styles(self):
"""更新Tab按钮的样式以显示选中状态"""
# 重置所有按钮为普通书签样式
for i, btn in enumerate(self.tab_buttons):
if i == self.current_strategy_index:
btn.configure(style='SelectedBookmark.TButton') # 选中状态
else:
btn.configure(style='Bookmark.TButton') # 普通状态
def toggle_log_panel(self):
"""切换日志面板的显示/隐藏"""
if self.log_visible:
# 隐藏日志面板
self.log_frame.pack_forget()
self.log_visible = False
self.log_toggle_btn.config(text="📋") # 日志图标
else:
# 显示日志面板
self.log_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0))
self.log_visible = True
self.log_toggle_btn.config(text="🔽") # 使用不同图标表示隐藏
def on_exit(self):
"""退出程序"""
from tkinter import messagebox
result = messagebox.askyesno("确认退出", "确定要退出系统吗?")
if result:
self.root.destroy()
def run(self):
"""运行程序"""
self.root.mainloop()
+8
View File
@@ -0,0 +1,8 @@
from qmt import QmtV
from eventbus import marketDataEventBus
qmtv:QmtV = None
def init_qmtv():
global qmtv
qmtv = QmtV()
+209
View File
@@ -0,0 +1,209 @@
import datetime
import threading
import time
import config
from xtquant.xttype import StockAccount, XtOrder, XtOrderResponse, XtPosition, XtTrade
from xtquant.xttrader import XtQuantTrader
from xtquant.xttype import StockAccount
from core.logger import LogLevel, PrintLog
from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount
from xtquant import xtconstant, xtdata
from eventbus import marketDataEventBus, EventMarketActiveSwitch, MarketDataUpdate, MarketOrderCreated, MarketOrderTraded
class QmtV(XtQuantTraderCallback):
def __init__(self) -> None:
self.xttrader: XtQuantTrader
self.inited: bool = False
self.details = {}
self.lastMarketDataUpdateTimestamp = time.time()
self.isMarketActive = False
self.refresh_thread = threading.Thread(target=self.marketStatusNotifier, daemon=True)
self.refresh_thread.start()
def getTrader(self) -> XtQuantTrader:
return self.xttrader
def init_qmtv(self):
sessionId= int(time.time())
self.xttrader = XtQuantTrader(config.miniQMTPath, sessionId)
xtdata.enable_hello = False
def connect(self) -> bool:
self.xttrader.register_callback(self)
self.xttrader.start()
self.xttrader.connect()
PrintLog(LogLevel.INFO, f'- [{'成功' if self.xttrader.connected else '失败'}]市场交易连接: {config.miniQMTPath}')
if self.xttrader.connected == False:
self.inited = False
return self.inited
else:
self.inited = True
self.account = StockAccount(config.miniQMTAccount, 'STOCK') # pyright: ignore[reportAssignmentType, reportAttributeAccessIssue]
PrintLog(LogLevel.INFO, f'- [成功]交易账号对象初始化完成, 账号: {config.miniQMTAccount}') # pyright: ignore[reportOptionalMemberAccess]
subscribe_result = self.xttrader.subscribe(self.account)
PrintLog(LogLevel.INFO, f'- [{'成功' if subscribe_result == 0 else '失败'}:{subscribe_result}]交易状态订阅')
if subscribe_result != 0:
self.inited = False
return self.inited
self.startMarketDataSubscription()
return self.inited
def getStockPosition(self, stock_code: str):
positions = self.xttrader.query_stock_positions(self.account)
if positions:
for temp in positions:
pos:XtPosition = temp
if pos.stock_code == stock_code:
return pos
return None
def queryPendingOrder(self, stock_code:str, tag: str) -> list[XtOrder]:
if stock_code == None or tag == None:
return []
orders = self.xttrader.query_stock_orders(self.account)
result = [order for order in orders if order.order_status == xtconstant.ORDER_REPORTED and order.stock_code == stock_code and order.strategy_name == tag]
return result
def orderAsync(self, stock_code, orderVolume, orderType, orderPrice, priceType, orderRemark, strategy_name):
return self.xttrader.order_stock_async(
self.account,
str(stock_code),
orderType,
orderVolume,
priceType,
orderPrice,
strategy_name, # strategy_name
orderRemark # remark # type: ignore
)
def cacheStockDetail(self, stock_code:str):
if stock_code in self.details:
return self.details[stock_code]
else:
self.details[stock_code] = xtdata.get_instrument_detail(stock_code, False)
return self.details[stock_code]
def getInstrumentName(self, stock_code:str):
return self.cacheStockDetail(stock_code)['InstrumentName']
def dailyUpStop(self, stock_code:str):
cacheStock = self.cacheStockDetail(stock_code)
PrintLog(LogLevel.INFO, f'- [成功]获取股票详情: {stock_code} {cacheStock["InstrumentName"]} {cacheStock["UpStopPrice"]}')
return cacheStock['UpStopPrice']
def dailyDownStop(self, stock_code:str):
return self.cacheStockDetail(stock_code)['DownStopPrice']
# ========================================#
def startMarketDataSubscription(self):
try:
self.subscriptionId = xtdata.subscribe_whole_quote(['SH', 'SZ'], self.onDataUpdate)
PrintLog(LogLevel.INFO, f'- [市场数据订阅成功-{self.subscriptionId}]')
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [市场数据订阅失败-{e}]')
def stopMarketDataSubscription(self):
PrintLog(LogLevel.INFO, '- 停止市场数据订阅')
if self.subscriptionId is not None and self.subscriptionId > 0:
xtdata.unsubscribe_quote(self.subscriptionId)
# ====== 市场回调方法 -- 以下方法由XtQuantData调用 ======
def onDataUpdate(self, data):
# 收集所有市场数据用于市场监控
marketDataEventBus.publish(marketDataEventBus.MarketDataUpdate, data)
now = time.time()
if now - self.lastMarketDataUpdateTimestamp < 5:
self.isMarketActive = True
self.lastMarketDataUpdateTimestamp = now
def marketStatusNotifier(self):
# 市场状态通知器
tmpMarketStatus = False
while True:
tmpTime = time.time()
time.sleep(10)
if tmpMarketStatus != self.isMarketActive and tmpTime - self.lastMarketDataUpdateTimestamp < 5:
tmpMarketStatus = self.isMarketActive
PrintLog(LogLevel.INFO, f'- [市场状态变更] {self.isMarketActive}')
marketDataEventBus.publish(EventMarketActiveSwitch, self.isMarketActive)
if tmpMarketStatus and self.isMarketActive and tmpTime - self.lastMarketDataUpdateTimestamp > 10: # 上次更新市场状态已经超过10秒
self.isMarketActive = False
PrintLog(LogLevel.INFO, f'- [市场状态变更] {self.isMarketActive}')
PrintLog(LogLevel.DEBUG, f'- [市场状态] {self.isMarketActive}') # 市场已 inactive
# ====== 市场回调方法 -- 以下方法由XtQuantTrader调用 ======
def on_connected(self):
"""
连接成功推送
"""
print(datetime.datetime.now(), '连接成功回调')
def on_disconnected(self):
"""
连接断开
:return:
"""
print(datetime.datetime.now(), '连接断开回调')
def on_stock_order(self, order:XtOrder):
"""
委托回报推送
:param order: XtOrder对象
:return:
"""
pass
# print(f"委托回调 on_stock_order 投资备注 {order.order_id} {order.strategy_name} {order.order_remark}")
def on_stock_trade(self, trade:XtTrade):
"""
成交变动推送
:param trade: XtTrade对象
:return:
"""
marketDataEventBus.publish(MarketOrderTraded, trade)
# stockCode = trade.stock_code
# ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# # 如果存在对应的StockTradeController,则调用其onDataUpdate方法
# if ctrl is not None and trade.strategy_name == ctrl.getName():
# ctrl.onOrderTrade(trade)
# else:
# print(f"委托回调 投资备注 {trade.strategy_name} 不匹配 {ctrl.getName()}")
def on_order_stock_async_response(self, response:XtOrderResponse):
# print(f"委托回调 on_order_stock_async_response 投资备注 {response.order_id} {response.seq} {response.error_msg}{response.strategy_name} {response.order_remark}")
marketDataEventBus.publish(MarketOrderCreated, response)
# stockCode = response.order_remark
# ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# # 如果存在对应的StockTradeController,则调用其onDataUpdate方法
# if ctrl is not None and response.strategy_name == ctrl.getName():
# ctrl.onAsyncOrderResponse(response)
# else:
# print(f"委托回调 投资备注 {response.strategy_name} 不匹配 {ctrl.getName()}")
def on_order_error(self, order_error):
"""
委托失败推送
:param order_error:XtOrderError 对象
:return:
"""
print(f"\n委托报错回调 {order_error.order_remark} {order_error.error_msg}")
def on_account_status(self, status):
"""
:param response: XtAccountStatus 对象
:return:
"""
print(datetime.datetime.now(), status)
+7
View File
@@ -0,0 +1,7 @@
from peewee import CharField, DateField
from core.database import BaseModel, db
class StockInfo(BaseModel):
stock_code = CharField(unique=True, primary_key=True)
stock_name = CharField()
-32
View File
@@ -1,32 +0,0 @@
"""
QMT 模块统一入口
根据配置或环境自动选择真实 QMT 或模拟器
"""
import sys
import config as _config
def _get_qmt():
"""获取 QMT 模块(配置优先于平台检测)"""
if _config.use_simulated_qmt:
print('[qmt] 配置指定模拟模式 → qmt_dummy')
from core.qmt_dummy import qmtv
return qmtv
if sys.platform == 'win32':
try:
print('[qmt] Windows 平台,尝试加载 qmt_real...')
from core.qmt_real import qmtv as real_qmtv
print('[qmt] qmt_real 加载成功')
return real_qmtv
except ImportError as e:
print(f'[qmt] qmt_real 加载失败: {e},回退 qmt_dummy')
# 非 Windows 或导入失败,使用模拟器
print('[qmt] 使用模拟模式 qmt_dummy')
from core.qmt_dummy import qmtv
return qmtv
# 导出单例
qmtv = _get_qmt()
-314
View File
@@ -1,314 +0,0 @@
"""
Dummy QMT 模拟器 - 用于在非 Windows 环境下模拟 QMT 交易功能
"""
import datetime
import threading
import time
import random
import config
import core.eventbus as eBus
from core.logger import LogLevel, PrintLog
class DummyPosition:
"""模拟持仓"""
def __init__(self, stock_code, stock_name, volume, yesterday_vol=0):
self.stock_code = stock_code
self.stock_name = stock_name
self.volume = volume
self.can_use_volume = volume
self.yesterday_volume = yesterday_vol
class DummyOrder:
"""模拟订单"""
def __init__(self, stock_code, order_id, status, price, volume):
self.stock_code = stock_code
self.order_id = order_id
self.order_status = status
self.order_price = price
self.volume = volume
class DummyTrade:
"""模拟成交"""
def __init__(self, stock_code, trade_id, price, volume, strategy_name):
self.stock_code = stock_code
self.trade_id = trade_id
self.trade_price = price
self.trade_volume = volume
self.strategy_name = strategy_name
class DummyOrderResponse:
"""模拟下单响应"""
def __init__(self, order_id, stock_code, seq, error_msg, strategy_name):
self.order_id = order_id
self.stock_code = stock_code
self.seq = seq
self.error_msg = error_msg
self.strategy_name = strategy_name
class DummyQmtV:
"""
Dummy QMT 模拟器
模拟 QmtV 类的接口,用于在没有 miniQMT 的环境下运行和测试
"""
def __init__(self) -> None:
self.inited = False
self.details = {}
self.lastMarketDataUpdateTimestamp = time.time()
self.isMarketActive = True
self.connected = False
self.account = None
self._positions = {}
self._pending_orders = []
self._market_data_thread = None
self._counter = 0
def getTrader(self):
return self
def init_qmtv(self):
"""初始化交易器"""
PrintLog(LogLevel.INFO, f'- [模拟] QMT 交易器初始化')
self.connected = True
self.inited = True
def connect(self) -> bool:
"""连接 QMT (模拟总是成功)"""
PrintLog(LogLevel.INFO, f'- [成功] 市场交易连接 (模拟模式)')
# 创建模拟账号
try:
from xtquant.xttype import StockAccount
self.account = StockAccount(config.account_no, 'STOCK')
except ImportError:
self.account = type('StockAccount', (), {'account_id': config.account_no})()
PrintLog(LogLevel.INFO, f'- [成功] 交易账号: {config.account_no}')
self._init_dummy_positions()
self.startMarketDataSubscription()
return self.inited
def _init_dummy_positions(self):
"""初始化模拟持仓数据"""
dummy_stocks = [
('600519', '贵州茅台', 100, 2800.0),
('000858', '五粮液', 200, 180.0),
('600036', '招商银行', 500, 42.0),
('000001', '平安银行', 300, 13.5),
]
for code, name, volume, price in dummy_stocks:
self._positions[code] = {
'stock_code': code,
'stock_name': name,
'volume': volume,
'can_use_volume': volume,
'open_cost': price,
'market_value': volume * price
}
PrintLog(LogLevel.INFO, f'- [模拟] 已加载 {len(self._positions)} 个持仓')
def getAllPositions(self) -> dict:
"""获取全部持仓,返回 {stock_code: position_object}"""
result = {}
for code, pos_data in self._positions.items():
result[code] = type('DummyPos', (), pos_data)()
return result
def getStockPosition(self, stock_code: str):
"""获取持仓 (模拟)"""
if stock_code in self._positions:
pos = self._positions[stock_code]
return type('DummyPos', (), pos)()
return None
def queryTodayOrders(self) -> list:
"""查询当日所有委托 (模拟)"""
return list(self._pending_orders)
def queryTodayTrades(self) -> list:
"""查询当日所有成交 (模拟)"""
return [] # 模拟模式无实际成交记录
def queryPendingOrder(self, stock_code: str, tag: str) -> list:
"""查询挂单"""
return [o for o in self._pending_orders
if o.stock_code == stock_code and
(tag is None or getattr(o, 'strategy_name', None) == tag)]
def orderAsync(self, stock_code, orderVolume, orderType, orderPrice, priceType, orderRemark, strategy_name):
"""异步下单 (模拟)"""
self._counter += 1
order_id = f"DUMMY{self._counter:06d}"
seq = self._counter
order = DummyOrder(
stock_code=stock_code,
order_id=order_id,
status='reported',
price=orderPrice,
volume=orderVolume
)
order.strategy_name = strategy_name
order.order_remark = orderRemark
self._pending_orders.append(order)
response = DummyOrderResponse(
order_id=order_id,
stock_code=stock_code,
seq=seq,
error_msg='成功',
strategy_name=strategy_name
)
response.order_remark = orderRemark
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
PrintLog(LogLevel.INFO, f'- [模拟下单] {stock_code} 数量:{orderVolume} 价格:{orderPrice} 订单号:{order_id}')
# 模拟成交 (80% 概率)
if random.random() > 0.2:
threading.Timer(random.uniform(0.5, 3.0), self._simulate_trade,
args=(stock_code, order_id, orderPrice, orderVolume, strategy_name)).start()
return 0
def _simulate_trade(self, stock_code, order_id, price, volume, strategy_name):
"""模拟成交"""
trade = DummyTrade(
stock_code=stock_code,
trade_id=f"TRADE{self._counter:06d}",
price=price,
volume=volume,
strategy_name=strategy_name
)
trade.trade_time = int(time.strftime('%H%M%S'))
trade.order_remark = stock_code
if stock_code in self._positions:
self._positions[stock_code]['volume'] += volume
self._positions[stock_code]['can_use_volume'] += volume
eBus.event_bus.publish(eBus.MarketOrderTraded, trade)
PrintLog(LogLevel.INFO, f'- [模拟成交] {stock_code} 数量:{volume} 价格:{price}')
def cacheStockDetail(self, stock_code: str):
"""获取股票详情 (模拟)"""
if stock_code not in self.details:
self.details[stock_code] = {
'InstrumentName': self._get_dummy_name(stock_code),
'UpStopPrice': 0,
'DownStopPrice': 0
}
return self.details[stock_code]
def _get_dummy_name(self, stock_code: str) -> str:
"""获取模拟股票名称"""
names = {
'600519': '贵州茅台', '000858': '五粮液', '600036': '招商银行',
'000001': '平安银行', '000002': '万科A', '600000': '浦发银行'
}
return names.get(stock_code, f'股票{stock_code}')
def getInstrumentName(self, stock_code: str) -> 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"]} 涨停价: 0')
return 0.0
def dailyDownStop(self, stock_code: str):
"""获取跌停价 (模拟)"""
return 0.0
def getLastPrice(self, stock_code: str) -> float:
"""主动获取最新市价(模拟)"""
if stock_code in self._positions:
return float(self._positions[stock_code].get('open_cost', 10.0))
# 给一个合理模拟价
return 10.0 + hash(stock_code) % 100
def startMarketDataSubscription(self):
"""启动市场数据订阅 (模拟)"""
try:
self._market_data_thread = threading.Thread(target=self._generate_market_data, daemon=True)
self._market_data_thread.start()
PrintLog(LogLevel.INFO, f'- [市场数据订阅成功-模拟]')
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [市场数据订阅失败-{e}]')
def stopMarketDataSubscription(self):
"""停止市场数据订阅"""
PrintLog(LogLevel.INFO, '- 停止市场数据订阅 (模拟)')
def _is_trading_time(self) -> bool:
import zoneinfo
beijing_tz = zoneinfo.ZoneInfo('Asia/Shanghai')
now = datetime.datetime.now(beijing_tz)
if now.weekday() >= 5:
return False
t = now.time()
return (
datetime.time(9, 30) <= t <= datetime.time(11, 30) or
datetime.time(13, 0) <= t <= datetime.time(15, 0)
)
def _generate_market_data(self):
"""生成模拟市场数据"""
stocks = ['600519', '000858', '600036', '000001', '000002', '600000']
base_prices = [2800.0, 180.0, 42.0, 13.5, 10.0, 10.0]
while True:
try:
for i, stock in enumerate(stocks):
data = {
'stock_code': stock,
'last_price': base_prices[i] + random.uniform(-1, 1),
'open_price': base_prices[i],
'high_price': base_prices[i] + random.uniform(0, 2),
'low_price': base_prices[i] - random.uniform(0, 2),
'volume': random.randint(1000, 10000),
'timestamp': time.time()
}
eBus.event_bus.publish(eBus.MarketDataUpdate, data)
base_prices[i] = data['last_price']
self.lastMarketDataUpdateTimestamp = time.time()
if self._is_trading_time():
self.isMarketActive = True
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, True)
else:
self.isMarketActive = False
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, False)
time.sleep(3)
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [市场数据模拟异常-{e}]')
time.sleep(1)
def on_connected(self):
print(datetime.datetime.now(), '模拟连接成功')
def on_disconnected(self):
print(datetime.datetime.now(), '模拟连接断开')
def on_stock_order(self, order):
pass
def on_stock_trade(self, trade):
eBus.event_bus.publish(eBus.MarketOrderTraded, trade)
def on_order_stock_async_response(self, response):
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
def on_order_error(self, order_error):
print(f"\n模拟委托报错回调: order_id={order_error.order_id}, error_id={order_error.error_id}, error_msg={order_error.error_msg}, remark={order_error.order_remark}")
eBus.event_bus.publish(eBus.MarketOrderError, order_error)
def on_account_status(self, status):
print(datetime.datetime.now(), status)
qmtv = DummyQmtV()
-648
View File
@@ -1,648 +0,0 @@
"""
QMT 真实交易实现 - 封装 xtquant SDK
"""
import datetime
import os
import subprocess
import threading
import time
import config
import core.eventbus as eBus
from core.logger import LogLevel, PrintLog
class RealQmtV:
"""
真实 QMT 交易器
封装 xtquant 的 XtQuantTrader,提供与模拟器一致的接口
"""
# miniQMT 进程名关键字(GUI 壳: XtMiniQmt.exe,交易引擎: miniquote.exe
_QMT_PROCESS_KEYWORDS = ['Qmt', 'qmt', 'QMT', 'miniquote', 'MiniQuote']
@staticmethod
def _discover_qmt_port() -> int:
"""
自动探测 miniQMT 监听端口。
方法1: SDK 内部扫描 (读取配置)
方法2: netstat 找 LISTENING 端口 → 反查所属进程名 → 匹配 QMT 关键字
返回端口号,未找到返回 0。
"""
# ---- 方法1: SDK 内部扫描 ----
try:
from xtquant import xtconn
addrs = xtconn.scan_available_server_addr()
for addr in addrs:
try:
port = int(addr.split(':')[1])
if port:
PrintLog(LogLevel.INFO, f'[端口探测] SDK 扫描发现端口: {port}')
return port
except (ValueError, IndexError):
continue
except Exception as e:
PrintLog(LogLevel.DEBUG, f'[端口探测] SDK 扫描异常: {e}')
# ---- 方法2: netstat → 反向查进程名 ----
try:
# 2a. netstat 找出所有 LISTENING 端口的 PID
pid_ports = {} # pid -> [port, ...]
netstat = subprocess.run(
['netstat', '-ano'],
capture_output=True, text=True, timeout=10
)
for line in netstat.stdout.splitlines():
if 'LISTENING' not in line and 'LISTEN' not in line:
continue
parts = line.split()
if len(parts) < 5:
continue
try:
local_addr = parts[1]
port = int(local_addr.rsplit(':', 1)[-1])
pid = int(parts[-1])
if port > 0:
pid_ports.setdefault(pid, []).append(port)
except (ValueError, IndexError):
continue
if not pid_ports:
PrintLog(LogLevel.DEBUG, '[端口探测] netstat 未找到任何 LISTENING 端口')
return 0
# 2b. 对每个有监听端口的 PID,查进程名是否匹配 QMT
for pid, ports in pid_ports.items():
name = RealQmtV._get_process_name(pid)
if name and any(kw in name for kw in RealQmtV._QMT_PROCESS_KEYWORDS):
port = ports[0]
PrintLog(LogLevel.INFO, f'[端口探测] 发现 QMT 进程: {name} (PID={pid}), 端口: {port}')
# 同时探测 userdata_mini 路径
exe_path = RealQmtV._get_process_exe_path(pid)
if exe_path:
PrintLog(LogLevel.INFO, f'[路径探测] 进程路径: {exe_path}')
found_path = RealQmtV._find_userdata_mini(exe_path)
if found_path:
PrintLog(LogLevel.INFO, f'[路径探测] 发现 userdata_mini: {found_path}')
if found_path != config.miniQMTPath:
PrintLog(LogLevel.INFO, f'[路径探测] 自动修正 miniQMTPath: {config.miniQMTPath} -> {found_path}')
config.miniQMTPath = found_path
# 同时从窗口标题提取资金账号
account = RealQmtV._discover_account()
if account:
if account != config.account_no:
PrintLog(LogLevel.INFO, f'[账号探测] 自动修正 account_no: {config.account_no[-4:]}**** -> {account[-4:]}****')
config.account_no = account
else:
PrintLog(LogLevel.INFO, f'[账号探测] 确认账号: {account[-4:]}****')
return port
except Exception as e:
PrintLog(LogLevel.INFO, f'[端口探测] 进程扫描异常: {e}')
PrintLog(LogLevel.WARNING, '[端口探测] 未能自动发现 miniQMT 端口')
return 0
@staticmethod
def _get_process_name(pid: int) -> str:
"""通过 PID 获取进程名(单个查询,不用扫全量 tasklist)"""
try:
result = subprocess.run(
['tasklist', '/fi', f'PID eq {pid}', '/fo', 'csv', '/nh'],
capture_output=True, text=True, timeout=5
)
for line in result.stdout.splitlines():
line = line.strip()
if not line or line.startswith('INFO:'):
continue
parts = [p.strip('"').strip() for p in line.split('","')]
if len(parts) >= 2:
return parts[0]
except Exception:
pass
return ''
@staticmethod
def _get_process_exe_path(pid: int) -> str:
"""通过 PID 获取进程的可执行文件完整路径"""
try:
result = subprocess.run(
['powershell', '-NoProfile', '-Command',
f'(Get-Process -Id {pid}).Path'],
capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=5
)
path = result.stdout.strip()
if path and os.path.isfile(path):
return path
except Exception:
pass
return ''
@staticmethod
def _find_userdata_mini(exe_path: str) -> str:
"""从 QMT 可执行文件路径向上查找 userdata_mini 目录"""
exe_dir = os.path.dirname(exe_path)
# 从 exe 所在目录开始,向上最多 3 层
for _ in range(4):
candidate = os.path.join(exe_dir, 'userdata_mini')
if os.path.isdir(candidate):
return candidate
parent = os.path.dirname(exe_dir)
if parent == exe_dir:
break
exe_dir = parent
return ''
@staticmethod
def _discover_account() -> str:
"""
从 XtMiniQmt.exe 的窗口标题中提取资金账号。
标题格式: "8882874667 - 国金证券QMT交易端 2.0.8.300"
返回账号字符串,失败返回空字符串。
"""
try:
# 找到 XtMiniQmt.exe 的 PID
tasklist = subprocess.run(
['tasklist', '/fo', 'csv', '/nh'],
capture_output=True, text=True, timeout=10
)
gui_pid = 0
for line in tasklist.stdout.splitlines():
line = line.strip()
if not line:
continue
parts = [p.strip('"').strip() for p in line.split('","')]
if len(parts) >= 2 and 'XtMiniQmt' in parts[0]:
gui_pid = int(parts[1])
break
if not gui_pid:
return ''
# 获取窗口标题(PowerShell 输出可能含中文,用 utf-8)
result = subprocess.run(
['powershell', '-NoProfile', '-Command',
f'(Get-Process -Id {gui_pid}).MainWindowTitle'],
capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=5
)
title = result.stdout.strip()
if title and ' - ' in title:
account = title.split(' - ')[0].strip()
if account.isdigit():
return account
except Exception:
pass
return ''
@staticmethod
def _to_plain_code(stock_code: str) -> str:
"""将 xtquant 格式 '600519.SH' 转换为数据库格式 '600519'"""
return stock_code.split('.')[0] if '.' in stock_code else stock_code
@staticmethod
def _to_full_code(stock_code: str) -> str:
"""将数据库格式 '600519' 转换为 xtquant 格式 '600519.SH'"""
if '.' in stock_code:
return stock_code # already has suffix
code = stock_code
if code.startswith(('6', '5', '9')):
return f'{code}.SH'
elif code.startswith(('0', '3', '2')):
return f'{code}.SZ'
# fallback: try both, prefer SH
return f'{code}.SH'
def __init__(self) -> None:
self.inited = False
self.connected = False
self.account = None
self.xt_trader = None
self.mini_qmt_path = ""
self._positions = {}
self._pending_orders = []
self._market_data_thread = None
self.isMarketActive = False
self.lastMarketDataUpdateTimestamp = time.time()
self.details = {}
def getTrader(self):
return self
def init_qmtv(self):
"""初始化 QMT 交易器"""
try:
from xtquant.xttrader import XtQuantTrader
from xtquant.xttype import StockAccount
self.mini_qmt_path = config.miniQMTPath
self.account = StockAccount(config.account_no, 'STOCK')
PrintLog(LogLevel.INFO, f'[QMT] 初始化: path={self.mini_qmt_path}, account={config.account_no[-4:]}****')
# 创建 XtQuantTrader 实例
session_id = int(time.time()) % 10000
PrintLog(LogLevel.INFO, f'[QMT] 创建 XtQuantTrader, session={session_id}')
self.xt_trader = XtQuantTrader(self.mini_qmt_path, session_id)
# 注册回调 — xtquant 只接受一个回调对象,会在上面调用 on_xxx 方法
self.xt_trader.register_callback(self)
self.inited = True
PrintLog(LogLevel.INFO, f'- [真实] QMT 交易器初始化成功')
except Exception as e:
self.inited = False
PrintLog(LogLevel.ERROR, f'- [失败] QMT 初始化: {e}')
def connect(self) -> bool:
"""连接 MiniQMT,失败自动探测端口并重试"""
if not self.inited:
PrintLog(LogLevel.ERROR, '[QMT] 连接失败: 未初始化')
return False
_connect_errors = {
0: '成功',
-1: '一般错误(miniQMT 可能未启动)',
-2: 'miniQMT 未运行(请先启动极简QMT)',
-3: '连接超时',
}
def _do_connect() -> int:
self.xt_trader.start()
PrintLog(LogLevel.INFO, '[QMT] xt_trader.start() 完成')
PrintLog(LogLevel.INFO, '[QMT] 正在连接 miniQMT...')
return self.xt_trader.connect()
try:
# 尝试默认连接
PrintLog(LogLevel.INFO, '[QMT] 尝试默认方式连接...')
connect_result = _do_connect()
# 失败则自动探测端口并重试
if connect_result != 0:
PrintLog(LogLevel.INFO, '[QMT] 默认连接失败,启动端口自动探测...')
discovered_port = self._discover_qmt_port()
if discovered_port > 0:
PrintLog(LogLevel.INFO, f'[QMT] 探测到端口 {discovered_port},尝试连接...')
try:
from xtquant import xtdata
xtdata.connect(ip='127.0.0.1', port=discovered_port)
PrintLog(LogLevel.INFO, f'[QMT] xtdata 连接成功 (端口: {discovered_port})')
except Exception as e:
PrintLog(LogLevel.ERROR, f'[QMT] xtdata 连接失败 (端口: {discovered_port}): {e}')
return False
connect_result = _do_connect()
else:
PrintLog(LogLevel.WARNING, '[QMT] 端口自动探测未找到 miniQMT 进程')
result_desc = _connect_errors.get(connect_result, f'未知({connect_result})')
PrintLog(LogLevel.INFO, f'[QMT] connect() 返回: {connect_result} ({result_desc})')
if connect_result == 0:
PrintLog(LogLevel.INFO, f'[QMT] 订阅账户...')
self.xt_trader.subscribe(self.account)
PrintLog(LogLevel.INFO, '[QMT] 订阅完成')
self.connected = True
self.startMarketDataSubscription()
PrintLog(LogLevel.INFO, f'[QMT] 连接成功 (账号: {config.account_no[-4:]}****)')
return True
else:
PrintLog(LogLevel.ERROR, f'[QMT] 连接失败: {result_desc}')
return False
except Exception as e:
PrintLog(LogLevel.ERROR, f'[QMT] 连接异常: {e}')
return False
def getAllPositions(self) -> dict:
"""获取全部持仓,返回 {plain_code: position_object}"""
if not self.connected:
return {}
try:
positions = self.xt_trader.query_stock_positions(self.account)
result = {}
for pos in positions:
code = self._to_plain_code(getattr(pos, 'stock_code', ''))
result[code] = pos
# 缓存以供 getStockPosition 使用
self._position_cache = result
return result
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [获取全部持仓失败]: {e}')
return {}
def getStockPosition(self, stock_code: str):
"""获取单只股票持仓(优先使用缓存)"""
if not self.connected:
return None
try:
# 优先查缓存
if hasattr(self, '_position_cache') and stock_code in self._position_cache:
return self._position_cache[stock_code]
# 回退查询
positions = self.xt_trader.query_stock_positions(self.account)
for pos in positions:
pos_code = self._to_plain_code(getattr(pos, 'stock_code', ''))
if pos_code == stock_code:
return pos
return None
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [持仓查询失败] {stock_code}: {e}')
return None
def queryPendingOrder(self, stock_code: str, tag: str) -> list:
"""查询挂单(过滤已撤/废单)"""
if not self.connected:
return []
try:
orders = self.xt_trader.query_stock_orders(self.account)
# 过滤已撤(54)和废单(57),避免策略误判"已有挂单"跳过下单
_CANCELED = {54, 57}
return [o for o in orders
if self._to_plain_code(getattr(o, 'stock_code', '')) == stock_code and
(tag is None or getattr(o, 'strategy_name', None) == tag) and
getattr(o, 'order_status', 0) not in _CANCELED]
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [查询挂单失败] {e}')
return []
def queryTodayOrders(self) -> list:
"""查询当日所有委托"""
if not self.connected:
return []
try:
return list(self.xt_trader.query_stock_orders(self.account))
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [查询委托失败] {e}')
return []
def queryTodayTrades(self) -> list:
"""查询当日所有成交"""
if not self.connected:
return []
try:
return list(self.xt_trader.query_stock_trades(self.account))
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [查询成交失败] {e}')
return []
def orderAsync(self, stock_code, orderVolume, orderType, orderPrice, priceType, orderRemark, strategy_name):
"""异步下单"""
if not self.connected:
PrintLog(LogLevel.ERROR, '- [下单失败] 未连接')
return -1
try:
full_code = self._to_full_code(stock_code)
seq = self.xt_trader.order_stock_async(
account=self.account,
stock_code=full_code,
order_volume=orderVolume,
order_type=orderType,
price=orderPrice,
price_type=priceType,
order_remark=orderRemark,
strategy_name=strategy_name
)
PrintLog(LogLevel.INFO,
f'- [下单] {stock_code} 数量:{orderVolume} 价格:{orderPrice} 类型:{orderType} seq:{seq}')
return 0
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [下单失败] {stock_code}: {e}')
return -1
def cacheStockDetail(self, stock_code: str):
"""获取股票详情"""
if stock_code not in self.details:
try:
from xtquant import xtdata
# xtquant 需要带后缀的完整代码
full_code = self._to_full_code(stock_code)
detail = xtdata.get_instrument_detail(full_code)
if detail:
# xtquant 返回 dict,使用 .get() 读取
self.details[stock_code] = {
'InstrumentName': detail.get('InstrumentName', stock_code) if isinstance(detail, dict) else getattr(detail, 'InstrumentName', stock_code),
'UpStopPrice': detail.get('UpStopPrice', 0) if isinstance(detail, dict) else getattr(detail, 'UpStopPrice', 0),
'DownStopPrice': detail.get('DownStopPrice', 0) if isinstance(detail, dict) else getattr(detail, 'DownStopPrice', 0)
}
else:
self.details[stock_code] = {
'InstrumentName': stock_code,
'UpStopPrice': 0,
'DownStopPrice': 0
}
except Exception:
self.details[stock_code] = {
'InstrumentName': stock_code,
'UpStopPrice': 0,
'DownStopPrice': 0
}
return self.details[stock_code]
def getInstrumentName(self, stock_code: str) -> str:
"""获取股票名称"""
return self.cacheStockDetail(stock_code)['InstrumentName']
def getInstrumentName_batch(self, stock_codes: list) -> dict:
"""批量获取股票名称,返回 {stock_code: name} dict"""
result = {}
missing = []
for code in stock_codes:
if code in self.details:
result[code] = self.details[code].get('InstrumentName', '')
else:
missing.append(code)
if not missing:
return result
try:
from xtquant import xtdata
for code in missing:
full_code = self._to_full_code(code)
detail = xtdata.get_instrument_detail(full_code)
if detail:
name = detail.get('instrumentName', detail.get('InstrumentName', ''))
self.details[code] = detail
result[code] = name
else:
result[code] = ''
except Exception:
for code in missing:
result[code] = ''
return result
def dailyUpStop(self, stock_code: str):
"""获取涨停价"""
detail = self.cacheStockDetail(stock_code)
up_stop = detail.get('UpStopPrice', 0)
PrintLog(LogLevel.DEBUG, f'- [详情] {stock_code} {detail["InstrumentName"]} 涨停价: {up_stop}')
return up_stop or 0.0
def dailyDownStop(self, stock_code: str):
"""获取跌停价"""
detail = self.cacheStockDetail(stock_code)
down_stop = detail.get('DownStopPrice', 0)
return down_stop or 0.0
def getLastPrice(self, stock_code: str) -> float:
"""主动获取最新市价(拉取模式,作为推送的兜底)"""
try:
from xtquant import xtdata
import json
full_code = self._to_full_code(stock_code)
# 方式1: 尝试 get_full_tick(参数是 list[str],返回 dict {code: {...}}
raw = xtdata.get_full_tick([full_code])
if raw:
tick = json.loads(raw) if isinstance(raw, str) else raw
if isinstance(tick, dict):
# 格式: {'600519.SH': {'lastPrice': 8.97, ...}}
for code, info in tick.items():
if isinstance(info, dict) and info.get('lastPrice', 0) > 0:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → tick: {info["lastPrice"]:.3f}')
return float(info['lastPrice'])
# 方式2: get_market_data 取最新1分钟K线收盘价
data = xtdata.get_market_data(
field_list=['close'],
stock_list=[full_code],
period='1m',
count=1
)
if data:
vals = None
if full_code in data:
row = data[full_code]
if hasattr(row, '__iter__') and not isinstance(row, str):
row = list(row)
if row:
vals = row
if not vals and 'close' in data:
field_data = data['close']
if full_code in field_data:
vals = list(field_data[full_code])
if vals and len(vals) > 0 and float(vals[0]) > 0:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → kline: {float(vals[0]):.3f}')
return float(vals[0])
# 方式3: 下载历史数据后再试
xtdata.download_history_data(full_code, '1m', '')
data = xtdata.get_market_data(
field_list=['close'],
stock_list=[full_code],
period='1m',
count=1
)
if data:
vals = None
if full_code in data:
row = data[full_code]
if hasattr(row, '__iter__') and not isinstance(row, str):
row = list(row)
if row:
vals = row
if not vals and 'close' in data:
field_data = data['close']
if full_code in field_data:
vals = list(field_data[full_code])
if vals and len(vals) > 0 and float(vals[0]) > 0:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → download+kline: {float(vals[0]):.3f}')
return float(vals[0])
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → 失败: 所有方式均无数据, raw={raw}')
except Exception as e:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → 异常: {e}')
return 0.0
def startMarketDataSubscription(self):
"""启动市场数据订阅"""
try:
from xtquant import xtdata
# 订阅沪深全市场实时行情
seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], self._on_market_data)
PrintLog(LogLevel.INFO, f'- [市场数据订阅成功-真实] seq={seq}')
# 启动行情活跃监控线程(默认不活跃,收到行情后激活)
self._market_data_thread = threading.Thread(
target=self._market_data_watchdog, daemon=True
)
self._market_data_thread.start()
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [市场数据订阅失败-{e}]')
def _on_market_data(self, datas: dict):
"""xtquant 行情回调 — 收到行情即标记市场活跃(但需满足 09:15 后才激活)"""
self.lastMarketDataUpdateTimestamp = time.time()
if not self.isMarketActive:
# 检查当前时间是否已过 09:15(集合竞价结束后才激活市场状态)
import zoneinfo
beijing_tz = zoneinfo.ZoneInfo("Asia/Shanghai")
now = datetime.datetime.now(beijing_tz)
t = now.time()
activation_time = datetime.time(9, 15)
if t >= activation_time:
self.isMarketActive = True
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, True)
PrintLog(LogLevel.INFO, f'- [行情] 市场激活 (时间 {t.strftime("%H:%M:%S")} >= 09:15)')
eBus.event_bus.publish(eBus.MarketDataUpdate, datas)
def _is_trading_time(self) -> bool:
"""判断当前是否在交易时间内(工作日 09:30-11:30 / 13:00-15:00,北京时间 UTC+8"""
import zoneinfo
beijing_tz = zoneinfo.ZoneInfo("Asia/Shanghai")
now = datetime.datetime.now(beijing_tz)
if now.weekday() >= 5: # 周六、周日
return False
t = now.time()
morning_start = datetime.time(9, 30)
morning_end = datetime.time(11, 30)
afternoon_start = datetime.time(13, 0)
afternoon_end = datetime.time(15, 0)
return (morning_start <= t <= morning_end) or (afternoon_start <= t <= afternoon_end)
def _market_data_watchdog(self):
"""行情活跃监控 — 超过 120 秒无行情 则标记市场不活跃(无论是否交易时间)"""
while True:
time.sleep(15)
if self.isMarketActive:
elapsed = time.time() - self.lastMarketDataUpdateTimestamp
# 只有超过 120 秒无行情才标记不活跃,不再区分交易时间
if elapsed > 120:
self.isMarketActive = False
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, False)
PrintLog(LogLevel.INFO, f'- [行情] 超过 {elapsed:.0f} 秒无数据,市场标记为不活跃')
def stopMarketDataSubscription(self):
"""停止市场数据订阅"""
self.isMarketActive = False
PrintLog(LogLevel.INFO, '- [市场数据订阅已停止]')
# ---- xtquant 回调处理 (xtquant 通过回调对象调用 on_xxx 方法) ----
def on_connected(self):
PrintLog(LogLevel.INFO, f'[QMT] on_connected: 真实 QMT 连接成功 {datetime.datetime.now()}')
def on_disconnected(self):
PrintLog(LogLevel.WARNING, f'[QMT] on_disconnected: 真实 QMT 连接断开 {datetime.datetime.now()}')
def on_stock_order(self, order):
self._pending_orders.append(order)
def on_stock_trade(self, trade):
eBus.event_bus.publish(eBus.MarketOrderTraded, trade)
def on_order_stock_async_response(self, response):
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
def on_order_error(self, order_error):
PrintLog(LogLevel.ERROR,
f'[QMT] 委托报错: order_id={order_error.order_id}, error_id={order_error.error_id}, '
f'error_msg={order_error.error_msg}, remark={order_error.order_remark}')
eBus.event_bus.publish(eBus.MarketOrderError, order_error)
def on_account_status(self, status):
PrintLog(LogLevel.INFO, f'[QMT] on_account_status: {datetime.datetime.now()} {status}')
qmtv = RealQmtV()
+11
View File
@@ -0,0 +1,11 @@
# 软件介绍
软件名称:神之一手交易系统
软件介绍:面向个人的交易管理系统,提供交易记录、复盘工具、持仓管理、资产监控、策略交易等功能。
# 模块介绍
1. /core/daily_review: 每日复盘模块目录
2. /core/market_data: 市场数据模块目录
3. /core/quick_trade: 快速交易模块目录
4. /core/strategy/builder: 策略构建模块目录
5. /core/strategy/trade: 策略交易模块目录
6. /core: 应用核心程序目录
-1
View File
@@ -1 +0,0 @@
# grid_seeker v6.4 评分模块
-165
View File
@@ -1,165 +0,0 @@
"""
grid_seeker v6.4 CLI 入口
Usage:
python -m core.scoring.cli sync all # 按依赖顺序执行全部同步
python -m core.scoring.cli sync kline # 仅同步个股+指数K线
python -m core.scoring.cli sync stocks # 仅同步股票基础信息
python -m core.scoring.cli sync industry # 仅同步行业映射
python -m core.scoring.cli sync market # 仅计算市场状态
python -m core.scoring.cli sync sector # 仅计算行业指数
python -m core.scoring.cli score # 完整评分管道 (最新交易日)
python -m core.scoring.cli score --date 20260615 # 指定日期
python -m core.scoring.cli score --dry-run # 试运行 (不写库)
python -m core.scoring.cli check 000001 # 检查单只股票数据充分性
python -m core.scoring.cli list-candidates # 列出全部候选股
"""
import sys
from datetime import date, datetime
def main():
if len(sys.argv) < 2:
_usage()
return
cmd = sys.argv[1]
if cmd == 'sync':
_cmd_sync()
elif cmd == 'score':
_cmd_score()
elif cmd == 'check':
_cmd_check()
elif cmd == 'list-candidates':
_cmd_list_candidates()
else:
print(f'未知命令: {cmd}')
_usage()
def _usage():
print(__doc__)
# ============================================================
# sync 命令
# ============================================================
def _cmd_sync():
target = sys.argv[2] if len(sys.argv) > 2 else 'all'
from core.scoring.sync import (
KlineStockSync, KlineIndexSync,
StocksSync, IndustrySync,
MarketRegimeSync, SectorFeaturesSync,
)
syncs = {
'kline': [KlineStockSync, KlineIndexSync],
'stocks': [StocksSync],
'industry': [IndustrySync],
'market': [MarketRegimeSync],
'sector': [SectorFeaturesSync],
}
if target == 'all':
# 按依赖顺序执行
order = [
('K线(个股)', KlineStockSync(count=300)),
('K线(指数)', KlineIndexSync(count=300)),
('股票信息', StocksSync()),
('行业映射', IndustrySync()),
('市场状态', MarketRegimeSync()),
('行业指数', SectorFeaturesSync()),
]
for label, sync in order:
print(f'\n===== {label} =====')
sync.run()
print('\n===== 全部同步完成 =====')
elif target in syncs:
for cls in syncs[target]:
cls().run()
else:
print(f'未知同步目标: {target}')
print(f'可用: all, {", ".join(syncs.keys())}')
# ============================================================
# score 命令
# ============================================================
def _cmd_score():
dry_run = '--dry-run' in sys.argv
date_str = None
for i, arg in enumerate(sys.argv):
if arg == '--date' and i + 1 < len(sys.argv):
date_str = sys.argv[i + 1]
break
if date_str:
trade_date = datetime.strptime(date_str, '%Y%m%d').date()
else:
trade_date = date.today()
print(f'评分日期: {trade_date}')
from core.scoring.inference.scorer import GridSeekerPipeline
engine = GridSeekerPipeline()
rankings = engine.run(trade_date)
if rankings.empty:
print('无评分结果')
return
if not dry_run:
engine.persist(rankings, trade_date)
print(f'结果已写入 ScoringResult ({len(rankings)} 条)')
# 打印 Top-20
print('\n===== Top-20 =====')
print(f'{"Rank":<6} {"Code":<10} {"Profit":>10} {"Rounds":>10} {"Prob":>10}')
print('-' * 50)
for code, row in rankings.head(20).iterrows():
print(f'{int(row["score_rank"]):<6} {code:<10} '
f'{row["stacking_probability"]:>10.4f} '
f'{row.get("rank_predicted_rounds", 0):>10.2f} '
f'{row.get("stacking_probability", 0):>10.4f}')
# ============================================================
# check 命令
# ============================================================
def _cmd_check():
if len(sys.argv) < 3:
print('Usage: python -m core.scoring.cli check <stock_code>')
return
stock_code = sys.argv[2]
from core.scoring.features.validator import _check_kline_sufficiency
ok, reason, close = _check_kline_sufficiency(stock_code, date.today())
if ok:
print(f'{stock_code}: ✅ 通过 (close={close:.2f})')
else:
print(f'{stock_code}: ❌ {reason}')
# ============================================================
# list-candidates 命令
# ============================================================
def _cmd_list_candidates():
from core.scoring.features.validator import load_candidates
ctx = load_candidates(date.today())
print(f'候选股总数: {len(ctx.candidates)}')
print(f'排除: {len(ctx.excluded)}')
print(f'\n候选股 (前100):')
for i, code in enumerate(ctx.candidates[:100]):
print(f' {i + 1}. {code}')
if ctx.excluded:
print(f'\n排除原因 (前20):')
for i, (code, reason) in enumerate(list(ctx.excluded.items())[:20]):
print(f' {code}: {reason}')
if __name__ == '__main__':
main()
-54
View File
@@ -1,54 +0,0 @@
"""
grid_seeker v6.6 评分配置常量
"""
from pathlib import Path
import config as app_config
# ---- 网格交易参数 ----
GRID_LOW = 1 # 网格下限
GRID_HIGH = 11 # 网格上限
GRID_STEP = 1 # 网格间距(整数格)
# ---- 候选股过滤 ----
FILTER_MIN_CLOSE = 5 # 最低收盘价(覆盖网格策略持仓股)
FILTER_MAX_CLOSE = 13 # 最高收盘价
REQUIRE_DAYS = 120 # 最少交易日数
# ---- 窗口参数 ----
WINDOW_180D = 180 # 长窗口(情绪特征)
WINDOW_60D = 60 # 中窗口
WINDOW_20D = 20 # 短窗口(独立性特征)
ATR_PERIOD = 14 # ATR 周期
# ---- 指数 ----
HS300_CODE = '000300' # 沪深300基准指数
TRACKED_INDICES = [
'000001', # 上证指数
'000300', # 沪深300
'000852', # 中证1000
'000905', # 中证500
'399001', # 深证成指
'399006', # 创业板指
]
# ---- 市场状态 ----
PANIC_ADVANCE_RATIO = 0.20 # 恐慌日涨跌比阈值
GREED_ADVANCE_RATIO = 0.55 # 贪婪日涨跌比阈值
PANIC_TURNOVER_RATIO = 1.5 # 恐慌日量比阈值
# ---- 模型文件 ----
def model_dir() -> Path:
"""模型文件目录"""
return app_config.app_dir() / 'models'
def get_model_path(name: str) -> Path:
"""获取指定模型文件路径"""
return model_dir() / f'{name}.pkl'
# 3 级模型文件名 (v6.6)
RANK_MODEL = 'rank'
TOP_MODEL = 'top'
STACKING_MODEL = 'stacking'
# Stacking 选股阈值 (v6.7r3 最优阈值 0.331)
STACKING_THRESHOLD = 0.33
-3
View File
@@ -1,3 +0,0 @@
# 特征工程子包
from core.scoring.features.validator import load_candidates, DataContext
from core.scoring.features.pipeline import FeaturePipeline
-147
View File
@@ -1,147 +0,0 @@
"""
情绪弹性特征 (4维) — calculate_relaxed_emotion_features(df, market_regime)
180 日窗口,筛选 advance_ratio < 0.20 恐慌日 + advance_ratio > 0.55 贪婪日。
"""
import numpy as np
import pandas as pd
from core.scoring.config import PANIC_ADVANCE_RATIO, GREED_ADVANCE_RATIO, WINDOW_180D
def calculate_relaxed_emotion_features(ctx) -> pd.DataFrame:
"""计算情绪弹性特征 (4维)"""
kline = ctx.kline.copy()
mkt = ctx.market_regime.copy() if ctx.market_regime is not None else pd.DataFrame()
if kline.empty or mkt.empty:
return pd.DataFrame()
# 筛选恐慌日和贪婪日
panic_dates = set()
greed_dates = set()
for _, row in mkt.iterrows():
td = row['trade_date']
ar = row.get('advance_ratio', 0.5)
if ar < PANIC_ADVANCE_RATIO:
panic_dates.add(td.date() if hasattr(td, 'date') else td)
if ar > GREED_ADVANCE_RATIO:
greed_dates.add(td.date() if hasattr(td, 'date') else td)
kline = kline.sort_values(['stock_code', 'trade_date'])
kline['td'] = (kline['trade_date'].dt.date
if hasattr(kline['trade_date'], 'dt')
else pd.to_datetime(kline['trade_date']).dt.date)
candidates = ctx.candidates
# 计算全市场平均振幅 (用于恐慌/贪婪 ratio)
kline['amp'] = (kline['high'] - kline['low']) / np.where(
kline['open'] > 0, kline['open'], 1
)
market_amp = kline.groupby('td')['amp'].mean().to_dict()
features = {}
grouped = kline.groupby('stock_code')
for code, group in grouped:
if code not in candidates:
continue
if len(group) < 20:
continue
g = group.sort_values('trade_date').tail(WINDOW_180D)
closes = g['close'].values
opens = g['open'].values
highs = g['high'].values
lows = g['low'].values
dates = g['td'].values
amps = (highs - lows) / np.where(opens > 0, opens, 1)
feat = {}
# 恐慌日分析
panic_indices = [i for i, d in enumerate(dates) if d in panic_dates]
if panic_indices:
panic_amp_ratios = []
panic_drop_ratios = []
panic_rebounds = []
for pi in panic_indices:
td = dates[pi]
mkt_amp_val = market_amp.get(td, amps[pi])
if mkt_amp_val > 0:
panic_amp_ratios.append(amps[pi] / mkt_amp_val)
# 恐慌日跌幅
if pi > 0 and closes[pi - 1] > 0:
drop = (closes[pi] - closes[pi - 1]) / closes[pi - 1]
# 全市场跌幅: 用 advance_ratio 估算
ar = _get_advance_ratio(mkt, td)
market_drop = -0.02 if ar < PANIC_ADVANCE_RATIO else -0.005
if market_drop != 0:
panic_drop_ratios.append(drop / market_drop)
# 恐慌次日反弹
if pi + 1 < len(g) and closes[pi] > 0:
panic_rebounds.append(highs[pi + 1] / closes[pi] - 1)
# 64. relaxed_panic_amplitude_ratio
feat['relaxed_panic_amplitude_ratio'] = (
np.median(panic_amp_ratios) if panic_amp_ratios else 1.0
)
# 65. relaxed_panic_rebound_strength
feat['relaxed_panic_rebound_strength'] = (
np.median(panic_rebounds) if panic_rebounds else 0.0
)
else:
feat['relaxed_panic_amplitude_ratio'] = 1.0
feat['relaxed_panic_rebound_strength'] = 0.0
# 贪婪日分析
greed_indices = [i for i, d in enumerate(dates) if d in greed_dates]
if greed_indices:
greed_gains = []
greed_amp_ratios = []
for gi in greed_indices:
td = dates[gi]
if gi > 0 and closes[gi - 1] > 0:
gain = (closes[gi] - closes[gi - 1]) / closes[gi - 1]
# 全市场收益
ar = _get_advance_ratio(mkt, td)
market_gain = 0.01 if ar > GREED_ADVANCE_RATIO else 0.003
if market_gain > 0:
greed_gains.append(gain / market_gain)
mkt_amp_val = market_amp.get(td, amps[gi])
if mkt_amp_val > 0:
greed_amp_ratios.append(amps[gi] / mkt_amp_val)
# 66. relaxed_greed_relative_gain
feat['relaxed_greed_relative_gain'] = (
np.median(greed_gains) if greed_gains else 0.0
)
# 67. relaxed_greed_amplitude_ratio
feat['relaxed_greed_amplitude_ratio'] = (
np.median(greed_amp_ratios) if greed_amp_ratios else 1.0
)
else:
feat['relaxed_greed_relative_gain'] = 0.0
feat['relaxed_greed_amplitude_ratio'] = 1.0
features[code] = feat
return pd.DataFrame.from_dict(features, orient='index')
def _get_advance_ratio(mkt_df, trade_date):
"""获取指定日期的 advance_ratio"""
if mkt_df is None or mkt_df.empty:
return 0.5
td = trade_date
match = mkt_df[mkt_df['trade_date'] == td]
if not match.empty:
return match.iloc[0].get('advance_ratio', 0.5)
return 0.5
@@ -1,97 +0,0 @@
"""
大盘独立性特征 (3维) — calculate_independence_features(df)
使用 HS300(000300) 作为 benchmark,最近 20 个交易日 OLS 回归。
"""
import numpy as np
import pandas as pd
from core.scoring.config import WINDOW_20D
from core.scoring.features.v3_2_features import _ols_slope
def calculate_independence_features(ctx) -> pd.DataFrame:
"""计算大盘独立性特征 (3维)"""
kline = ctx.kline.copy()
hs300 = ctx.hs300_kline.copy() if ctx.hs300_kline is not None else pd.DataFrame()
if kline.empty or hs300.empty:
return pd.DataFrame()
# 准备 HS300 收益率序列
hs300 = hs300.sort_values('trade_date')
hs300['market_return'] = hs300['close'].pct_change()
hs300['market_amplitude'] = (hs300['high'] - hs300['low']) / hs300['open']
hs300 = hs300.dropna(subset=['market_return', 'market_amplitude'])
# 对齐日期
hs300_dates = set(hs300['trade_date'].dt.date
if hasattr(hs300['trade_date'], 'dt') else hs300['trade_date'])
kline = kline.sort_values(['stock_code', 'trade_date'])
kline['trade_date_dt'] = (kline['trade_date'].dt.date
if hasattr(kline['trade_date'], 'dt')
else pd.to_datetime(kline['trade_date']).dt.date)
candidates = ctx.candidates
# 计算 HS300 最近20日平均振幅
hs300_tail = hs300.tail(WINDOW_20D)
hs300_avg_amp = hs300_tail['market_amplitude'].mean() if len(hs300_tail) > 0 else 0
features = {}
grouped = kline.groupby('stock_code')
for code, group in grouped:
if code not in candidates:
continue
if len(group) < 20:
continue
g = group.sort_values('trade_date').tail(120)
closes = g['close'].values
# 计算个股日收益率
stock_rets = np.diff(closes) / np.where(closes[:-1] > 0, closes[:-1], 1)
# 对齐 HS300 收益率 (取对应日期)
# 简化: 取最近 N 个交易日的数据点
n = min(WINDOW_20D, len(stock_rets))
# 获取 HS300 最近 n 天的 market_return
market_rets = hs300['market_return'].tail(n + 1).values
if len(market_rets) < n:
market_rets = hs300['market_return'].values[-n - 1:]
# 对齐长度
min_len = min(n, len(market_rets) - 1, len(stock_rets))
if min_len < 5: # 至少需要5个数据点做回归
continue
stock_ret_window = stock_rets[-min_len:]
market_ret_window = market_rets[-min_len:]
feat = {}
# OLS 回归: stock_ret ~ market_return
try:
slope, r_value = _ols_slope(market_ret_window, stock_ret_window)
residuals = stock_ret_window - slope * market_ret_window
# 58. market_residual_volatility_20d: std(residuals) × √252
feat['market_residual_volatility_20d'] = np.std(residuals, ddof=1) * np.sqrt(252)
# 59. market_independence_ratio_20d: 1 - R²
feat['market_independence_ratio_20d'] = 1 - r_value ** 2
except Exception:
feat['market_residual_volatility_20d'] = 0
feat['market_independence_ratio_20d'] = 1
# 60. market_amplitude_deviation_20d: 个股平均振幅 - HS300 平均振幅
g_amps = (g['high'].values[-20:] - g['low'].values[-20:]) / np.where(
g['open'].values[-20:] > 0, g['open'].values[-20:], 1
)
feat['market_amplitude_deviation_20d'] = np.mean(g_amps) - hs300_avg_amp
features[code] = feat
return pd.DataFrame.from_dict(features, orient='index')
@@ -1,84 +0,0 @@
"""
负向指标 (5维) — calculate_negative_features(df)
注意: #53 trend_consistency_20d 与 #39 同名不同义,更新字典时会覆盖 #39
"""
import numpy as np
import pandas as pd
def calculate_negative_features(ctx) -> pd.DataFrame:
"""计算负向指标 (5维)"""
kline = ctx.kline.copy()
if kline.empty:
return pd.DataFrame()
kline = kline.sort_values(['stock_code', 'trade_date'])
candidates = ctx.candidates
features = {}
grouped = kline.groupby('stock_code')
for code, group in grouped:
if code not in candidates:
continue
g = group.tail(120)
if len(g) < 20:
continue
closes = g['close'].values
opens = g['open'].values
highs = g['high'].values
lows = g['low'].values
volumes = g['volume'].values
feat = {}
# 53. trend_consistency_20d (负向版本): |mean(return>0) - 0.5| × 100
# 衡量偏离均衡的程度,越接近50%越弱
rets_20 = np.diff(closes[-21:]) / np.where(closes[-21:-1] > 0, closes[-21:-1], 1)
feat['trend_consistency_20d'] = abs(np.mean(rets_20 > 0) - 0.5) * 100
# 54. max_consecutive_direction_20d: 最大连续同向天数
rets_sign = np.sign(np.diff(closes[-21:]))
max_consec = 0
curr_consec = 0
curr_sign = 0
for s in rets_sign:
if s != 0 and s == curr_sign:
curr_consec += 1
elif s != 0:
curr_sign = s
curr_consec = 1
else:
curr_consec = 0
max_consec = max(max_consec, curr_consec)
feat['max_consecutive_direction_20d'] = max_consec
# 55. gap_risk_20d: mean(|open_t - close_{t-1}|/close_{t-1} > 0.02) × 100
gap_count = 0
n = 0
for i in range(max(0, len(g) - 20), len(g)):
if i > 0 and closes[i - 1] > 0:
gap_pct = abs(opens[i] - closes[i - 1]) / closes[i - 1]
if gap_pct > 0.02:
gap_count += 1
n += 1
feat['gap_risk_20d'] = gap_count / n * 100 if n > 0 else 0
# 56. liquidity_drying_up_20d: min(vol_20d)/mean(vol_60d)
vol_20 = volumes[-20:]
vol_60 = volumes[-60:] if len(volumes) >= 60 else volumes
feat['liquidity_drying_up_20d'] = (
np.min(vol_20) / np.mean(vol_60) if np.mean(vol_60) > 0 else 1
)
# 57. price_stagnation_20d: (max(high_20d)-min(low_20d))/close × 100
h20 = np.max(highs[-20:])
l20 = np.min(lows[-20:])
feat['price_stagnation_20d'] = (
(h20 - l20) / closes[-1] * 100 if closes[-1] > 0 else 0
)
features[code] = feat
return pd.DataFrame.from_dict(features, orient='index')
-106
View File
@@ -1,106 +0,0 @@
"""
特征工程编排器 — 串联全部特征组,输出完整特征 DataFrame
"""
import pandas as pd
from datetime import date
from core.scoring.features.validator import load_candidates, DataContext
from core.scoring.features.v3_2_features import calculate_features_v3_2
from core.scoring.features.v3_3_features import calculate_features_v3_3
from core.scoring.features.v3_4_features import calculate_features_v3_4
from core.scoring.features.negative_features import calculate_negative_features
from core.scoring.features.independence_features import calculate_independence_features
from core.scoring.features.sector_features import calculate_sector_independence_features
from core.scoring.features.emotion_features import calculate_relaxed_emotion_features
from core.logger import LogLevel, PrintLog
class FeaturePipeline:
"""
特征工程管道 — 串联 8 组特征计算,输出完整特征矩阵。
Usage:
pipeline = FeaturePipeline(trade_date=date.today())
feature_df = pipeline.run() # DataFrame indexed by stock_code
"""
def __init__(self, trade_date: date):
self.trade_date = trade_date
self.ctx: DataContext = None
def run(self) -> pd.DataFrame:
"""
执行完整特征工程管道。
返回: DataFrame indexed by stock_code, columns = 全部特征
"""
# Stage 0: 加载候选股和数据
PrintLog(LogLevel.INFO, '[pipeline] Stage 0: 加载候选股...')
self.ctx = load_candidates(self.trade_date)
if not self.ctx.candidates:
PrintLog(LogLevel.WARNING, '[pipeline] 无候选股通过过滤')
return pd.DataFrame()
PrintLog(LogLevel.INFO, f'[pipeline] 候选股: {len(self.ctx.candidates)}, '
f'排除: {len(self.ctx.excluded)}')
# Stage 1: v3.2 基础特征 (20维)
PrintLog(LogLevel.INFO, '[pipeline] Stage 1: v3.2 基础特征 (20维)...')
df = calculate_features_v3_2(self.ctx)
PrintLog(LogLevel.INFO, f'[pipeline] → {len(df)} stocks, {len(df.columns)} features')
# Stage 2: v3.3 扩展特征 (16维)
PrintLog(LogLevel.INFO, '[pipeline] Stage 2: v3.3 扩展特征 (16维)...')
df_v33 = calculate_features_v3_3(self.ctx)
df = df.join(df_v33, how='inner', rsuffix='_v33')
PrintLog(LogLevel.INFO, f'[pipeline] → {len(df)} stocks, {len(df.columns)} features')
# Stage 3: v3.4 扩展特征 (16维)
PrintLog(LogLevel.INFO, '[pipeline] Stage 3: v3.4 扩展特征 (16维)...')
df_v34 = calculate_features_v3_4(self.ctx)
df = df.join(df_v34, how='inner', rsuffix='_v34')
PrintLog(LogLevel.INFO, f'[pipeline] → {len(df)} stocks, {len(df.columns)} features')
# Stage 4: 负向指标 (5维) — 注意 #53 覆盖 #39
PrintLog(LogLevel.INFO, '[pipeline] Stage 4: 负向指标 (5维)...')
df_neg = calculate_negative_features(self.ctx)
# 使用 update 模式: 负向指标的 trend_consistency_20d 覆盖 v3.4 版本
common_cols = set(df.columns) & set(df_neg.columns)
for col in common_cols:
df[col] = df_neg[col] # 覆盖
new_cols = set(df_neg.columns) - common_cols
for col in new_cols:
df[col] = df_neg[col]
PrintLog(LogLevel.INFO, f'[pipeline] → {len(df)} stocks, {len(df.columns)} features')
# Stage 5: 大盘独立性 (3维)
PrintLog(LogLevel.INFO, '[pipeline] Stage 5: 大盘独立性 (3维)...')
df_ind = calculate_independence_features(self.ctx)
df = df.join(df_ind, how='left')
df[df_ind.columns] = df[df_ind.columns].fillna(0)
PrintLog(LogLevel.INFO, f'[pipeline] → {len(df)} stocks, {len(df.columns)} features')
# Stage 6: 行业独立性 (3维)
PrintLog(LogLevel.INFO, '[pipeline] Stage 6: 行业独立性 (3维)...')
df_sec = calculate_sector_independence_features(self.ctx)
df = df.join(df_sec, how='left')
df[df_sec.columns] = df[df_sec.columns].fillna(0)
PrintLog(LogLevel.INFO, f'[pipeline] → {len(df)} stocks, {len(df.columns)} features')
# Stage 7: 情绪弹性 (4维)
PrintLog(LogLevel.INFO, '[pipeline] Stage 7: 情绪弹性 (4维)...')
df_emo = calculate_relaxed_emotion_features(self.ctx)
df = df.join(df_emo, how='left')
df[df_emo.columns] = df[df_emo.columns].fillna(1.0)
PrintLog(LogLevel.INFO, f'[pipeline] → {len(df)} stocks, {len(df.columns)} features')
# 添加 latest_close 列 (Meta Ranker 输入)
df['latest_close'] = 0.0
for code in df.index:
g = self.ctx.kline[self.ctx.kline['stock_code'] == code]
if not g.empty:
g_sorted = g.sort_values('trade_date')
df.at[code, 'latest_close'] = float(g_sorted['close'].iloc[-1])
PrintLog(LogLevel.INFO,
f'[pipeline] 完成: {len(df)} 只股票, {len(df.columns)} 维特征')
return df
-88
View File
@@ -1,88 +0,0 @@
"""
行业独立性特征 (3维) — calculate_sector_independence_features(df)
匹配 sector_features_daily,最近 20 个交易日 OLS 回归。
"""
import numpy as np
import pandas as pd
from core.scoring.config import WINDOW_20D
from core.scoring.features.v3_2_features import _ols_slope
def calculate_sector_independence_features(ctx) -> pd.DataFrame:
"""计算行业独立性特征 (3维)"""
kline = ctx.kline.copy()
sector_df = ctx.sector_features.copy() if ctx.sector_features is not None else pd.DataFrame()
industry_map = ctx.industry_map
if kline.empty or sector_df.empty or not industry_map:
return pd.DataFrame()
sector_df = sector_df.sort_values(['sector_name', 'trade_date'])
kline = kline.sort_values(['stock_code', 'trade_date'])
candidates = ctx.candidates
features = {}
grouped = kline.groupby('stock_code')
for code, group in grouped:
if code not in candidates:
continue
if len(group) < 20:
continue
# 获取行业名称
sector_name = industry_map.get(str(code), '')
if not sector_name:
continue
# 获取该行业的指数数据
sector_data = sector_df[sector_df['sector_name'] == sector_name]
if sector_data.empty or len(sector_data) < 5:
continue
sector_data = sector_data.sort_values('trade_date').tail(120)
sector_rets = sector_data['sector_ret'].values / 100 # 转为小数
sector_amps = sector_data['sector_amplitude'].values / 100
g = group.sort_values('trade_date').tail(120)
closes = g['close'].values
# 个股日收益率
stock_rets = np.diff(closes) / np.where(closes[:-1] > 0, closes[:-1], 1)
# 对齐长度
n = min(WINDOW_20D, len(stock_rets), len(sector_rets) - 1)
if n < 5:
continue
stock_ret_window = stock_rets[-n:]
sector_ret_window = sector_rets[-n:]
feat = {}
# OLS: stock_ret ~ sector_ret
try:
slope, r_value = _ols_slope(sector_ret_window, stock_ret_window)
residuals = stock_ret_window - slope * sector_ret_window
# 61. sector_residual_volatility_20d
feat['sector_residual_volatility_20d'] = np.std(residuals, ddof=1) * np.sqrt(252)
# 62. sector_independence_ratio_20d: 1 - R²
feat['sector_independence_ratio_20d'] = 1 - r_value ** 2
except Exception:
feat['sector_residual_volatility_20d'] = 0
feat['sector_independence_ratio_20d'] = 1
# 63. sector_amplitude_deviation_20d: 个股振幅 - 行业平均振幅
g_amps = (g['high'].values[-20:] - g['low'].values[-20:]) / np.where(
g['open'].values[-20:] > 0, g['open'].values[-20:], 1
)
sector_avg_amp = np.mean(sector_amps[-20:]) if len(sector_amps) >= 20 else np.mean(sector_amps)
feat['sector_amplitude_deviation_20d'] = np.mean(g_amps) - sector_avg_amp
features[code] = feat
return pd.DataFrame.from_dict(features, orient='index')
-203
View File
@@ -1,203 +0,0 @@
"""
基础特征 v3.2 (20维) — calculate_features(df, "v3.2")
"""
import numpy as np
import pandas as pd
import numpy as np
import pandas as pd
from core.scoring.config import GRID_LOW, GRID_HIGH
def calculate_features_v3_2(ctx) -> pd.DataFrame:
"""
计算 v3.2 基础特征 (20维)。
返回 DataFrame indexed by stock_code。
"""
kline = ctx.kline.copy()
if kline.empty:
return pd.DataFrame()
kline = kline.sort_values(['stock_code', 'trade_date'])
candidates = ctx.candidates
features = {}
grouped = kline.groupby('stock_code')
for code, group in grouped:
if code not in candidates:
continue
g = group.tail(120) # 取最近120日用于大部分窗口
if len(g) < 20:
continue
closes = g['close'].values
opens = g['open'].values
highs = g['high'].values
lows = g['low'].values
volumes = g['volume'].values
feat = {}
# 1. rolling_grid_ratio_20d: mean(close∈[1,11]) × 100
recent_20_close = closes[-20:]
feat['rolling_grid_ratio_20d'] = (
np.mean((recent_20_close >= GRID_LOW) & (recent_20_close <= GRID_HIGH)) * 100
)
# 2. cross_freq_20d: 日高低区间穿越≥1条整数网格线的天数比例
cross_count = 0
for i in range(max(0, len(g) - 20), len(g)):
h, l = highs[i], lows[i]
if h > l:
grid_low = int(np.ceil(l))
grid_high = int(np.floor(h))
if grid_high >= grid_low:
cross_count += 1
feat['cross_freq_20d'] = cross_count / min(20, len(g)) * 100
# 3. avg_daily_amp: mean((high-low)/open × 100)
amps = (highs - lows) / np.where(opens > 0, opens, 1) * 100
feat['avg_daily_amp'] = np.mean(amps[-20:])
# 4. high_amp_days: mean(振幅>2%) × 100
feat['high_amp_days'] = np.mean(amps[-20:] > 2) * 100
# 5. atr_pct: mean(ATR_14)/close × 100
atr = _compute_atr(highs, lows, closes, 14)
feat['atr_pct'] = np.mean(atr[-14:]) / closes[-1] * 100 if closes[-1] > 0 else 0
# 6. volatility_20d: std(log_return) × √252 × 100
log_rets = np.diff(np.log(np.maximum(closes, 1e-10)))
vol_20 = np.std(log_rets[-20:], ddof=1) if len(log_rets) >= 20 else 0
feat['volatility_20d'] = vol_20 * np.sqrt(252) * 100
# 7. price_cv: std(close)/mean(close) × 100
feat['price_cv'] = np.std(closes) / np.mean(closes) * 100 if np.mean(closes) > 0 else 0
# 8. bb_width: (MA20+2σ - (MA20-2σ))/MA20 × 100
ma20 = np.mean(closes[-20:])
std20 = np.std(closes[-20:], ddof=1)
feat['bb_width'] = (4 * std20) / ma20 * 100 if ma20 > 0 else 0
# 9. volume_ratio: mean(vol_20d)/mean(vol_60d)
vol_20m = np.mean(volumes[-20:])
vol_60m = np.mean(volumes[-60:]) if len(volumes) >= 60 else vol_20m
feat['volume_ratio'] = vol_20m / vol_60m if vol_60m > 0 else 1
# 10. obv_slope: OBV序列最近20日线性回归斜率
obv = _compute_obv(closes, volumes)
feat['obv_slope'] = _ols_slope(np.arange(20), obv[-20:])[0] if len(obv) >= 20 else 0
# 11. amplitude_cv: std(振幅)/mean(振幅)
feat['amplitude_cv'] = (np.std(amps[-20:]) / np.mean(amps[-20:])
if np.mean(amps[-20:]) > 0 else 0)
# 12. price_entropy: Shannon熵 (10 bins)
feat['price_entropy'] = _shannon_entropy(closes[-60:], bins=10)
# 13. intraday_trend_strength: mean(|close-open|/open) × 100
intraday = np.abs(closes[-20:] - opens[-20:]) / np.where(opens[-20:] > 0, opens[-20:], 1) * 100
feat['intraday_trend_strength'] = np.mean(intraday)
# 14-17. 交叉特征 (依赖前序特征)
feat['amp_x_grid'] = feat['avg_daily_amp'] * feat['rolling_grid_ratio_20d'] / 100
feat['amp_x_grid_vol'] = (feat['avg_daily_amp'] * feat['rolling_grid_ratio_20d'] / 100 *
feat['volume_ratio'] / 10000)
feat['amp_cv_x_entropy'] = feat['amplitude_cv'] * feat['price_entropy']
feat['cross_freq_x_bb'] = feat['cross_freq_20d'] * feat['bb_width'] / 100
# 18. ln_float_mv: ln(close × total_share + 1)
total_share = _get_total_share(ctx, code)
float_mv = closes[-1] * total_share if total_share else closes[-1] * 1e8
feat['ln_float_mv'] = np.log(float_mv + 1)
# 19. mv_vol_interact: ln_float_mv × volatility_20d/100
feat['mv_vol_interact'] = feat['ln_float_mv'] * feat['volatility_20d'] / 100
# 20. small_cap_premium: 1/(浮动市值 + 1)
feat['small_cap_premium'] = 1.0 / (float_mv + 1)
features[code] = feat
return pd.DataFrame.from_dict(features, orient='index')
# ---- 辅助函数 ----
def _compute_atr(highs, lows, closes, period=14):
"""计算 ATR"""
n = len(closes)
tr = np.zeros(n)
for i in range(1, n):
h_l = highs[i] - lows[i]
h_c = abs(highs[i] - closes[i - 1])
l_c = abs(lows[i] - closes[i - 1])
tr[i] = max(h_l, h_c, l_c)
atr = np.zeros(n)
atr[:period] = np.mean(tr[:period])
for i in range(period, n):
atr[i] = (atr[i - 1] * (period - 1) + tr[i]) / period
return atr
def _compute_obv(closes, volumes):
"""计算 OBV"""
obv = np.zeros(len(closes))
obv[0] = volumes[0]
for i in range(1, len(closes)):
if closes[i] > closes[i - 1]:
obv[i] = obv[i - 1] + volumes[i]
elif closes[i] < closes[i - 1]:
obv[i] = obv[i - 1] - volumes[i]
else:
obv[i] = obv[i - 1]
return obv
def _shannon_entropy(values, bins=10):
"""Shannon 熵"""
if len(values) < bins:
return 0.0
hist, _ = np.histogram(values, bins=bins)
hist = hist / hist.sum()
hist = hist[hist > 0]
return -np.sum(hist * np.log2(hist))
def _get_total_share(ctx, code):
"""从 StockInfo 获取总股本"""
# 尝试各种前缀
for prefix in ['', 'SH', 'SZ', 'BJ']:
key = f'{code}.{prefix}' if prefix else code
info = ctx.stock_info.get(key, {})
ts = info.get('total_share', None)
if ts and ts > 0:
return ts
return None
def _ols_slope(x, y):
"""
纯 numpy OLS 线性回归。
等价于 scipy.stats.linregress(x, y),返回 (slope, r_value)。
slope=0 且 r_value=0 表示计算失败(数据不足或方差为零)。
"""
if len(x) < 2 or len(y) < 2 or len(x) != len(y):
return 0.0, 0.0
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
x_mean = x.mean()
y_mean = y.mean()
num = np.sum((x - x_mean) * (y - y_mean))
den = np.sum((x - x_mean) ** 2)
if den < 1e-12:
return 0.0, 0.0
slope = num / den
ss_xy = num
ss_xx = np.sum((x - x_mean) ** 2)
ss_yy = np.sum((y - y_mean) ** 2)
if ss_xx < 1e-12 or ss_yy < 1e-12:
r_value = 0.0
else:
r_value = ss_xy / (np.sqrt(ss_xx) * np.sqrt(ss_yy))
return slope, r_value
-130
View File
@@ -1,130 +0,0 @@
"""
扩展特征 v3.3 (16维)
"""
import numpy as np
import pandas as pd
from core.scoring.features.v3_2_features import _ols_slope
def calculate_features_v3_3(ctx) -> pd.DataFrame:
"""计算 v3.3 扩展特征 (16维)"""
kline = ctx.kline.copy()
if kline.empty:
return pd.DataFrame()
kline = kline.sort_values(['stock_code', 'trade_date'])
candidates = ctx.candidates
features = {}
grouped = kline.groupby('stock_code')
for code, group in grouped:
if code not in candidates:
continue
g = group.tail(120)
if len(g) < 20:
continue
closes = g['close'].values
highs = g['high'].values
lows = g['low'].values
volumes = g['volume'].values
feat = {}
# 21. grid_touch_count_20d: 高低区间触碰网格线总次数
feat['grid_touch_count_20d'] = _grid_touch_count(highs[-20:], lows[-20:])
# 22. grid_touch_count_60d
h60 = highs[-60:] if len(highs) >= 60 else highs
l60 = lows[-60:] if len(lows) >= 60 else lows
feat['grid_touch_count_60d'] = _grid_touch_count(h60, l60)
# 23. grid_cross_density_20d
cross_count = 0
w = min(20, len(g))
for i in range(len(g) - w, len(g)):
h, l = highs[i], lows[i]
if h > l and int(np.floor(h)) >= int(np.ceil(l)):
cross_count += 1
feat['grid_cross_density_20d'] = cross_count / w
# 24. near_grid_line_ratio_20d
recent_closes = closes[-20:]
dist_to_int = np.abs(recent_closes - np.round(recent_closes))
feat['near_grid_line_ratio_20d'] = np.mean(dist_to_int <= 0.15) * 100
# 25. trend_slope_20d
feat['trend_slope_20d'] = _ols_slope(np.arange(20), closes[-20:])[0]
# 26. trend_slope_60d
c60 = closes[-60:] if len(closes) >= 60 else closes
feat['trend_slope_60d'] = _ols_slope(np.arange(len(c60)), c60)[0]
# 27. trend_abs_slope_20d
feat['trend_abs_slope_20d'] = abs(feat['trend_slope_20d'])
# 28. range_position_60d
h60_mx = np.max(highs[-60:]) if len(highs) >= 60 else np.max(highs)
l60_mn = np.min(lows[-60:]) if len(lows) >= 60 else np.min(lows)
feat['range_position_60d'] = (closes[-1] - l60_mn) / (h60_mx - l60_mn) * 100 \
if h60_mx > l60_mn else 50
# 29. drawdown_60d
c60_arr = closes[-60:] if len(closes) >= 60 else closes
cummax = np.maximum.accumulate(c60_arr)
dd = (1 - c60_arr / cummax) * 100
feat['drawdown_60d'] = np.max(dd)
# 30. rebound_from_low_60d
feat['rebound_from_low_60d'] = (closes[-1] - l60_mn) / l60_mn * 100 if l60_mn > 0 else 0
# 31. dist_to_grid_upper
from core.scoring.config import GRID_HIGH
feat['dist_to_grid_upper'] = max(0, (GRID_HIGH - closes[-1]) / 10 * 100)
# 32. dist_to_grid_lower
from core.scoring.config import GRID_LOW
feat['dist_to_grid_lower'] = max(0, (closes[-1] - GRID_LOW) / 10 * 100)
# 33. grid_room_balance
upper = feat['dist_to_grid_upper']
lower = feat['dist_to_grid_lower']
feat['grid_room_balance'] = min(upper, lower) / (upper + lower) * 100 \
if (upper + lower) > 0 else 50
# 34. amount_mean_20d
amounts = closes[-20:] * volumes[-20:]
feat['amount_mean_20d'] = np.mean(amounts)
# 35. amount_cv_20d
feat['amount_cv_20d'] = np.std(amounts) / np.mean(amounts) * 100 \
if np.mean(amounts) > 0 else 0
# 36. turnover_proxy_20d
total_share = _get_total_share(ctx, code) or 1e8
feat['turnover_proxy_20d'] = np.mean(volumes[-20:]) / (total_share * 1e8) * 100
features[code] = feat
return pd.DataFrame.from_dict(features, orient='index')
def _get_total_share(ctx, code):
for prefix in ['', 'SH', 'SZ', 'BJ']:
key = f'{code}.{prefix}' if prefix else code
info = ctx.stock_info.get(key, {})
ts = info.get('total_share', None)
if ts and ts > 0:
return ts
return None
def _grid_touch_count(highs, lows):
count = 0
for h, l in zip(highs, lows):
if h > l:
grid_low = int(np.ceil(l))
grid_high = int(np.floor(h))
count += max(0, grid_high - grid_low + 1)
return count
-133
View File
@@ -1,133 +0,0 @@
"""
扩展特征 v3.4 + v6.7新增 (20维: 16维 v3.4 + 4维 v6.7新增)
"""
import numpy as np
import pandas as pd
from core.scoring.features.v3_2_features import _ols_slope
from core.scoring.features.v3_3_features import _grid_touch_count
from core.scoring.config import GRID_LOW, GRID_HIGH
def calculate_features_v3_4(ctx) -> pd.DataFrame:
"""计算 v3.4 扩展特征 (16维)"""
kline = ctx.kline.copy()
if kline.empty:
return pd.DataFrame()
kline = kline.sort_values(['stock_code', 'trade_date'])
candidates = ctx.candidates
features = {}
grouped = kline.groupby('stock_code')
for code, group in grouped:
if code not in candidates:
continue
g = group.tail(120)
if len(g) < 20:
continue
closes = g['close'].values
opens = g['open'].values
highs = g['high'].values
lows = g['low'].values
volumes = g['volume'].values
feat = {}
# 37. down_days_20d
rets_20 = np.diff(closes[-21:]) / closes[-21:-1]
feat['down_days_20d'] = np.mean(rets_20 < 0) * 100
# 38. up_days_20d
feat['up_days_20d'] = np.mean(rets_20 > 0) * 100
# 39. trend_consistency_20d
feat['trend_consistency_20d'] = max(feat['up_days_20d'], feat['down_days_20d'])
# 40. ma20_deviation_pct
ma20 = np.mean(closes[-20:])
feat['ma20_deviation_pct'] = (closes[-1] - ma20) / ma20 * 100 if ma20 > 0 else 0
# 41. ma60_deviation_pct
ma60 = np.mean(closes[-60:]) if len(closes) >= 60 else np.mean(closes)
feat['ma60_deviation_pct'] = (closes[-1] - ma60) / ma60 * 100 if ma60 > 0 else 0
# 42. ma20_ma60_gap_pct
feat['ma20_ma60_gap_pct'] = (ma20 - ma60) / ma60 * 100 if ma60 > 0 else 0
# 43. usable_grid_count_upper
feat['usable_grid_count_upper'] = sum(
1 for g in range(GRID_LOW, GRID_HIGH + 1) if g > closes[-1])
# 44. usable_grid_count_lower
feat['usable_grid_count_lower'] = sum(
1 for g in range(GRID_LOW, GRID_HIGH + 1) if g < closes[-1])
# 45. near_upper_boundary_risk
feat['near_upper_boundary_risk'] = max(0, min(100, (closes[-1] - 9) / 2 * 100))
# 46. near_lower_boundary_risk
feat['near_lower_boundary_risk'] = max(0, min(100, (3 - closes[-1]) / 2 * 100))
# 47. volume_cv_20d
vol_20 = volumes[-20:]
feat['volume_cv_20d'] = np.std(vol_20) / np.mean(vol_20) * 100 \
if np.mean(vol_20) > 0 else 0
# 48. amount_trend_20d
amounts = closes[-20:] * volumes[-20:]
feat['amount_trend_20d'] = _ols_slope(np.arange(len(amounts)), amounts)[0]
# 49. low_volume_days_20d
mean_vol = np.mean(vol_20)
feat['low_volume_days_20d'] = np.mean(vol_20 < 0.5 * mean_vol) * 100
# 50. close_reversal_count_20d
rets_sign = np.sign(np.diff(closes[-21:]))
reversals = sum(
1 for i in range(1, len(rets_sign))
if rets_sign[i] != 0 and rets_sign[i - 1] != 0 and rets_sign[i] != rets_sign[i - 1])
feat['close_reversal_count_20d'] = reversals
# 51. range_compression_20d
amps = (highs - lows) / np.where(closes > 0, closes, 1) * 100
amp_20_mean = np.mean(amps[-20:])
amp_60_mean = np.mean(amps[-60:]) if len(amps) >= 60 else amp_20_mean
feat['range_compression_20d'] = amp_20_mean / amp_60_mean * 100 if amp_60_mean > 0 else 100
# 52. wick_ratio_20d
upper_wick = highs[-20:] - np.maximum(opens[-20:], closes[-20:])
lower_wick = np.minimum(opens[-20:], closes[-20:]) - lows[-20:]
body = np.abs(closes[-20:] - opens[-20:])
total_len = highs[-20:] - lows[-20:]
wick_len = upper_wick + lower_wick
feat['wick_ratio_20d'] = np.mean(
wick_len / np.where(total_len > 0, total_len, 1)) * 100
# ── v6.7 新增 4 维特征 ──────────────────────────────
# 53. vol_decay_5d: 近5日波动率 / 近20日波动率 (波动率用对数收益std)
log_ret = np.diff(np.log(np.maximum(closes, 1e-10)))
vol_5d = np.std(log_ret[-5:], ddof=1) if len(log_ret) >= 5 else 0
vol_20d = np.std(log_ret[-20:], ddof=1) if len(log_ret) >= 20 else vol_5d
feat['vol_decay_5d'] = float(vol_5d / vol_20d) if vol_20d > 0 else 0.0
# 54. grid_touch_relative_10d: 10日振幅比 / 60日振幅比
range_10d = float(np.max(highs[-10:]) - np.min(lows[-10:]))
range_60d = float(np.max(highs[-60:]) - np.min(lows[-60:])) if len(highs) >= 60 else range_10d
close_now = float(closes[-1])
close_60d_mean = float(np.mean(closes[-60:])) if len(closes) >= 60 else close_now
if close_now > 0 and close_60d_mean > 0 and range_60d > 0:
feat['grid_touch_relative_10d'] = (range_10d / close_now) / (range_60d / close_60d_mean)
else:
feat['grid_touch_relative_10d'] = 0.0
# 55. vol_decay_x_grid_balance: vol_decay × 网格均衡度
feat['vol_decay_x_grid_balance'] = feat['vol_decay_5d'] * feat.get('grid_room_balance', 0.0)
# 56. vol_decay_x_dist_lower: vol_decay × 下轨距离
feat['vol_decay_x_dist_lower'] = feat['vol_decay_5d'] * feat.get('dist_to_grid_lower', 0.0)
features[code] = feat
return pd.DataFrame.from_dict(features, orient='index')
-209
View File
@@ -1,209 +0,0 @@
"""
候选股过滤与数据加载
"""
import pandas as pd
from datetime import date, timedelta
from core.scoring.models import (
KlineStock, StockInfo, IndustryMapping,
KlineIndex, MarketRegimeDaily, SectorFeaturesDaily,
)
from core.scoring.config import (
FILTER_MIN_CLOSE, FILTER_MAX_CLOSE, REQUIRE_DAYS,
WINDOW_180D, HS300_CODE,
)
from core.logger import LogLevel, PrintLog
class DataContext:
"""特征计算所需的全部数据上下文"""
def __init__(self, trade_date: date):
self.trade_date = trade_date
self.require_days = REQUIRE_DAYS
# 原始数据
self.kline: pd.DataFrame = None # 候选股 K线 (180d)
self.stock_info: dict = {} # code → StockInfo dict
self.industry_map: dict = {} # code → industry_name
self.hs300_kline: pd.DataFrame = None # HS300 K线 (180d)
self.market_regime: pd.DataFrame = None # 市场状态 (180d)
self.sector_features: pd.DataFrame = None # 行业指数 (180d)
# 候选股列表
self.candidates: list[str] = []
self.excluded: dict[str, str] = {} # code → reason
def _get_stock_codes_for_date(trade_date: date) -> list:
"""获取评分日所有符合条件的股票代码(非ST/退市)"""
rows = (StockInfo
.select(StockInfo.code, StockInfo.listing_status)
.where(StockInfo.listing_status.not_in(['delisted', 'ST']))
.dicts())
return [row['code'] for row in rows]
def _check_kline_sufficiency(stock_code: str, trade_date: date) -> tuple[bool, str, float]:
"""检查单只股票的K线数据是否满足评分条件"""
# 查询最近 REQUIRE_DAYS + 30 (留缓冲) 个交易日
start = trade_date - timedelta(days=(REQUIRE_DAYS + 60) * 2)
rows = (KlineStock
.select(KlineStock.trade_date, KlineStock.close)
.where(
(KlineStock.stock_code == stock_code) &
(KlineStock.trade_date >= start) &
(KlineStock.trade_date <= trade_date)
)
.order_by(KlineStock.trade_date.desc())
.dicts())
if not rows:
return False, '无K线数据', 0
# 过滤有效收盘价 (close > 0)
valid_rows = [r for r in rows if r['close'] and r['close'] > 0]
if len(valid_rows) < REQUIRE_DAYS:
return False, f'交易天数不足({len(valid_rows)}<{REQUIRE_DAYS})', 0
latest_close = valid_rows[0]['close']
# 价格区间检查
if latest_close < FILTER_MIN_CLOSE:
return False, f'价格过低({latest_close:.2f}<{FILTER_MIN_CLOSE})', latest_close
if latest_close > FILTER_MAX_CLOSE:
return False, f'价格过高({latest_close:.2f}>{FILTER_MAX_CLOSE})', latest_close
return True, '', latest_close
def load_candidates(trade_date: date) -> DataContext:
"""
加载评分日候选股及全部所需数据。
返回 DataContext,包含候选股列表和预加载的原始数据。
"""
ctx = DataContext(trade_date)
start_180 = trade_date - timedelta(days=365) # 取约1年数据覆盖180个交易日
# 1. 获取非ST/退市的全部股票
all_codes = [r.split('.')[0] for r in _get_stock_codes_for_date(trade_date)]
PrintLog(LogLevel.INFO, f'[validator] 全市场有效股票: {len(all_codes)}')
# 2. 批量加载 KlineStock (180d 窗口)
raw_codes = all_codes # 使用纯数字代码查询
kline_rows = (KlineStock
.select()
.where(
(KlineStock.stock_code.in_(raw_codes)) &
(KlineStock.trade_date >= start_180) &
(KlineStock.trade_date <= trade_date)
)
.order_by(KlineStock.stock_code, KlineStock.trade_date)
.dicts())
kline_df = pd.DataFrame(kline_rows)
if kline_df.empty:
PrintLog(LogLevel.WARNING, '[validator] KlineStock 无数据')
return ctx
kline_df['trade_date'] = pd.to_datetime(kline_df['trade_date'])
PrintLog(LogLevel.INFO, f'[validator] K线原始数据: {len(kline_df)}')
# 3. 逐股过滤
candidates = []
excluded = {}
grouped = kline_df.groupby('stock_code')
for code, group in grouped:
g_sorted = group.sort_values('trade_date', ascending=False)
valid_rows = g_sorted[g_sorted['close'].notna() & (g_sorted['close'] > 0)]
if len(valid_rows) < REQUIRE_DAYS:
excluded[code] = f'交易天数不足({len(valid_rows)}<{REQUIRE_DAYS})'
continue
latest_close = valid_rows.iloc[0]['close']
if latest_close < FILTER_MIN_CLOSE:
excluded[code] = f'价格过低({latest_close:.2f}<{FILTER_MIN_CLOSE})'
continue
if latest_close > FILTER_MAX_CLOSE:
excluded[code] = f'价格过高({latest_close:.2f}>{FILTER_MAX_CLOSE})'
continue
candidates.append(code)
PrintLog(LogLevel.INFO,
f'[validator] 候选: {len(candidates)} 通过, {len(excluded)} 排除')
# 3.5 补充:强制加入网格持仓股(不受价格过滤限制)
from core.sfgrid.model import SFGridTradeTarget
pos_rows = list(SFGridTradeTarget
.select(SFGridTradeTarget.stock_code)
.where(SFGridTradeTarget.enabled == True)
.dicts())
pos_codes = [r['stock_code'].split('.')[0] for r in pos_rows]
forced = [c for c in pos_codes if c not in candidates and c not in excluded]
if forced:
PrintLog(LogLevel.INFO, f'[validator] 强制加入网格持仓股: {forced}')
candidates.extend(forced)
# 4. 裁剪K线到只含候选股 (保留最近180日)
kline_df = kline_df[kline_df['stock_code'].isin(candidates)].copy()
cut_date = trade_date - timedelta(days=365)
kline_df = kline_df[kline_df['trade_date'] >= pd.Timestamp(cut_date)]
ctx.kline = kline_df
ctx.candidates = candidates
ctx.excluded = excluded
# 5. 加载 StockInfo
stock_rows = (StockInfo
.select()
.where(StockInfo.code.in_([f'{c}.SH' for c in candidates] +
[f'{c}.SZ' for c in candidates] +
[f'{c}.BJ' for c in candidates]))
.dicts())
ctx.stock_info = {r['code']: r for r in stock_rows}
# 6. 加载行业映射: code → industry_name
ind_rows = (IndustryMapping
.select()
.where(IndustryMapping.code.in_(candidates))
.dicts())
ctx.industry_map = {r['code']: r['industry_name'] for r in ind_rows}
PrintLog(LogLevel.INFO, f'[validator] 行业映射: {len(ctx.industry_map)}')
# 7. 加载 HS300 K线
hs300_rows = (KlineIndex
.select()
.where(
(KlineIndex.index_code == HS300_CODE) &
(KlineIndex.trade_date >= start_180) &
(KlineIndex.trade_date <= trade_date)
)
.order_by(KlineIndex.trade_date)
.dicts())
ctx.hs300_kline = pd.DataFrame(hs300_rows)
if not ctx.hs300_kline.empty:
ctx.hs300_kline['trade_date'] = pd.to_datetime(ctx.hs300_kline['trade_date'])
# 8. 加载市场状态 (180d)
mkt_rows = (MarketRegimeDaily
.select()
.where(
(MarketRegimeDaily.trade_date >= start_180) &
(MarketRegimeDaily.trade_date <= trade_date)
)
.order_by(MarketRegimeDaily.trade_date)
.dicts())
ctx.market_regime = pd.DataFrame(mkt_rows)
if not ctx.market_regime.empty:
ctx.market_regime['trade_date'] = pd.to_datetime(ctx.market_regime['trade_date'])
# 9. 加载行业指数 (180d)
sec_rows = (SectorFeaturesDaily
.select()
.where(
(SectorFeaturesDaily.trade_date >= start_180) &
(SectorFeaturesDaily.trade_date <= trade_date)
)
.order_by(SectorFeaturesDaily.trade_date)
.dicts())
ctx.sector_features = pd.DataFrame(sec_rows)
if not ctx.sector_features.empty:
ctx.sector_features['trade_date'] = pd.to_datetime(ctx.sector_features['trade_date'])
return ctx
-2
View File
@@ -1,2 +0,0 @@
# 模型推理子包
from core.scoring.inference.scorer import GridSeekerPipeline
-226
View File
@@ -1,226 +0,0 @@
"""
grid_seeker v6.7r3 三级模型推理管道
Rank → Top → Stacking → stacking_probability (最终排序)
"""
import pickle
import numpy as np
import pandas as pd
from pathlib import Path
from datetime import date
from core.scoring.config import (
get_model_path, RANK_MODEL, TOP_MODEL, STACKING_MODEL,
STACKING_THRESHOLD,
)
from core.scoring.features.pipeline import FeaturePipeline
from core.scoring.models import ScoringResult
from core.database import db
from core.logger import LogLevel, PrintLog
# ============================================================
# Rank 模型输入特征 (56维 v6.7/v3.4, 同时支持 feat_names 和 selected_features)
# ============================================================
def _get_rank_features() -> list:
import pickle
from core.scoring.config import get_model_path
path = get_model_path(RANK_MODEL)
with open(path, 'rb') as f:
obj = pickle.load(f)
if isinstance(obj, dict):
# v6.7r3 使用 feat_names, v6.6 使用 selected_features
sf = obj.get('feat_names', []) or obj.get('selected_features', [])
if sf:
return sf
raise RuntimeError("无法从 rank.pkl 读取 feat_names 或 selected_features")
RANK_FEATURE_COLS = _get_rank_features()
# Top/Stacking 模型只用 52 维基础特征(不含 v6.7 新增的4维)
# v6.7 新增: vol_decay_5d, grid_touch_relative_10d, vol_decay_x_grid_balance, vol_decay_x_dist_lower
_V67_NEW_FEATS = {
'vol_decay_5d', 'grid_touch_relative_10d',
'vol_decay_x_grid_balance', 'vol_decay_x_dist_lower'
}
BASE_52_COLS = [f for f in RANK_FEATURE_COLS if f not in _V67_NEW_FEATS]
class GridSeekerPipeline:
"""
grid_seeker v6.7r3 三级模型评分管道。
Usage:
engine = GridSeekerPipeline()
rankings = engine.run(trade_date=date.today())
# 返回 DataFrame: stock_code, stacking_probability, rank 等
"""
def __init__(self, model_dir: Path = None):
self._rank_model = None
self._top_model = None
self._stacking_model = None
# ---- 模型加载 ----
def _load_model(self, name: str):
"""加载单个 .pkl 模型"""
path = get_model_path(name)
if not path.exists():
raise FileNotFoundError(f'模型文件不存在: {path}')
with open(path, 'rb') as f:
obj = pickle.load(f)
# 支持 dict 格式 {"model": lgbm_model, ...} 或直接返回模型对象
if isinstance(obj, dict):
return obj.get('model', obj)
return obj
@property
def rank_model(self):
if self._rank_model is None:
self._rank_model = self._load_model(RANK_MODEL)
return self._rank_model
@property
def top_model(self):
if self._top_model is None:
self._top_model = self._load_model(TOP_MODEL)
return self._top_model
@property
def stacking_model(self):
if self._stacking_model is None:
self._stacking_model = self._load_model(STACKING_MODEL)
return self._stacking_model
# ---- 预测 ----
def _predict_with_model(self, model, X: pd.DataFrame, feature_cols: list) -> np.ndarray:
"""
使用模型预测。自动选择特征子集,兼容 sklearn API (predict/predict_proba)。
"""
available = [c for c in feature_cols if c in X.columns]
missing = set(feature_cols) - set(available)
if missing:
PrintLog(LogLevel.WARNING,
f'[scorer] 缺少特征列 ({len(missing)}): {list(missing)[:5]}...')
X_sub = X[available].fillna(0).values
try:
if hasattr(model, 'predict_proba'):
proba = model.predict_proba(X_sub)
if proba.shape[1] >= 2:
return proba[:, 1]
return proba[:, 0]
elif hasattr(model, 'predict'):
return model.predict(X_sub)
else:
return model.predict(X_sub)
except Exception as e:
PrintLog(LogLevel.ERROR, f'[scorer] 模型预测失败: {e}')
raise
# ---- 主流程 ----
def run(self, trade_date: date) -> pd.DataFrame:
"""
执行完整的 3 级评分管道。
Returns:
DataFrame indexed by stock_code, 含 stacking_probability / rank 等列,
按 stacking_probability 降序排列
"""
PrintLog(LogLevel.INFO, f'[scorer] ===== grid_seeker v6.7r3 评分开始 ({trade_date}) =====')
# 1. 特征工程
pipeline = FeaturePipeline(trade_date)
feature_df = pipeline.run()
if feature_df.empty:
PrintLog(LogLevel.WARNING, '[scorer] 无股票通过特征工程, 终止')
return pd.DataFrame()
PrintLog(LogLevel.INFO,
f'[scorer] 特征工程完成: {len(feature_df)} stocks, '
f'{len(feature_df.columns)} dims')
# 2. Stage 1: Rank 模型 → rank_predicted_rounds (52维)
PrintLog(LogLevel.INFO, '[scorer] Stage 1/3: Rank 模型...')
feature_df['rank_predicted_rounds'] = self._predict_with_model(
self.rank_model, feature_df, RANK_FEATURE_COLS
)
# 3. Stage 2: Top 模型 → top_elite_prob (53维 = 52基础 + rank)
PrintLog(LogLevel.INFO, '[scorer] Stage 2/3: Top 模型...')
top_cols = BASE_52_COLS + ['rank_predicted_rounds']
feature_df['top_elite_prob'] = self._predict_with_model(
self.top_model, feature_df, top_cols
)
# 4. Stage 3: Stacking 模型 → stacking_probability (55维 = 52基础 + rank + top)
PrintLog(LogLevel.INFO, '[scorer] Stage 3/3: Stacking 模型...')
stk_cols = BASE_52_COLS + ['rank_predicted_rounds', 'top_elite_prob']
feature_df['stacking_probability'] = self._predict_with_model(
self.stacking_model, feature_df, stk_cols
)
# 5. 排序(直接用 stacking_probability
feature_df['score_rank'] = feature_df['stacking_probability'].rank(
ascending=False, method='min'
).astype(int)
feature_df['candidate_count'] = len(feature_df)
feature_df = feature_df.sort_values('score_rank')
n_above = (feature_df['stacking_probability'] >= STACKING_THRESHOLD).sum()
PrintLog(LogLevel.INFO,
f'[scorer] 评分完成: {len(feature_df)} 只候选, '
f'{n_above} 只高于阈值 {STACKING_THRESHOLD}')
PrintLog(LogLevel.INFO,
f'[scorer] Top-5: '
f'{feature_df.head(5)[["stacking_probability", "rank_predicted_rounds"]].to_dict("index")}')
return feature_df
def persist(self, rankings: pd.DataFrame, trade_date: date):
"""将评分结果持久化到 ScoringResult 表"""
if rankings.empty:
return
records = []
for code, row in rankings.iterrows():
records.append({
'stock_code': str(code),
'trade_date': trade_date,
'predicted_profit': float(row.get('stacking_probability', 0)),
'rank_predicted_rounds': float(row.get('rank_predicted_rounds', 0))
if 'rank_predicted_rounds' in row else None,
'top_elite_prob': float(row.get('top_elite_prob', 0))
if 'top_elite_prob' in row else None,
'stacking_probability': float(row.get('stacking_probability', 0))
if 'stacking_probability' in row else None,
'score_rank': int(row.get('score_rank', 0)),
'candidate_count': int(row.get('candidate_count', 0)),
})
with db.atomic():
for batch in _chunked(records, 500):
ScoringResult.insert_many(batch).on_conflict_replace().execute()
PrintLog(LogLevel.INFO,
f'[scorer] 评分结果已持久化: {len(records)}')
def get_top_n(self, trade_date: date, n: int = 50) -> list[dict]:
"""查询历史评分 Top-N"""
rows = (ScoringResult
.select()
.where(
(ScoringResult.trade_date == trade_date) &
(ScoringResult.score_rank <= n)
)
.order_by(ScoringResult.score_rank)
.dicts())
return list(rows)
def _chunked(lst: list, n: int):
for i in range(0, len(lst), n):
yield lst[i:i + n]
-146
View File
@@ -1,146 +0,0 @@
"""
grid_seeker v6.4 数据库模型 — 6 张数据表 + 1 张评分结果表
所有模型继承 core.database.BaseModel,复用现有 SQLite 连接。
"""
from peewee import (
CharField, DateField, FloatField, IntegerField,
CompositeKey, TextField,
)
from core.database import BaseModel, db
# ============================================================
# 1. kline_stock — 个股日K线
# ============================================================
class KlineStock(BaseModel):
stock_code = CharField(max_length=10) # 纯数字6位
trade_date = DateField()
open = FloatField()
high = FloatField()
low = FloatField()
close = FloatField()
volume = FloatField() # 成交量(股)
class Meta:
primary_key = CompositeKey('stock_code', 'trade_date')
indexes = (
(('stock_code', 'trade_date'), False),
)
# ============================================================
# 2. stocks — 股票基础信息
# ============================================================
class StockInfo(BaseModel):
code = CharField(max_length=12, primary_key=True) # 带 SH/SZ/BJ 前缀
name = CharField(max_length=32)
exchange = CharField(max_length=4) # SH / SZ / BJ
list_date = DateField(null=True)
listing_status = CharField(max_length=16, default='normal') # normal / delisted / ST
total_share = FloatField(null=True) # 总股本(股)
float_share = FloatField(null=True) # 流通股本(股)
share_updated_at = DateField(null=True) # 股本数据同步时间
# ============================================================
# 3. industry — 股票-行业映射
# ============================================================
class IndustryMapping(BaseModel):
code = CharField(max_length=6, primary_key=True) # 纯数字6位
industry_name = CharField(max_length=64, index=True)
industry_classification = CharField(max_length=32) # 行业分类体系名称
update_date = DateField()
# ============================================================
# 4. kline_index — 指数日K线
# ============================================================
class KlineIndex(BaseModel):
index_code = CharField(max_length=10) # 指数代码(6位数字)
trade_date = DateField()
open = FloatField()
high = FloatField()
low = FloatField()
close = FloatField()
volume = FloatField()
class Meta:
primary_key = CompositeKey('index_code', 'trade_date')
indexes = (
(('index_code', 'trade_date'), False),
)
# ============================================================
# 5. market_regime_daily — 市场状态(本地计算)
# ============================================================
class MarketRegimeDaily(BaseModel):
trade_date = DateField(primary_key=True)
advancers = FloatField(default=0) # 当日上涨家数
decliners = FloatField(default=0) # 当日下跌家数
advance_ratio = FloatField(default=0) # 涨跌比 = advancers/(advancers+decliners)
turnover = FloatField(default=0) # 全市场成交额
turnover_avg_5d = FloatField(default=0) # 5日滚动均量
turnover_ratio_5d = FloatField(default=0) # 量比 = turnover/turnover_avg_5d
source = CharField(max_length=32, default='qmt')
is_extreme_panic = IntegerField(default=0) # advance_ratio<0.2 且 turnover_ratio_5d>1.5
# ============================================================
# 6. sector_features_daily — 行业聚合指数(本地计算)
# ============================================================
class SectorFeaturesDaily(BaseModel):
trade_date = DateField()
sector_name = CharField(max_length=64) # 与 industry.industry_name 对应
sector_ret = FloatField(default=0) # 行业日收益率(均值)
sector_amplitude = FloatField(default=0) # 行业平均振幅
close = FloatField(default=100) # 行业指数(基值100
ema10 = FloatField(default=0)
ema20 = FloatField(default=0)
ema200 = FloatField(default=0)
score = IntegerField(default=0) # 趋势评分 0/1/2
class Meta:
primary_key = CompositeKey('trade_date', 'sector_name')
# ============================================================
# 7. ScoringResult — 评分结果(模型输出写入表)
# ============================================================
class ScoringResult(BaseModel):
stock_code = CharField(max_length=6) # 纯数字6位
trade_date = DateField() # 评分日
predicted_profit = FloatField(default=0) # 最终预测利润(=stacking_probability
rank_predicted_rounds = FloatField(null=True) # Rank 模型输出
top_elite_prob = FloatField(null=True) # Top 模型输出
stacking_probability = FloatField(null=True) # Stacking 模型输出
score_rank = IntegerField(default=0) # 排名
candidate_count = IntegerField(default=0) # 候选股总数
class Meta:
primary_key = CompositeKey('stock_code', 'trade_date')
# ============================================================
# 8. PendingPoolAction — 待执行股票池操作(阶段一标记,阶段二执行)
# ============================================================
class PendingPoolAction(BaseModel):
"""pending_pool_actions 表 — T日标记的操作,待 T+1 执行"""
action_date = DateField() # T日日期(标记日期)
action_type = CharField(max_length=20) # 'eliminate' | 'liquidate'
stock_code = CharField(max_length=6) # 股票代码
reason = TextField(null=True) # 淘汰原因描述
class Meta:
primary_key = CompositeKey('action_date', 'action_type', 'stock_code')
# ============================================================
# 建表
# ============================================================
ALL_SCORING_TABLES = [
KlineStock, StockInfo, IndustryMapping, KlineIndex,
MarketRegimeDaily, SectorFeaturesDaily, ScoringResult, PendingPoolAction,
]
db.create_tables(ALL_SCORING_TABLES)
-7
View File
@@ -1,7 +0,0 @@
# 数据同步子包
from core.scoring.sync.base import BaseSync
from core.scoring.sync.kline_sync import KlineStockSync, KlineIndexSync
from core.scoring.sync.stocks_sync import StocksSync
from core.scoring.sync.industry_sync import IndustrySync
from core.scoring.sync.market_regime import MarketRegimeSync
from core.scoring.sync.sector_features import SectorFeaturesSync
-41
View File
@@ -1,41 +0,0 @@
"""
数据同步抽象基类
"""
import abc
from datetime import datetime
from core.logger import LogLevel, PrintLog
class BaseSync(abc.ABC):
"""数据同步抽象基类,所有同步操作遵循 _fetch → _upsert 模式"""
def __init__(self):
self.stats = {'inserted': 0, 'updated': 0, 'skipped': 0, 'errors': 0}
def run(self, **kwargs) -> dict:
"""同步入口: 拉取数据 → 写入数据库 → 返回统计"""
name = self.__class__.__name__
PrintLog(LogLevel.INFO, f'[sync] {name} 开始同步...')
t0 = datetime.now()
try:
data = self._fetch(**kwargs)
self._upsert(data)
elapsed = (datetime.now() - t0).total_seconds()
PrintLog(
LogLevel.INFO,
f'[sync] {name} 完成 ({elapsed:.1f}s) — '
f'insert={self.stats["inserted"]} update={self.stats["updated"]} '
f'skip={self.stats["skipped"]} err={self.stats["errors"]}'
)
except Exception as e:
PrintLog(LogLevel.ERROR, f'[sync] {name} 失败: {e}')
raise
return self.stats
@abc.abstractmethod
def _fetch(self, **kwargs):
"""从数据源拉取原始数据。子类实现。"""
@abc.abstractmethod
def _upsert(self, data):
"""将数据写入数据库。子类实现。"""
-84
View File
@@ -1,84 +0,0 @@
"""
行业映射同步 — 从 QMT get_sector_list + get_stock_list_in_sector 获取
"""
from datetime import date
from core.scoring.sync.base import BaseSync
from core.scoring.models import IndustryMapping
from core.database import db
from core.logger import LogLevel, PrintLog
class IndustrySync(BaseSync):
"""行业映射同步 — QMT 行业板块 → IndustryMapping 表(全量替换)"""
def _fetch(self, **kwargs):
"""从 QMT 拉取全部行业板块的成份股映射"""
from xtquant import xtdata
all_sectors = xtdata.get_sector_list()
PrintLog(LogLevel.INFO, f'[sync] Industry: 共 {len(all_sectors)} 个板块')
# 尝试使用 get_sector_info 过滤行业板块
industry_sectors = []
try:
sector_info = xtdata.get_sector_info()
if sector_info is not None and not sector_info.empty:
for _, row in sector_info.iterrows():
cat = row.get('category', '')
if '行业' in str(cat):
industry_sectors.append(row['sector'])
except Exception:
pass
# 如果 get_sector_info 无效,回退到名称过滤
if not industry_sectors:
for s in all_sectors:
# 排除明显非行业的板块
skip_markers = ['概念', '风格', '地域', '地区', '指数', '自定义',
'ETF', 'LOF', '债券', '基金', '期货', '期权']
if any(m in s for m in skip_markers):
continue
industry_sectors.append(s)
PrintLog(LogLevel.INFO, f'[sync] Industry: 筛选出 {len(industry_sectors)} 个行业板块')
# 构建 code → {industry_name, classification} 映射
mapping = {} # code → (industry_name, classification)
today = date.today()
for sector_name in industry_sectors:
try:
stocks = xtdata.get_stock_list_in_sector(sector_name)
for full_code in stocks:
code = full_code.split('.')[0]
if code not in mapping:
mapping[code] = {
'code': code,
'industry_name': sector_name,
'industry_classification': 'QMT',
'update_date': today,
}
except Exception:
self.stats['errors'] += 1
self.stats['inserted'] = len(mapping)
return list(mapping.values())
def _upsert(self, data: list):
"""全量替换: 清空旧数据 → 批量插入新数据"""
if not data:
PrintLog(LogLevel.WARNING, '[sync] Industry: 无数据, 跳过')
return
with db.atomic():
IndustryMapping.delete().execute()
for batch in _chunked(data, 500):
IndustryMapping.insert_many(batch).execute()
PrintLog(LogLevel.INFO,
f'[sync] Industry: 全量替换完成, {len(data)} 条映射')
def _chunked(lst: list, n: int):
for i in range(0, len(lst), n):
yield lst[i:i + n]
-250
View File
@@ -1,250 +0,0 @@
"""
K线数据同步 — 个股日K + 指数日K
数据源: QMT xtdata
增量同步: 只拉 max(trade_date) 之后的增量数据
线程锁: KlineStockSync / KlineIndexSync 各自内部锁
"""
import pandas as pd
from datetime import date, datetime, timedelta
from core.scoring.sync.base import BaseSync
from core.scoring.models import KlineStock, KlineIndex
from core.scoring.config import TRACKED_INDICES
from core.database import db
from core.logger import LogLevel, PrintLog
BATCH_SIZE = 50
DEFAULT_COUNT = 300
# 全局同步状态标记(字典引用传递,可被外部轮询)
_sync_state = {"kline": False, "index": False, "stocks": False,
"industry": False, "market": False, "sector": False}
def is_syncing(key="kline") -> bool:
return _sync_state.get(key, False)
def _latest_date(model_cls) -> date | None:
from peewee import fn
row = model_cls.select(fn.MAX(model_cls.trade_date)).scalar()
if isinstance(row, date):
return row
return None
def _safe_get(df_dict, code, dt, default=0.0) -> float:
"""安全获取 DataFrame 值"""
if df_dict is None:
return default
df = df_dict.get(code)
if df is None or code not in df.index:
return default
try:
val = df.loc[code, dt]
if pd.isna(val):
return default
return float(val)
except Exception:
return default
class KlineStockSync(BaseSync):
"""个股日K线同步 — 增量:只拉 max(trade_date) 之后的增量数据"""
def __init__(self, count: int = DEFAULT_COUNT):
super().__init__()
self.count = count
def _fetch(self, **kwargs):
from xtquant import xtdata
# 增量判断: 以数据库最新一条记录为准
latest = _latest_date(KlineStock)
today = date.today()
# 增量起点: last_db_date + 1; 截止: 昨天(盘中不能同步当天数据)
# 注意: 不能用 latest >= today 跳过,因为 latest 可能是错误的未收盘数据
start_date = (latest + timedelta(days=1)) if latest else None
end_date = today - timedelta(days=1) # 固定截止到昨天,收盘后同步昨天数据
start_str = start_date.strftime('%Y%m%d') if start_date else ""
end_str = end_date.strftime('%Y%m%d')
PrintLog(LogLevel.INFO,
f'[sync] KlineStock: 增量同步 {start_str} ~ {end_str}')
all_stocks = xtdata.get_stock_list_in_sector("沪深A股")
PrintLog(LogLevel.INFO, f'[sync] KlineStock: {len(all_stocks)} 只A股')
field_list = ['open', 'high', 'low', 'close', 'volume']
total = len(all_stocks)
inserted = 0
for i, code in enumerate(all_stocks):
if i > 0 and i % 50 == 0:
PrintLog(LogLevel.INFO, f'[sync] KlineStock: {i}/{total} ({i*100//total}%)')
try:
xtdata.download_history_data(code, period='1d', start_time=start_str, end_time=end_str)
except Exception:
self.stats['errors'] += 1
continue
try:
result = xtdata.get_market_data(
field_list=field_list, stock_list=[code], period='1d',
start_time=start_str, end_time=end_str,
dividend_type='none', fill_data=False)
inserted += self._upsert_incremental(code, result, start_date, end_date)
except Exception:
self.stats['errors'] += 1
self.stats['inserted'] = inserted
PrintLog(LogLevel.INFO,
f'[sync] KlineStock 完成: 新增={inserted} '
f'跳过={self.stats["skipped"]} 错误={self.stats["errors"]}')
return self.stats
def _upsert_incremental(self, full_code: str, result: dict, start_date, end_date) -> int:
if not result:
return 0
close_df = result.get('close')
if close_df is None or close_df.empty:
return 0
stock_code = full_code.split('.')[0]
records = []
vol_df = result.get('volume')
for td in close_df.columns:
# xtdata 返回的列名可能是字符串 'YYYYMMDD' 或 datetime,需统一转成 date
if isinstance(td, str):
td_date = datetime.strptime(td, '%Y%m%d').date()
else:
td_date = td.date() if hasattr(td, 'date') else td
# 过滤: 不在增量范围内的跳过 (start_date < td <= end_date)
if start_date is not None and td_date < start_date:
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue
if end_date is not None and td_date > end_date:
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue
# 跳过成交量为0的无效数据(盘中未结算数据)
vol = vol_df.loc[full_code, td] if vol_df is not None else None
if vol is None or (isinstance(vol, float) and pd.isna(vol)) or vol == 0:
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue
close_val = close_df.loc[full_code, td]
if close_val is None or (isinstance(close_val, float) and pd.isna(close_val)):
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue
records.append({
'stock_code': stock_code,
'trade_date': td_date,
'open': _safe_get(result.get('open'), full_code, td),
'high': _safe_get(result.get('high'), full_code, td),
'low': _safe_get(result.get('low'), full_code, td),
'close': float(close_val),
'volume': float(vol),
})
if records:
with db.atomic():
for batch in _chunked(records, 500):
KlineStock.insert_many(batch).on_conflict_replace().execute()
return len(records)
return 0
def _upsert(self, data):
pass
class KlineIndexSync(BaseSync):
"""指数日K线同步 — 增量同步"""
def __init__(self, indices: list = None, count: int = DEFAULT_COUNT):
super().__init__()
self.indices = indices or TRACKED_INDICES
self.count = count
def _fetch(self, **kwargs):
from xtquant import xtdata
index_codes = []
for code in self.indices:
if code.startswith(('000', '001')):
index_codes.append(f'{code}.SH')
elif code.startswith('399'):
index_codes.append(f'{code}.SZ')
else:
index_codes.append(f'{code}.SH')
latest = _latest_date(KlineIndex)
today = date.today()
start_date = (latest + timedelta(days=1)) if latest else None
end_date = today - timedelta(days=1) # 截止到昨天
start_str = start_date.strftime('%Y%m%d') if start_date else ""
end_str = end_date.strftime('%Y%m%d')
PrintLog(LogLevel.INFO,
f'[sync] KlineIndex: 增量同步 {start_str} ~ {end_str}')
for code in index_codes:
try:
xtdata.download_history_data(code, period='1d', start_time=start_str, end_time=end_str)
except Exception:
self.stats['errors'] += 1
field_list = ['open', 'high', 'low', 'close', 'volume']
result = xtdata.get_market_data(
field_list=field_list, stock_list=index_codes, period='1d',
start_time=start_str, end_time=end_str,
dividend_type='none', fill_data=False)
def _upsert(self, data):
if not data:
return
latest = _latest_date(KlineIndex)
today = date.today()
start_date = (latest + timedelta(days=1)) if latest else None
end_date = today - timedelta(days=1)
records = []
close_df = data.get('close')
vol_df = data.get('volume')
if close_df is None or close_df.empty:
return
for full_code in close_df.index:
index_code = full_code.split('.')[0]
for td in close_df.columns:
if isinstance(td, str):
td_date = datetime.strptime(td, '%Y%m%d').date()
else:
td_date = td.date() if hasattr(td, 'date') else td
# 增量范围过滤 (start_date < td <= end_date)
if start_date is not None and td_date < start_date:
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue
if end_date is not None and td_date > end_date:
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue
# 过滤成交量为0的无效数据
vol = vol_df.loc[full_code, td] if vol_df is not None else None
if vol is None or (isinstance(vol, float) and pd.isna(vol)) or vol == 0:
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue
close_val = close_df.loc[full_code, td]
if close_val is None or (isinstance(close_val, float) and pd.isna(close_val)):
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue
records.append({
'index_code': index_code,
'trade_date': td_date,
'open': _safe_get(data.get('open'), full_code, td),
'high': _safe_get(data.get('high'), full_code, td),
'low': _safe_get(data.get('low'), full_code, td),
'close': float(close_val),
'volume': float(vol),
})
if records:
with db.atomic():
for batch in _chunked(records, 500):
KlineIndex.insert_many(batch).on_conflict_replace().execute()
self.stats['inserted'] = self.stats.get('inserted', 0) + len(records)
def _chunked(lst: list, n: int):
for i in range(0, len(lst), n):
yield lst[i:i + n]
-98
View File
@@ -1,98 +0,0 @@
"""
市场状态计算 — 从 kline_stock 聚合生成 market_regime_daily
纯本地计算,不依赖外部数据源。
"""
import pandas as pd
from peewee import fn, Case
from core.scoring.sync.base import BaseSync
from core.scoring.models import KlineStock, MarketRegimeDaily
from core.scoring.config import PANIC_ADVANCE_RATIO, PANIC_TURNOVER_RATIO
from core.database import db
from core.logger import LogLevel, PrintLog
class MarketRegimeSync(BaseSync):
"""市场状态同步 — kline_stock 聚合 → market_regime_daily"""
def _fetch(self, **kwargs):
"""从 KlineStock 逐日聚合涨跌家数和成交额"""
PrintLog(LogLevel.INFO, '[sync] MarketRegime: 开始聚合全市场数据...')
# peewee 聚合查询: 逐日统计 advancers / decliners / turnover
query = (KlineStock
.select(
KlineStock.trade_date,
fn.SUM(
Case(None, [(KlineStock.close > KlineStock.open, 1)], 0)
).alias('advancers'),
fn.SUM(
Case(None, [(KlineStock.close < KlineStock.open, 1)], 0)
).alias('decliners'),
fn.SUM(KlineStock.close * KlineStock.volume).alias('turnover'),
)
.group_by(KlineStock.trade_date)
.order_by(KlineStock.trade_date))
rows = list(query.dicts())
if not rows:
PrintLog(LogLevel.WARNING, '[sync] MarketRegime: KlineStock 表为空')
return None
df = pd.DataFrame(rows)
df['trade_date'] = pd.to_datetime(df['trade_date'])
df = df.sort_values('trade_date').reset_index(drop=True)
# 计算涨跌比
total = df['advancers'] + df['decliners']
df['advance_ratio'] = (df['advancers'] / total.replace(0, 1)).round(4)
# 5日滚动均量
df['turnover_avg_5d'] = (df['turnover']
.rolling(window=5, min_periods=1)
.mean()
.round(2))
# 量比
df['turnover_ratio_5d'] = (df['turnover'] /
df['turnover_avg_5d'].replace(0, 1)).round(4)
# 极端恐慌标记
df['is_extreme_panic'] = (
(df['advance_ratio'] < PANIC_ADVANCE_RATIO) &
(df['turnover_ratio_5d'] > PANIC_TURNOVER_RATIO)
).astype(int)
PrintLog(LogLevel.INFO,
f'[sync] MarketRegime: 聚合完成, {len(df)} 个交易日')
return df
def _upsert(self, df):
"""写入 MarketRegimeDaily 表"""
if df is None or df.empty:
return
records = []
for _, row in df.iterrows():
records.append({
'trade_date': row['trade_date'].date(),
'advancers': int(row['advancers']),
'decliners': int(row['decliners']),
'advance_ratio': float(row['advance_ratio']),
'turnover': float(row['turnover']),
'turnover_avg_5d': float(row['turnover_avg_5d']),
'turnover_ratio_5d': float(row['turnover_ratio_5d']),
'source': 'qmt',
'is_extreme_panic': int(row['is_extreme_panic']),
})
if records:
with db.atomic():
for batch in _chunked(records, 500):
MarketRegimeDaily.insert_many(batch).on_conflict_replace().execute()
self.stats['inserted'] += len(records)
def _chunked(lst: list, n: int):
for i in range(0, len(lst), n):
yield lst[i:i + n]
-161
View File
@@ -1,161 +0,0 @@
"""
行业聚合指数计算 — 从 kline_stock + industry 生成 sector_features_daily
纯本地计算,不依赖外部数据源。
"""
import pandas as pd
from core.scoring.sync.base import BaseSync
from core.scoring.models import KlineStock, IndustryMapping, SectorFeaturesDaily
from core.database import db
from core.logger import LogLevel, PrintLog
class SectorFeaturesSync(BaseSync):
"""行业聚合指数同步 — kline_stock + industry → sector_features_daily"""
def _fetch(self, **kwargs):
"""从数据库加载原始数据, 计算行业指数特征(分块处理避免内存溢出)"""
PrintLog(LogLevel.INFO, '[sync] SectorFeatures: 加载原始数据...')
# 1. 加载行业映射: code → industry_name
industries = list(IndustryMapping.select().dicts())
if not industries:
PrintLog(LogLevel.WARNING, '[sync] SectorFeatures: IndustryMapping 表为空, 请先执行 industry sync')
return None
code_to_industry = {row['code']: row['industry_name'] for row in industries}
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: {len(code_to_industry)} 条行业映射')
# 2. 分块加载 K 线数据,避免内存溢出
# 聚合结果: {(trade_date, sector_name): [sum_pct_chg, sum_amp, count]}
sector_daily_agg = {} # key: (date, sector) -> {'ret_sum': float, 'amp_sum': float, 'count': int}
CHUNK_SIZE = 50000
last_date_per_stock = {} # stock_code -> prev_close
PrintLog(LogLevel.INFO, '[sync] SectorFeatures: 分块处理K线数据...')
chunk_num = 0
while True:
chunk_num += 1
rows = list(KlineStock
.select(
KlineStock.stock_code,
KlineStock.trade_date,
KlineStock.open,
KlineStock.high,
KlineStock.low,
KlineStock.close,
)
.order_by(KlineStock.stock_code, KlineStock.trade_date)
.offset((chunk_num - 1) * CHUNK_SIZE)
.limit(CHUNK_SIZE)
.dicts())
if not rows:
break
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: 处理块 {chunk_num} ({len(rows)} 行)...')
for row in rows:
code = str(row['stock_code'])
td = row['trade_date']
open_p = float(row['open'])
high = float(row['high'])
low = float(row['low'])
close = float(row['close'])
sector = code_to_industry.get(code)
if sector is None:
continue
# 计算日收益率和振幅
prev_close = last_date_per_stock.get(code)
if prev_close is not None and prev_close > 0 and open_p > 0 and close > 0:
pct_chg = (close - prev_close) / prev_close * 100
amp = (high - low) / open_p * 100
key = (td, sector)
if key not in sector_daily_agg:
sector_daily_agg[key] = {'ret_sum': 0.0, 'amp_sum': 0.0, 'count': 0}
sector_daily_agg[key]['ret_sum'] += pct_chg
sector_daily_agg[key]['amp_sum'] += amp
sector_daily_agg[key]['count'] += 1
last_date_per_stock[code] = close
if not sector_daily_agg:
PrintLog(LogLevel.WARNING, '[sync] SectorFeatures: 无有效K线数据')
return None
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: 聚合完成, {len(sector_daily_agg)} 个行业-日组合')
# 3. 构建聚合 DataFrame
agg_data = []
for (td, sector), vals in sector_daily_agg.items():
agg_data.append({
'trade_date': td,
'sector_name': sector,
'sector_ret': vals['ret_sum'] / vals['count'],
'sector_amplitude': vals['amp_sum'] / vals['count'],
})
agg = pd.DataFrame(agg_data)
agg = agg.sort_values(['sector_name', 'trade_date'])
agg['trade_date'] = pd.to_datetime(agg['trade_date'])
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: {len(agg)} 行, {agg["sector_name"].nunique()} 个行业')
# 4. 构建行业指数 (基值=100)
agg = agg.sort_values(['sector_name', 'trade_date'])
agg['sector_index'] = agg.groupby('sector_name')['sector_ret'].transform(
lambda x: (1 + x / 100).cumprod() * 100
)
# 重新基值=100 (每行业独立)
for name, group in agg.groupby('sector_name'):
idx = group.index
first_val = group['sector_index'].iloc[0]
agg.loc[idx, 'sector_index'] = group['sector_index'] / first_val * 100
# 5. 计算 EMA 均线
agg['ema10'] = (agg.groupby('sector_name')['sector_index']
.transform(lambda x: x.ewm(span=10, min_periods=1).mean()))
agg['ema20'] = (agg.groupby('sector_name')['sector_index']
.transform(lambda x: x.ewm(span=20, min_periods=1).mean()))
agg['ema200'] = (agg.groupby('sector_name')['sector_index']
.transform(lambda x: x.ewm(span=200, min_periods=1).mean()))
# 6. 趋势评分: close>ema200 得1分 + ema10>ema20 得1分
agg['score'] = (
(agg['sector_index'] > agg['ema200']).astype(int) +
(agg['ema10'] > agg['ema20']).astype(int)
)
PrintLog(LogLevel.INFO,
f'[sync] SectorFeatures: 计算完成, {len(agg)} 行, '
f'{agg["sector_name"].nunique()} 个行业')
return agg
def _upsert(self, agg):
"""写入 SectorFeaturesDaily 表"""
if agg is None or agg.empty:
return
records = []
for _, row in agg.iterrows():
records.append({
'trade_date': row['trade_date'].date()
if hasattr(row['trade_date'], 'date') else row['trade_date'],
'sector_name': str(row['sector_name']),
'sector_ret': round(float(row['sector_ret']), 4),
'sector_amplitude': round(float(row['sector_amplitude']), 4),
'close': round(float(row['sector_index']), 4),
'ema10': round(float(row['ema10']), 4),
'ema20': round(float(row['ema20']), 4),
'ema200': round(float(row['ema200']), 4),
'score': int(row['score']),
})
if records:
with db.atomic():
for batch in _chunked(records, 500):
SectorFeaturesDaily.insert_many(batch).on_conflict_replace().execute()
self.stats['inserted'] += len(records)
def _chunked(lst: list, n: int):
for i in range(0, len(lst), n):
yield lst[i:i + n]
-100
View File
@@ -1,100 +0,0 @@
"""
股票基础信息同步 — 从 QMT get_instrument_detail 获取
"""
from datetime import date
from core.scoring.sync.base import BaseSync
from core.scoring.models import StockInfo
from core.database import db
from core.logger import LogLevel, PrintLog
BATCH_SIZE = 200
class StocksSync(BaseSync):
"""股票基础信息同步 — QMT get_instrument_detail_list"""
def _fetch(self, **kwargs):
"""从 QMT 拉取全部A股基础信息"""
from xtquant import xtdata
all_stocks = xtdata.get_stock_list_in_sector("沪深A股")
PrintLog(LogLevel.INFO, f'[sync] Stocks: 获取到 {len(all_stocks)} 只A股')
result = {}
total = len(all_stocks)
for i in range(0, total, BATCH_SIZE):
batch = all_stocks[i:i + BATCH_SIZE]
try:
details = xtdata.get_instrument_detail_list(batch)
result.update(details)
except Exception as e:
PrintLog(LogLevel.ERROR,
f'[sync] Stocks batch {i}-{min(i + BATCH_SIZE, total)} failed: {e}')
self.stats['errors'] += len(batch)
return result
def _upsert(self, data: dict):
"""写入 StockInfo 表"""
records = []
today = date.today()
for full_code, inst in data.items():
if not inst:
self.stats['skipped'] += 1
continue
code_num = full_code.split('.')[0]
# 判断交易所
exchange = inst.get('ExchangeID', '')
if not exchange:
if '.SH' in full_code:
exchange = 'SH'
elif '.SZ' in full_code:
exchange = 'SZ'
elif '.BJ' in full_code:
exchange = 'BJ'
# OpenDate 格式: 'YYYYMMDD' 或 int
open_date = inst.get('OpenDate', '')
if open_date and len(str(open_date)) == 8:
list_date_val = f'{str(open_date)[:4]}-{str(open_date)[4:6]}-{str(open_date)[6:8]}'
else:
list_date_val = None
# InstrumentStatus 含义:
# 0/1 = 正常股票(含已退市但仍在QMT列表的)
# -1 = ST / *ST
# 30/31 = *ST
# 已退市股(名字含XD/退)K线不足120日,会在候选股过滤时被排除
status = inst.get('InstrumentStatus', -1)
if status in (-1, 30, 31):
listing_status = 'ST'
else:
listing_status = 'normal'
total_share = inst.get('TotalVolume', None)
float_share = inst.get('FloatVolume', None)
records.append({
'code': full_code,
'name': str(inst.get('InstrumentName', '')),
'exchange': exchange,
'list_date': list_date_val,
'listing_status': listing_status,
'total_share': float(total_share) if total_share else None,
'float_share': float(float_share) if float_share else None,
'share_updated_at': today,
})
if records:
with db.atomic():
for batch in _chunked(records, 500):
StockInfo.insert_many(batch).on_conflict_replace().execute()
self.stats['inserted'] += len(records)
def _chunked(lst: list, n: int):
for i in range(0, len(lst), n):
yield lst[i:i + n]
-15
View File
@@ -1,15 +0,0 @@
# 删除交易标的事件
EventTradeTargetUpdate = "trade_target_update"
EventTradeTargetDeleted = "trade_target_deleted"
# 评分系统事件
EventScoringCompleted = "scoring_completed" # 评分完成, data: {'date', 'count'}
EventSyncProgress = "sync_progress" # 同步进度, data: {'source', 'status', 'stats'}
# 股票池管理器事件(阶段一)
# T日收盘标记完成, data: {action: 'eliminate'|'liquidate', stock_codes: list[str], count: int}
EventPoolMark = "pool_mark"
# T+1日执行完成(阶段二用)
# data: {action: 'eliminate'|'liquidate'|'refill', stock_codes: list[str], count: int}
EventPoolActionExecute = "pool_action_execute"
-486
View File
@@ -1,486 +0,0 @@
"""
pool_manager.py — 网格自动交易股票池管理器(阶段一:T日标记)
===================================================================
后台 daemon 线程运行,每日定时:
09:25 K线数据同步
09:30 执行评分
15:30 沉寂检测标记
周五15:30 额外执行周度淘汰标记
所有操作只记录到数据库,不执行真实交易(阶段二实现)。
"""
from __future__ import annotations
import threading
import time
from collections import defaultdict
from datetime import date, datetime, timedelta
import numpy as np
import pandas as pd
from core.logger import LogLevel, PrintLog
from core.scoring.models import PendingPoolAction, ScoringResult
from core.sfgrid.model import SFGridTradeTarget
from core.sfgrid.bus_events import EventPoolMark
from core.eventbus import event_bus
# ============================================================
# 配置
# ============================================================
TOP_N = 10 # 最大持仓数
TOP_MODEL_N = 50 # 评分池 Top N
ELIM_WINDOW = 2 # 连续 N 周不在 Top50 则淘汰
SLUMBER_DAYS = 10 # 沉寂触发天数(连续)
SLUMBER_TRIGGERS = 3 # 沉寂触发特征数(5个中触发几个)
# ============================================================
# 沉寂检测 — 直接复用 ref_backtest_strategy.py 的纯函数
# ============================================================
def is_slumbering(df: pd.DataFrame,
lookback_60: int = 60,
lookback_20: int = 20,
min_triggers: int = SLUMBER_TRIGGERS) -> bool:
"""
检测一只股是否陷入'沉寂'(资金离场后长期低位震荡)。
5 特征,>= min_triggers 触发则返回 True。
df 要求:包含 close/high/low/volume 列,index 为日期升序,
至少 60 条记录。
"""
if df is None or len(df) < lookback_60:
return False
# 过滤停牌日期(volume=0 的行会导致 log_ret = NaN
active = df[df["volume"] > 0]
if len(active) < lookback_60:
return False
sub = active.tail(lookback_60)
close = sub["close"].values
high = sub["high"].values
low = sub["low"].values
vol = sub["volume"].values
# 1. 波动率塌陷
log_ret = np.log(close[1:] / close[:-1])
if len(log_ret) < lookback_20:
return False
vol_20d = float(np.std(log_ret[-lookback_20:], ddof=1))
vol_60d = float(np.std(log_ret, ddof=1))
vol_collapse = (vol_60d > 0) and (vol_20d / vol_60d < 0.6)
# 2. 振幅萎缩
amp_20d = float(np.mean((high[-lookback_20:] - low[-lookback_20:]) / close[-lookback_20:]) * 100)
amp_shrink = amp_20d < 2.5
# 3. 成交量枯竭
avg_vol_20 = float(np.mean(vol[-lookback_20:]))
avg_vol_60 = float(np.mean(vol))
vol_dry = (avg_vol_60 > 0) and (avg_vol_20 / avg_vol_60 < 0.5)
# 4. 价格弱势
price_max_60 = float(np.max(close))
price_weak = price_max_60 > 0 and (close[-1] / price_max_60) < 0.85
# 5. 反弹失败
recent_high_30 = float(np.max(high[-30:]))
past_high_60 = float(np.max(high))
rebound_fail = past_high_60 > 0 and (recent_high_30 / past_high_60) < 0.95
triggers = [vol_collapse, amp_shrink, vol_dry, price_weak, rebound_fail]
return sum(triggers) >= min_triggers
# ============================================================
# 工具函数
# ============================================================
def is_trading_day(td: date) -> bool:
"""简单判断是否为交易日(周一~周五)"""
return td.weekday() < 5 # 0=周一, 4=周五
def get_week_id(td: date) -> int:
"""返回年内周序号(周一为起始)"""
return td.isocalendar()[1]
def seconds_to_target(target_hour: int, target_minute: int) -> float:
"""计算从现在到目标时间(当天 target_hour:target_minute)的秒数。"""
now = datetime.now()
today_target = datetime(now.year, now.month, now.day, target_hour, target_minute, 0)
if now >= today_target:
# 今天已过,推到明天
today_target += timedelta(days=1)
return (today_target - now).total_seconds()
# ============================================================
# 股票池管理器
# ============================================================
class PoolManager:
"""
网格股票池自动管理器(阶段一:T日标记)
使用 threading.Timer 递归调度,实现每日 09:25 / 09:30 / 15:30 定时任务。
所有操作只写入数据库,不执行真实交易。
"""
def __init__(self):
self._thread: threading.Thread | None = None
self._stop_event = threading.Event()
# 周度淘汰历史:key=股票代码,value=[(week_id, rank), ...]
# rank=0 表示在 Top50rank=-1 表示不在 Top50
self._top50_history: dict[str, list[tuple[int, int]]] = defaultdict(list)
# 调试:手动触发时传入自定义日期(仅供测试用)
self._override_date: date | None = None
# 加载历史排名数据(从 ScoringResult 重建)
self._rebuild_top50_history()
# ---- 对外控制接口 ----
def start(self):
"""启动后台管理线程(幂等)"""
if self._thread is not None and self._thread.is_alive():
PrintLog(LogLevel.WARNING, '[PoolManager] 已启动,忽略重复调用')
return
self._stop_event.clear()
self._thread = threading.Thread(target=self._run_loop, daemon=True, name='PoolManager')
self._thread.start()
PrintLog(LogLevel.INFO, '[PoolManager] 已启动')
def stop(self):
"""停止后台管理线程"""
self._stop_event.set()
if self._thread is not None:
self._thread.join(timeout=5)
self._thread = None
PrintLog(LogLevel.INFO, '[PoolManager] 已停止')
# ---- 每日定时任务 ----
def _run_loop(self):
"""后台线程主循环:计算出距下次任务的时间,注册下一个 Timer"""
while not self._stop_event.is_set():
now = datetime.now()
td = self._override_date or now.date()
# 确定当天要执行的任务及距其的秒数
delay, task_name = self._compute_next_delay(now, td)
# 保证 Timer 不会太久(最多 24 小时),处理节假日顺延
if delay <= 0:
delay = 60 # 异常时等 1 分钟重算
PrintLog(LogLevel.INFO,
f'[PoolManager] 计划任务 "{task_name}"{delay:.0f} 秒后执行')
timer = threading.Timer(delay, self._execute_task, args=(task_name, td))
timer.name = f'PoolManager-{task_name}'
timer.start()
# 等待定时器完成或停止信号
timer.join()
if self._stop_event.is_set():
break
def _compute_next_delay(self, now: datetime, td: date) -> tuple[float, str]:
"""
计算距下一个任务的时间和任务名称。
任务顺序:09:25 → 09:30 → 15:30 → (下一天 09:25)
"""
h, m = now.hour, now.minute
if h < 9 or (h == 9 and m < 25):
# 现在在 09:25 之前 → 先执行 09:25
return seconds_to_target(9, 25), '_sync_data'
elif h == 9 and 25 <= m < 30:
# 09:25~09:30 之间 → 立即执行 09:30
return 0.0, '_run_scoring'
elif (h == 9 and m >= 30) or h < 15:
# 09:30 之后、15:30 之前 → 执行 15:30
return seconds_to_target(15, 30), '_mark_slumber'
elif h >= 15:
# 15:30 之后 → 推到下一天 09:25
delay = seconds_to_target(9, 25) + (td.weekday() < 4 and 1 or 3) * 86400 # 工作日+1,周末+3
return delay, '_sync_data'
# 默认兜底
return seconds_to_target(9, 25), '_sync_data'
def _execute_task(self, task_name: str, td: date):
"""根据任务名执行对应任务"""
try:
if task_name == '_sync_data':
self._sync_data()
# 同步完成后自动调度评分
self._run_scoring()
# 调度 15:30
self._schedule_next(target_hour=15, target_minute=30,
task_name='_mark_slumber', td=td)
elif task_name == '_run_scoring':
self._run_scoring()
# 评分完成后调度沉寂检测
self._schedule_next(target_hour=15, target_minute=30,
task_name='_mark_slumber', td=td)
elif task_name == '_mark_slumber':
self._mark_slumber(td)
# 如果是周五,额外调度淘汰标记
if td.weekday() == 4: # 周五
self._mark_weekly_elim(td)
# 调度下一天 09:25
self._schedule_next(target_hour=9, target_minute=25,
task_name='_sync_data', td=td)
except Exception as e:
PrintLog(LogLevel.ERROR, f'[PoolManager] 任务 {task_name} 执行异常: {e}')
def _schedule_next(self, target_hour: int, target_minute: int,
task_name: str, td: date):
"""注册一个 Timer,在指定时间执行 task_name"""
# 计算 delay
delay = seconds_to_target(target_hour, target_minute)
# 如果 target 在过去(如周末顺延),加 1 天
if delay <= 0:
delay += 86400
def wrapper():
now = datetime.now()
# 重新计算真实日期
exec_td = self._override_date or now.date()
self._execute_task(task_name, exec_td)
timer = threading.Timer(delay, wrapper, name=f'PoolManager-sched-{task_name}')
timer.start()
# ---- 任务实现 ----
def _sync_data(self):
"""09:25 — 同步 K 线数据"""
PrintLog(LogLevel.INFO, '[PoolManager] 开始同步K线数据...')
try:
from core.scoring.sync.kline_sync import KlineStockSync
syncer = KlineStockSync()
syncer.run()
PrintLog(LogLevel.INFO, '[PoolManager] K线数据同步完成')
except Exception as e:
PrintLog(LogLevel.ERROR, f'[PoolManager] K线同步失败: {e}')
def _run_scoring(self):
"""09:30 — 执行评分"""
PrintLog(LogLevel.INFO, '[PoolManager] 开始执行评分...')
try:
from core.scoring.inference.scorer import GridSeekerPipeline
pipeline = GridSeekerPipeline()
td = self._override_date or date.today()
result = pipeline.run(trade_date=td)
if not result.empty:
pipeline.persist(result, trade_date=td)
PrintLog(LogLevel.INFO, f'[PoolManager] 评分完成,写入 {len(result)} 条结果')
# 触发 UI 刷新
event_bus.publish(EventPoolMark, {
'action': 'scoring',
'stock_codes': [],
'count': len(result),
})
else:
PrintLog(LogLevel.WARNING, '[PoolManager] 评分无结果')
except Exception as e:
PrintLog(LogLevel.ERROR, f'[PoolManager] 评分失败: {e}')
def _mark_slumber(self, td: date):
"""15:30 — 沉寂检测,标记待卖出股"""
if not is_trading_day(td):
PrintLog(LogLevel.INFO, '[PoolManager] 非交易日,跳过沉寂检测')
return
PrintLog(LogLevel.INFO, '[PoolManager] 开始沉寂检测...')
# 获取所有 enabled=True 的持仓
targets = SFGridTradeTarget.select().where(SFGridTradeTarget.enabled == True)
if not targets:
PrintLog(LogLevel.INFO, '[PoolManager] 无持仓,跳过沉寂检测')
return
marked: list[str] = []
today_str = td.strftime('%Y-%m-%d')
for tgt in targets:
try:
# 读取该股 K 线数据(从本地 SQLite KlineStock 表)
from core.scoring.models import KlineStock
klines = list(KlineStock
.select()
.where(KlineStock.stock_code == tgt.stock_code)
.order_by(KlineStock.trade_date)
.dicts())
if not klines or len(klines) < 60:
continue
df = pd.DataFrame(klines)
if 'trade_date' in df.columns and not pd.api.types.is_datetime64_any_dtype(df['trade_date']):
df['trade_date'] = pd.to_datetime(df['trade_date'])
df = df.set_index('trade_date').sort_index()
if is_slumbering(df):
self._write_pending(td, 'liquidate', tgt.stock_code,
reason=f'沉寂检测触发({today_str}')
marked.append(tgt.stock_code)
PrintLog(LogLevel.INFO,
f'[PoolManager] 沉寂标记: {tgt.stock_code}')
except Exception as e:
PrintLog(LogLevel.WARNING,
f'[PoolManager] 沉寂检测异常 {tgt.stock_code}: {e}')
if marked:
event_bus.publish(EventPoolMark, {
'action': 'liquidate',
'stock_codes': marked,
'count': len(marked),
})
PrintLog(LogLevel.INFO,
f'[PoolManager] 沉寂检测完成,标记 {len(marked)} 只股')
else:
PrintLog(LogLevel.INFO, '[PoolManager] 沉寂检测完成,无股触发')
def _mark_weekly_elim(self, td: date):
"""周五 15:30 — 周度评分淘汰标记(连续2周不在Top50则卖出)"""
if not is_trading_day(td):
return
PrintLog(LogLevel.INFO, '[PoolManager] 开始周度淘汰检测...')
# 获取最近 N 周的评分排名
week_id = get_week_id(td)
today_str = td.strftime('%Y-%m-%d')
# 查今日 Top50
top50_codes: Set[str] = set()
rows = (ScoringResult
.select(ScoringResult.stock_code)
.where(ScoringResult.trade_date == td)
.where(ScoringResult.score_rank <= TOP_MODEL_N)
.dicts())
for r in rows:
top50_codes.add(r['stock_code'])
if not top50_codes:
PrintLog(LogLevel.WARNING, '[PoolManager] 今日无评分数据,无法进行淘汰检测')
return
# 更新历史排名
targets = SFGridTradeTarget.select().where(SFGridTradeTarget.enabled == True)
for tgt in targets:
code = tgt.stock_code
rank_in_top50 = 0 if code in top50_codes else -1
self._top50_history[code].append((week_id, rank_in_top50))
# 只保留近 8 周记录,防止内存膨胀
if len(self._top50_history[code]) > 8:
self._top50_history[code] = self._top50_history[code][-8:]
# 检测连续 N 周不在 Top50 的股
marked: list[str] = []
for tgt in targets:
code = tgt.stock_code
hist = self._top50_history.get(code, [])
if len(hist) < ELIM_WINDOW:
continue
recent = hist[-ELIM_WINDOW:]
if all(rank == -1 for _, rank in recent):
self._write_pending(td, 'eliminate', code,
reason=f'连续{ELIM_WINDOW}周不在Top50{today_str}')
marked.append(code)
PrintLog(LogLevel.INFO,
f'[PoolManager] 淘汰标记: {code},历史: {recent}')
if marked:
event_bus.publish(EventPoolMark, {
'action': 'eliminate',
'stock_codes': marked,
'count': len(marked),
})
PrintLog(LogLevel.INFO,
f'[PoolManager] 周度淘汰检测完成,标记 {len(marked)} 只股')
else:
PrintLog(LogLevel.INFO, '[PoolManager] 周度淘汰检测完成,无股触发')
def _write_pending(self, td: date, action_type: str, stock_code: str, reason: str = ''):
"""写入 pending_pool_actions 表(幂等)"""
try:
PendingPoolAction.insert(
action_date=td,
action_type=action_type,
stock_code=stock_code,
reason=reason,
).on_conflict_replace().execute()
except Exception as e:
PrintLog(LogLevel.WARNING,
f'[PoolManager] 写入待处理操作失败: {e}')
def _rebuild_top50_history(self):
"""启动时从 ScoringResult 表重建 _top50_history(用于淘汰判断)"""
try:
rows = (ScoringResult
.select(ScoringResult.stock_code, ScoringResult.trade_date,
ScoringResult.score_rank)
.where(ScoringResult.score_rank <= TOP_MODEL_N)
.order_by(ScoringResult.trade_date)
.dicts())
week_groups: dict = defaultdict(list)
for r in rows:
td = r['trade_date']
if isinstance(td, str):
td = datetime.strptime(td, '%Y-%m-%d').date()
week_id = get_week_id(td)
week_groups[(r['stock_code'], week_id)].append(r['score_rank'])
# 取每周最新一条(排名最靠前的)
for (code, week_id), ranks in week_groups.items():
best_rank = min(ranks)
rank_val = 0 if best_rank <= TOP_MODEL_N else -1
self._top50_history[code].append((week_id, rank_val))
# 去重,每 week_id 只留一条
for code in self._top50_history:
seen = set()
cleaned = []
for w, r in self._top50_history[code]:
if w not in seen:
seen.add(w)
cleaned.append((w, r))
self._top50_history[code] = cleaned
PrintLog(LogLevel.INFO,
f'[PoolManager] 历史排名已重建,{len(self._top50_history)} 只股有历史数据')
except Exception as e:
PrintLog(LogLevel.ERROR, f'[PoolManager] 重建历史排名失败: {e}')
# ---- 手动触发(供 UI 调试按钮调用)----
def trigger_sync(self):
"""手动触发数据同步"""
threading.Thread(target=self._sync_data, daemon=True).start()
def trigger_scoring(self):
"""手动触发评分"""
threading.Thread(target=self._run_scoring, daemon=True).start()
def trigger_slumber(self, td: date | None = None):
"""手动触发沉寂检测"""
td = td or (self._override_date or date.today())
threading.Thread(target=self._mark_slumber, args=(td,), daemon=True).start()
def trigger_weekly_elim(self, td: date | None = None):
"""手动触发周度淘汰检测"""
td = td or (self._override_date or date.today())
threading.Thread(target=self._mark_weekly_elim, args=(td,), daemon=True).start()
-567
View File
@@ -1,567 +0,0 @@
"""
网格交易策略控制器
核心逻辑:在预设的价格网格上低买高卖,每个网格节点同时挂一对买卖单,
成交后自动切换到相邻网格并刷新订单。
网格结构示意(以 grid_index 为中心):
价格从高到低排列在 getPriceGrid() 列表中
grid_index=0 是最低价(底部),越大价格越高(顶部)
卖出方向(上移): grid_index - 1 (价格更低,空单)
买入方向(下移): grid_index + 1 (价格更高,多单)
成交 → 上移一格(卖出成交): grid_index -= 1,赚取一格差价
成交 → 下移一格(买入成交): grid_index += 1,持仓成本降低
状态机:
status=0: 未建仓,需先下建仓单买入初始仓位
status=1: 已建仓,运行网格交易(上下各挂一单)
"""
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
from xtquant import xtconstant
from xtquant.xttype import XtOrderError, XtOrderResponse, XtTrade
import threading
import core.eventbus as eBus
class SFGridStrategy:
"""
单标的网格交易策略控制器
每个 SFGridTradeTarget 数据库记录对应一个 SFGridStrategy 实例。
负责:建仓 → 挂网格单 → 监听成交/错误事件 → 调整网格 → 刷新订单。
订单 remark 格式: "{订单类型},{网格索引},{股票代码}"
例: "BUY,3,000001" 表示在网格索引 3 处挂买入单,标的 000001
例: "INIT,1,000001" 表示建仓单,建仓在网格索引 1
"""
def __init__(self, tradeTarget: model.SFGridTradeTarget):
"""
初始化网格策略控制器
参数:
tradeTarget: 数据库中的交易标记录,包含网格参数、当前状态等
"""
self.tradeTarget: model.SFGridTradeTarget = tradeTarget
# orderGrid 必须在所有可能触发回调的操作之前初始化
# orderGrid: 网格索引 → 订单编号(seq 或 order_id)的映射
# seq 是 xtquant 返回的下单序号(下单瞬间),order_id 是交易所返回的正式订单号(异步回调后更新)
self.orderGrid = {} # {grid_index: order_seq | order_id}
# 数据更新锁:保护 orderGrid 和 tradeTarget 的并发访问
# QMT 回调在独立线程中触发,必须在可能触发回调的操作之前创建
# 注意:这个锁必须在订阅事件之前创建,防止事件在初始化期间触发
# 注意:必须使用 RLock 而非 Lock,因为 refreshGridOrder 在持有此锁时也会被调用
#(如 onOrderTrade 回调中),Lock 会导致同一线程重复获取时永久阻塞(死锁)
self.dataUpdateLock = threading.RLock()
# 订阅事件总线:监听订单创建、成交、失败三种事件
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
event_bus.subscribe(eBus.MarketOrderError, self.onOrderError)
event_bus.subscribe(eBus.EventMarketActiveSwitch, self.onMarketActiveSwitch)
# 获取当日涨跌停价格(用于价格边界校验)
self.todayUpStopPrice = qmtv.dailyUpStop(tradeTarget.stock_code) # type: ignore
self.todayDownStopPrice = qmtv.dailyDownStop(tradeTarget.stock_code) # type: ignore
PrintLog(LogLevel.INFO,
f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造开始: '
f'网格={tradeTarget.grid_index}, 启用={tradeTarget.enabled}')
# 加载券商侧已存在的未成交订单,恢复到 orderGrid 中
self.loadExistOrders()
# 根据数据库中的 enabled 字段决定是否启动交易
self.enabledTrading(tradeTarget.enabled) # type: ignore
PrintLog(LogLevel.INFO,
f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造结束: '
f'grid_index={self.tradeTarget.grid_index}')
# ── 订单加载 ──────────────────────────────────────────────
def loadExistOrders(self):
"""
从券商侧加载该策略的未成交订单,恢复到 orderGrid
用于程序重启后恢复状态:数据库中可能没有记录所有挂单,
通过 queryPendingOrder 从 QMT 获取实际存在的订单。
"""
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
for order in orders:
# 只处理本策略的订单(通过 strategy_name 过滤)
if order.strategy_name != self.getName():
continue
parsed = self._parse_remark(order.order_remark)
if parsed is None:
continue
_, gridIdx, _ = parsed
self.orderGrid[gridIdx] = order.order_id
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 初始化: '
f'加载现有订单, 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):
"""
市场数据状态切换回调(由 UI 层调用)
当市场数据从不可用变为可用时,如果策略已启用则刷新网格订单。
"""
PrintLog(LogLevel.INFO,
f'|- [市场状态切换] 标的{self.tradeTarget.targetName()} '
f'isActive={isActive}, enabled={self.tradeTarget.enabled}')
if isActive and self.tradeTarget.enabled:
self.refreshGridOrder()
# ── 核心:网格下单逻辑 ────────────────────────────────────
def refreshGridOrder(self):
"""
刷新网格挂单 —— 策略的核心下单方法
逻辑分支:
1. 前置检查: 市场未激活 或 策略未启用 → 跳过不下单
2. status=0 (未建仓): 下一个建仓单(买入初始仓位)
3. status=1 (已建仓): 在 grid_index 上下各挂一单
- 上方 (sellIdx = grid_index - 1): 挂卖出单(价格更低时卖出获利)
- 下方 (buyIdx = grid_index + 1): 挂买入单(价格更低时补仓)
每个方向都先检查是否已存在同价位订单,避免重复下单
"""
# ── 前置检查:市场和策略状态 ──
# 注意:这里用 dataUpdateLock 包裹检查和下单操作,防止竞态条件:
# 主线程在检查 isMarketActive 时,另一线程的行情回调可能同时将其设为 True,
# 导致部分标的通过检查下单,部分被拦截(表现为一票有单、一票无单)
with self.dataUpdateLock:
if not qmtv.isMarketActive or not self.tradeTarget.enabled:
PrintLog(LogLevel.INFO,
f'|- 市场 {qmtv.isMarketActive}, 策略 {self.getName()} '
f'{self.tradeTarget.enabled}, 不下单')
return
# 获取当前该标的所有未成交订单
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
# ── 统一网格逻辑 ──
# grid_index=0 空仓: 只挂买单 @ grid[1],无持仓可卖
# grid_index>0 有仓: 上方挂卖单 @ grid[idx-1],下方挂买单 @ grid[idx+1]
if self.tradeTarget.grid_index >= 0:
currentIdx = self.tradeTarget.grid_index # type: ignore
# --- 上方挂卖出单(空单)---
# 条件: grid_index > 0,即当前位置不是价格最低点,还有向下(卖出)空间
if currentIdx > 0:
sellIdx = currentIdx - 1 # 向上一个网格
sellPrice = self.tradeTarget.getPriceGrid()[sellIdx]
sell_remark = self._make_remark(OrderTypeSell, sellIdx)
# 检查是否已存在同 remark 的卖单(避免重复挂单)
# 注意:必须同时查 QMT 订单簿和本地 orderGrid
# - QMT 订单簿:已确认的订单(onOrderCreateAsync 之后)
# - orderGrid:本地下单后、回调前的新单(orderAsync 返回后直接写入)
# 两者并集才能完整覆盖所有已存在订单,防止 onOrderCreateAsync 回调
# 之前再次触发 refreshGridOrder 导致重复下单
qmt_has_order = any(o.order_remark == sell_remark for o in orders)
local_has_order = sellIdx in self.orderGrid
if not qmt_has_order and not local_has_order:
# 卖单价格超过涨停价 → 今日无法成交,跳过下单
# 防御性检查:若属性未初始化(初始化顺序导致),先获取
if not hasattr(self, 'todayUpStopPrice') or self.todayUpStopPrice is None:
self.todayUpStopPrice = qmtv.dailyUpStop(self.tradeTarget.stock_code) # type: ignore
if sellPrice > self.todayUpStopPrice:
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] '
f'上方网格[{sellIdx}]卖价 {sellPrice:.3f} > 涨停价 {self.todayUpStopPrice:.3f}'
f'今日无法下卖单 (当前网格基准 grid-{currentIdx})')
else:
tmpOrderSeq = qmtv.orderAsync(
str(self.tradeTarget.stock_code),
self.tradeTarget.grid_volume,
xtconstant.STOCK_SELL, # 卖出
sellPrice,
xtconstant.FIX_PRICE,
sell_remark,
self.getName(),
)
self.orderGrid[sellIdx] = tmpOrderSeq
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
f'下空单,价格: {sellPrice:.3f}')
else:
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
f'已存在同价位空单,跳过下单')
# --- 下方挂买入单(多单)---
# 条件: grid_index < 价格网格长度-1,即当前位置不是价格最高点,还有向上(买入)空间
if currentIdx < len(self.tradeTarget.getPriceGrid()) - 1:
buyIdx = currentIdx + 1 # 向下一个网格
buyPrice = self.tradeTarget.getPriceGrid()[buyIdx]
buy_remark = self._make_remark(OrderTypeBuy, buyIdx)
# 检查是否已存在同 remark 的买单(避免重复挂单)
# 必须同时查 QMT 订单簿和本地 orderGrid(见上方卖单注释)
qmt_has_order = any(o.order_remark == buy_remark for o in orders)
local_has_order = buyIdx in self.orderGrid
if not qmt_has_order and not local_has_order:
# 买单价格低于跌停价 → 今日无法成交,跳过下单
# 防御性检查:若属性未初始化(初始化顺序导致),先获取
if not hasattr(self, 'todayDownStopPrice') or self.todayDownStopPrice is None:
self.todayDownStopPrice = qmtv.dailyDownStop(self.tradeTarget.stock_code) # type: ignore
if buyPrice < self.todayDownStopPrice:
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] '
f'下方网格[{buyIdx}]买价 {buyPrice:.3f} < 跌停价 {self.todayDownStopPrice:.3f}'
f'今日无法下买单 (当前网格基准 grid-{currentIdx})')
else:
tmpOrderSeq = qmtv.orderAsync(
str(self.tradeTarget.stock_code),
self.tradeTarget.grid_volume,
xtconstant.STOCK_BUY, # 买入
buyPrice,
xtconstant.FIX_PRICE,
buy_remark,
self.getName(),
)
self.orderGrid[buyIdx] = tmpOrderSeq
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
f'下多单,价格: {buyPrice:.3f}')
else:
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
f'已存在同价位多单,跳过下单')
else:
# grid_index 已到达价格网格上边界,无法再挂买入单(价格已经到顶)
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
f'已过下边界,停止多单交易')
# ── 标的管理 ──────────────────────────────────────────────
def deleteTradeTarget(self, tradeTarget: model.SFGridTradeTarget):
"""
从数据库中删除该交易标的
同时发布 EventTradeTargetDeleted 事件通知 UI 刷新。
"""
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:
"""
启用或停用该标的的网格交易
启用时 (enabled=True):
- grid_index=0 空仓: 直接调用 refreshGridOrder(只挂买单)
- grid_index>0 有仓: 检查持仓是否满足 grid_volume × grid_index
满足则刷新网格单,不满足则回退 enabled=False(风控保护)
停用时 (enabled=False):
- 取消该标的所有未成交订单,停止交易监控
"""
PrintLog(LogLevel.INFO,
f" |- [DEBUG] enabledTrading({enabled}) 调用前: "
f"grid_index={self.tradeTarget.grid_index}")
self.tradeTarget.enabled = enabled # type: ignore
if enabled:
# ── 启用交易 ──
PrintLog(LogLevel.INFO,
f" |- 标的{self.tradeTarget.targetName()}交易启动, "
f"持仓量:{self.tradeTarget.current_position}")
if self.tradeTarget.grid_index == 0:
# 空仓: refreshGridOrder 会在 grid[1] 挂第一笔买单
PrintLog(LogLevel.INFO,
f" |- 标的{self.tradeTarget.targetName()}空仓, "
f"等待首次买入建仓")
else:
# 有仓: 检查现有持仓是否满足当前网格位置的仓位需求
# 最小需求仓位 = 每格股数 × 当前网格索引
# 例: grid_volume=100, grid_index=3 → 需持股 300 股
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' |- 仓位检查: 持仓需求充足, '
f'(gridVolume*gridIndex)={minRequirePosition}, '
f'当前持仓:{self.tradeTarget.current_position}')
else:
PrintLog(LogLevel.INFO,
f' |- 仓位检查: 持仓需求不足, '
f'(gridVolume*gridIndex)={minRequirePosition}, '
f'当前持仓:{self.tradeTarget.current_position}, '
f'交易启动失败')
self.tradeTarget.enabled = False # type: ignore
# 刷新网格订单(空仓只挂买单,有仓买卖对冲)
# 只有市场活跃时才下单,收盘后不再尝试下单
if qmtv.isMarketActive:
self.refreshGridOrder()
else:
PrintLog(LogLevel.INFO,
f' |- 市场已休市,跳过刷新网格订单')
else:
# ── 停用交易: 取消所有未成交订单 ──
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
for order in orders:
try:
qmtv.xt_trader.cancel_order_stock_async(qmtv.account, order.order_id)
except AttributeError:
pass # 模拟模式无 xt_trader,跳过撤单
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:
"""查询交易是否已启用"""
PrintLog(LogLevel.DEBUG, f'|- 检查交易状态[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - {self.tradeTarget.enabled}')
return bool(self.tradeTarget.enabled)
# ── 事件回调: 订单创建 ────────────────────────────────────
def onOrderCreateAsync(self, response: XtOrderResponse):
"""
QMT 异步下单成功回调
xtquant 下单是异步的:orderAsync() 返回 seq(序号),
交易所确认后通过此回调返回正式的 order_id。
此处将 orderGrid 中的临时 seq 替换为正式 order_id。
"""
parsed = self._filter_event(response.order_remark, response.strategy_name)
if parsed is None:
return
_, gridIdx, _ = parsed
self.dataUpdateLock.acquire()
try:
PrintLog(LogLevel.INFO,
f"委托创建通知 onOrderCreateAsync[{self.tradeTarget.targetName()}]: "
f"{response.order_id}")
# 将 orderGrid 中的临时 seq 替换为正式 order_id
self.orderGrid[gridIdx] = response.order_id
PrintLog(LogLevel.INFO,
f"委托创建通知 onOrderCreateAsync 更新 grid-{gridIdx} "
f"seq:{response.seq} -> order_id:{response.order_id}")
except Exception as e:
PrintLog(LogLevel.ERROR,
f"|- 委托创建通知 onOrderCreateAsync"
f"[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}]: "
f"{response.order_id} - {str(e)}")
finally:
self.dataUpdateLock.release()
# ── 事件回调: 订单失败 ────────────────────────────────────
def onOrderError(self, order_error: XtOrderError):
"""
QMT 委托失败回调
当 xtquant 拒绝订单时触发(如资金不足、代码格式错误、涨跌停限制等)。
清理 orderGrid 中对应网格索引的孤立条目,防止后续 refreshGridOrder
误判"已有同价位订单"而跳过重新下单。
"""
parsed = self._filter_event(order_error.order_remark, order_error.strategy_name)
if parsed is None:
return
_, gridIdx, _ = parsed
self.dataUpdateLock.acquire()
try:
# 从 orderGrid 中移除失败的订单条目,后续 refreshGridOrder 会重新挂单
if gridIdx in self.orderGrid:
del self.orderGrid[gridIdx]
PrintLog(LogLevel.ERROR,
f'委托失败[{self.tradeTarget.targetName()}] grid-{gridIdx}: '
f'order_id={order_error.order_id}, error_id={order_error.error_id}, '
f'error_msg={order_error.error_msg}')
except Exception as e:
PrintLog(LogLevel.ERROR,
f'委托失败处理异常[{self.tradeTarget.stock_code}]: {str(e)}')
finally:
self.dataUpdateLock.release()
# ── 事件回调: 订单成交 ────────────────────────────────────
def onOrderTrade(self, trade: XtTrade):
"""
QMT 委托成交通知回调
收到成交后:
1. 判断成交方向(买入下移 / 卖出上移)→ 更新 grid_index
2. 首次建仓(grid_index==0 时成交)→ 记录 init_price
3. 卖出成交 → 累计 grid_match_count 和 grid_total_profit
4. 清理 orderGrid → 持久化 → 刷新网格挂单
"""
# ── 过滤:只处理本策略本标的的成交 ──
parsed = self._filter_event(trade.order_remark, trade.strategy_name)
if parsed is None:
return
_, gridIdx, _ = parsed # gridIdx: 成交订单对应的网格索引(int)
PrintLog(LogLevel.INFO,
f'|- 委托成交通知'
f'[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}] : '
f'{trade.order_id}')
self.dataUpdateLock.acquire()
try:
# ── 首次建仓:记录建仓价 ──
# grid_index==0 表示成交前处于空仓状态,这笔成交就是首次建仓
if self.tradeTarget.grid_index == 0:
self.tradeTarget.init_price = trade.traded_price # type: ignore
# ── 同步更新持仓量 ──
# 注意:xtquant 的成交推送不包含最新持仓,此处根据成交方向估算变动
# 买入成交(建仓/补仓)→ 持仓增加
# 卖出成交(减仓/清仓)→ 持仓减少
if gridIdx > self.tradeTarget.grid_index:
# 买入方向:持仓增加
self.tradeTarget.current_position += int(trade.traded_volume) # type: ignore
elif gridIdx < self.tradeTarget.grid_index:
# 卖出方向:持仓减少
self.tradeTarget.current_position -= int(trade.traded_volume) # type: ignore
# ── 网格方向判断 ──
# 比较成交单的网格索引 vs 当前网格索引,判断价格移动方向
oriIdx = self.tradeTarget.grid_index # 成交前的网格位置
if gridIdx > self.tradeTarget.grid_index:
# 成交单在下方(更大索引 = 更低价格)→ 买入成交,持仓下移
self.tradeTarget.grid_index += 1 # type: ignore
# 首次建仓时 oriIdx==0,加上"建仓单"前缀便于识别
desc = "建仓单(下移)" if oriIdx == 0 else "下移一格"
elif gridIdx < self.tradeTarget.grid_index:
# 成交单在上方(更小索引 = 更高价格)→ 卖出成交,持仓上移
self.tradeTarget.grid_index -= 1 # type: ignore
# 卖出获利:累计匹配次数和利润
self.tradeTarget.grid_match_count += 1 # type: ignore
# 单格利润 = grid_size × 成交量
self.tradeTarget.grid_total_profit += ( # type: ignore
self.tradeTarget.grid_size * trade.traded_volume)
desc = "上移一格"
else:
# gridIdx == grid_index: 同格成交,正常情况下不会出现
desc = "同格(异常)"
PrintLog(LogLevel.INFO,
f'|- [{self.tradeTarget.targetName()}] '
f'原网格 {oriIdx} → 现网格 {self.tradeTarget.grid_index}'
f'{desc}')
# ── 成交后统一处理 ──
# 1. 持久化状态到数据库(grid_index、持仓量等已变更)
self.saveProxy()
# 2. 从 orderGrid 清理已成交订单(pop 防 xtquant 重复推送 KeyError
self.orderGrid.pop(gridIdx, None)
# 3. 打印成交报告
PrintLog(LogLevel.INFO,
f"|- 成交报告[{self.tradeTarget.targetName()}] : "
f"====================================")
PrintLog(LogLevel.INFO,
f"|- 标的[{self.tradeTarget.targetName()}] "
f"{desc}-单号{trade.order_id}已成交 ")
PrintLog(LogLevel.INFO,
f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
PrintLog(LogLevel.INFO,
f' 手续费 : {trade.commission:.3f}')
# 4. 刷新网格订单:在新的 grid_index 位置重新挂买卖单
# 只有市场活跃时才下单,收盘后不再尝试下单
if qmtv.isMarketActive:
self.refreshGridOrder()
else:
PrintLog(LogLevel.INFO,
f'|- 成交后市场已休市,跳过刷新网格订单')
finally:
self.dataUpdateLock.release()
# ── 工具方法 ──────────────────────────────────────────────
def _make_remark(self, order_tag: str, grid_idx: int) -> str:
"""构建订单 remark: '{type},{gridIdx},{stockCode}'"""
return f'{order_tag},{grid_idx},{self.tradeTarget.stock_code}'
@staticmethod
def _parse_remark(remark: str):
"""
解析订单 remark → (orderType:str, gridIdx:int, stockCode:str)
格式不符返回 None
"""
if not remark:
return None
parts = remark.split(',')
if len(parts) < 3:
return None
try:
return parts[0], int(parts[1]), parts[2]
except (ValueError, IndexError):
return None
def _filter_event(self, remark: str, strategy_name: str):
"""
事件过滤器:解析 remark 并校验是否属于本策略本标的
通过返回 parsed tuple,不通过返回 None
"""
parsed = self._parse_remark(remark)
if parsed is None:
return None
if strategy_name != self.getName() or self.tradeTarget.stock_code != parsed[2]:
return None
return parsed
def getName(self):
"""返回策略名称,用于在 QMT 中标识订单归属"""
return "SFGRID"
def saveProxy(self):
"""
持久化 tradeTarget 到数据库,并发布 UI 更新事件
每次状态变更后调用,确保数据库与内存一致,
同时通知 UI 刷新表格显示。
"""
PrintLog(LogLevel.DEBUG,
f'|- [DEBUG] saveProxy: {self.tradeTarget.targetName()} '
f'网格={self.tradeTarget.grid_index}')
rc = self.tradeTarget.save()
event_bus.publish(EventTradeTargetUpdate, self.tradeTarget)
return rc
+3
View File
@@ -0,0 +1,3 @@
# 删除交易标的事件
EventTradeTargetUpdate = "trade_target_update"
EventTradeTargetDeleted = "trade_target_deleted"
@@ -2,10 +2,6 @@ from peewee import CharField, IntegerField, FloatField, BooleanField
from core.database import BaseModel, db
# 策略类型常量
STRATEGY_TYPE_UNCLASSIFIED = 0 # 未分类持仓
STRATEGY_TYPE_GRID = 1 # 网格策略
# 定义Target类,对应targets表
class SFGridTradeTarget(BaseModel):
@@ -16,13 +12,12 @@ class SFGridTradeTarget(BaseModel):
init_price = FloatField(null=True) # 建仓成本
grid_match_count = IntegerField(default=0)
grid_total_profit = FloatField(default=0.0)
status = IntegerField(default=0) # 已废弃,改用 strategy_type + grid_index
status = IntegerField(default=0) # -1表示新标的,未完成交易配置,0表示新标的,已完成交易配置,1表示已建初始仓,正常交易中
enabled = BooleanField(default=False) # 是否启动交易线程
strategy_type = IntegerField(default=0) # 0=未分类, 1=网格策略
grid_start_price = FloatField(default=10.0) # 基线价格
grid_size = FloatField(default=1.0) # 网格价位差
grid_volume = IntegerField(default=200) # 网格交易量
grid_size = FloatField(default=0.1) # 网格价位差
grid_volume = IntegerField(default=100) # 网格交易量
grid_upper_count = IntegerField(default=1) # 基线价格上方网格数
grid_lower_count = IntegerField(default=10) # 基线价格下方网格数
@@ -47,14 +42,3 @@ class SFGridTradeTarget(BaseModel):
db.create_tables([SFGridTradeTarget])
# 数据库迁移: 为已有表添加 strategy_type 字段(如果不存在)
try:
from playhouse.migrate import migrate, SqliteMigrator
migrator = SqliteMigrator(db)
migrate(
migrator.add_column('sfgridtradetarget', 'strategy_type', SFGridTradeTarget.strategy_type),
)
except Exception:
# 字段已存在或迁移失败 — 静默跳过
pass
+223
View File
@@ -0,0 +1,223 @@
from core.logger import LogLevel, PrintLog
from core.qmt import qmtv
from core.sfgrid import bus_events
from core.sfgrid.bus_events import EventTradeTargetUpdate
import core.sfgrid.model as model
from core.eventbus import event_bus
from core.constants import OrderTypeBuy, OrderTypeSell, OrderTypeInit
from xtquant import xtconstant
from xtquant.xttype import XtOrderResponse, XtTrade
import threading
import core.eventbus as eBus
class SFGridStrategy:
def __init__(self, tradeTarget: model.SFGridTradeTarget):
self.tradeTarget:model.SFGridTradeTarget = tradeTarget
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
self.todayUpStopPrice=qmtv.dailyUpStop(tradeTarget.stock_code) # type: ignore
self.todayDownStopPrice=qmtv.dailyDownStop(tradeTarget.stock_code) # type: ignore
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}初始化: 停涨价 {self.todayUpStopPrice:.3f}, 停跌价 {self.todayDownStopPrice:.3f}')
self.orderGrid = {} # grid index, order_seq | order_id
self.loadExistOrders()
self.enabledTrading(tradeTarget.enabled) # type: ignore
self.dataUpdateLock = threading.Lock()
def loadExistOrders(self):
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
for order in orders:
if order.strategy_name != self.getName():
continue
gridIdx = int(order.order_remark.split(',')[1])
self.orderGrid[gridIdx] = order.order_id
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 初始化: 加载现有订单, grid-{gridIdx} order_id:{self.orderGrid[gridIdx]}')
def printPendingOrder(self):
for idx, order_id in self.orderGrid.items():
PrintLog(LogLevel.DEBUG, f" {idx} : {order_id}")
def onMarketActiveSwitch(self, isActive: bool):
if isActive and self.tradeTarget.enabled:
self.refreshGridOrder()
def refreshGridOrder(self): # 下网格单
if not qmtv.isMarketActive or not self.tradeTarget.enabled:
PrintLog(LogLevel.INFO, f'|- 市场 {qmtv.isMarketActive}, 策略 {self.getName()} {self.tradeTarget.enabled}, 不下单')
return
currentIdx:int = 0
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
if self.tradeTarget.status == 0 and len([order for order in orders if order.order_remark == f'{OrderTypeInit},1,{self.tradeTarget.stock_code}']) == 0: # status == 0 表示已配置好交易参数,且不存在执行中的建仓单
price = self.tradeTarget.getPriceGrid()[0]
remark = f'{OrderTypeInit},1,{self.tradeTarget.stock_code}'
tmpOrderSeq = qmtv.orderAsync(
str(self.tradeTarget.stock_code),
self.tradeTarget.grid_volume,
xtconstant.STOCK_BUY,
price,
xtconstant.FIX_PRICE,
remark, # remark # type: ignore
self.getName(), # strategy_name
)
self.orderGrid[1] = tmpOrderSeq # seq
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 初始化: 建仓单,建仓价: {price:.3f}')
elif self.tradeTarget.status == 1: # 下网格单
currentIdx = self.tradeTarget.grid_index # type: ignore
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
# 向上下一单,向下下一单
if currentIdx > 0: # 可以下空单
sellIdx = currentIdx - 1
sellPrice = self.tradeTarget.getPriceGrid()[sellIdx]
remark = f'{OrderTypeSell},{sellIdx},{self.tradeTarget.stock_code}'
if len([order for order in orders if order.order_remark == remark]) == 0: # 网格节点没有卖单,下单
# 不存在策略内同价位订单,下单
tmpOrderSeq = qmtv.orderAsync(
str(self.tradeTarget.stock_code),
self.tradeTarget.grid_volume,
xtconstant.STOCK_SELL,
sellPrice,
xtconstant.FIX_PRICE,
remark, # remark # type: ignore
self.getName(), # strategy_name
)
self.orderGrid[sellIdx] = tmpOrderSeq # seq
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 下空单,价格: {sellPrice:.3f}')
else:
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已存在同价位空单,跳过下单')
if currentIdx < len(self.tradeTarget.getPriceGrid()) - 1: # 可以下多单
print(f'length: {len(self.tradeTarget.getPriceGrid())}, currentIdx = {currentIdx}')
buyIdx = currentIdx + 1
buyPrice = self.tradeTarget.getPriceGrid()[buyIdx]
remark = f'{OrderTypeBuy},{buyIdx},{self.tradeTarget.stock_code}'
if len([order for order in orders if order.order_type == xtconstant.STOCK_BUY and order.price == buyPrice]) == 0:
tmpOrderSeq = qmtv.orderAsync(
str(self.tradeTarget.stock_code),
self.tradeTarget.grid_volume,
xtconstant.STOCK_BUY,
buyPrice,
xtconstant.FIX_PRICE,
remark, # remark # type: ignore
self.getName(), # strategy_name
)
self.orderGrid[buyIdx] = tmpOrderSeq # seq
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 下多单,价格: {buyPrice:.3f}')
else:
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已存在同价位多单,跳过下单')
else:
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已过下边界,停止多单交易')
def deleteTradeTarget(self, tradeTarget:model.SFGridTradeTarget):
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}信息删除: START')
self.dataUpdateLock.acquire()
try:
tradeTarget.delete_instance()
event_bus.publish(bus_events.EventTradeTargetDeleted, tradeTarget)
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}信息删除: END')
finally:
self.dataUpdateLock.release()
def enabledTrading(self, enabled: bool) -> model.SFGridTradeTarget:
self.tradeTarget.enabled = enabled # type: ignore
if enabled:
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易启动, 持仓量:{self.tradeTarget.current_position}")
if self.tradeTarget.status == 0: # 未建仓
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}初始状态, 设置网格序号 1,")
self.tradeTarget.grid_index = 1 # pyright: ignore[reportAttributeAccessIssue]
else: # 已建仓
# 交易阶段,检查仓位,检查现有订单
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 无需建初始仓 当前仓位: {self.tradeTarget.current_position} 状态: {self.tradeTarget.status}")
minRequirePosition:int = self.tradeTarget.grid_volume * int(self.tradeTarget.grid_index) # type: ignore
if minRequirePosition <= int(self.tradeTarget.current_position): # type: ignore
PrintLog(LogLevel.INFO, f' |- 仓位检查: 持仓需求充足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}')
else:
PrintLog(LogLevel.INFO, f' |- 仓位检查: 持仓需求不足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}, 交易启动失败')
self.tradeTarget.enabled = False # type: ignore
self.refreshGridOrder()
else:
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
for order in orders:
qmtv.xttrader.cancel_order_stock_async(qmtv.account, order.order_id)
if len(orders) > 0:
PrintLog(LogLevel.INFO, f' |- 取消未成交订单 {len(orders)}')
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易监控暂停")
self.saveProxy()
return self.tradeTarget
def isEnabled(self) -> bool:
print(f'|- 检查交易状态[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - {self.tradeTarget.enabled}')
return bool(self.tradeTarget.enabled) # 修复返回类型问题
def onOrderCreateAsync(self, response:XtOrderResponse): # 下单成功回调,更新orderID到 self.orderGrid
remark = response.order_remark.split(',')
stockCode = remark[2] # 从remark中获取stockCode
if response.strategy_name != self.getName() or len(remark) < 3 or self.tradeTarget.stock_code != stockCode:
return
self.dataUpdateLock.acquire()
try:
gridIdx = remark[1] # 从remark中获取gridIdx
PrintLog(LogLevel.INFO, f"委托创建通知 onOrderCreateAsync[{self.tradeTarget.targetName()}]: {response.order_id}")
self.orderGrid[gridIdx] = response.order_id
PrintLog(LogLevel.INFO, f"委托创建通知 onOrderCreateAsync 更新 grid-{gridIdx} seq:{response.seq} -> order_id:{response.order_id}")
except Exception as e:
PrintLog(LogLevel.ERROR, f"|- 委托创建通知 onOrderCreateAsync[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}]: {response.order_id} - {str(e)}")
finally:
self.dataUpdateLock.release()
def onOrderTrade(self, trade:XtTrade): # TODO 委托成交通知,处理成交后网格切换
remark = trade.order_remark.split(',')
if trade.strategy_name != self.getName() or len(remark) < 3 or self.tradeTarget.stock_code != trade.stock_code:
return
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}] : {trade.order_id}')
self.dataUpdateLock.acquire()
try:
orderType = trade.order_remark.split(',')[0]
gridIdx = trade.order_remark.split(',')[1] # 从remark中获取gridIdx
type:str = ""
if orderType == OrderTypeInit:
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] - 建仓单成交')
self.tradeTarget.status = 1 # type: ignore
self.tradeTarget.init_price = trade.traded_price # type: ignore
self.tradeTarget.grid_index = 1 # type: ignore
type = "建仓单"
else:
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] - 网格单成交')
oriIdx = self.tradeTarget.grid_index
if gridIdx > self.tradeTarget.grid_index:
type = "下移一格"
self.tradeTarget.grid_index +=1
elif gridIdx < self.tradeTarget.grid_index:
type = "上移一格"
self.tradeTarget.grid_match_count += 1
self.tradeTarget.grid_total_profit += self.tradeTarget.grid_size * trade.traded_volume
self.tradeTarget.grid_index -= 1
else:
type = "保持格, 理论上不应该输出"
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} - 原网格位置 {oriIdx}, 现网格位置 {self.tradeTarget.grid_index}')
self.saveProxy()
del self.orderGrid[gridIdx]
PrintLog(LogLevel.INFO, f"|- 成交报告[{self.tradeTarget.targetName()}] : ====================================")
PrintLog(LogLevel.INFO, f"|- 标的[{self.tradeTarget.targetName()}] {type}-单号{trade.order_id}已成交 ")
PrintLog(LogLevel.INFO, f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
PrintLog(LogLevel.INFO, f' 手续费 : {trade.commission:.3f}')
self.refreshGridOrder() # 更新网格订单
finally:
self.dataUpdateLock.release()
def getName(self):
return "SFGRID"
def saveProxy(self):
rc = self.tradeTarget.save()
event_bus.publish(EventTradeTargetUpdate, self.tradeTarget)
return rc
+985
View File
@@ -0,0 +1,985 @@
from typing import Any
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime
import threading
import time
import core.eventbus as eBus
from core.logger import LogLevel, PrintLog
from core.sfgrid import bus_events
from core.sfgrid.model import SFGridTradeTarget
from core.qmt import qmtv
from core.sfgrid.sfgrid_strategy import SFGridStrategy
class TradeTargetUI(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.tradeTargetData:dict[int, SFGridTradeTarget] = {} # id->trade_target
self.stockCodeIdMap:dict[str, int] = {}
self.strategy_ctrl:dict[int, SFGridStrategy] = {} # stock_code->trade_target
self.targetMarketPrice: dict[int, float] = {}
self.targetAvgPrice: dict[int, float] = {}
self.listening_stock = []
# 监控价格,默认值为10
self.monitor_price = 10.0
self.init_trade_target_pool()
# 市场监控数据
self.marketData: dict[str, Any] = {} # 存储市场数据 {stock_code: {stock_name, last_price, time}}
# 市场监控窗口显示状态
self.market_monitor_visible = True
# 创建界面
self.create_ui()
eBus.event_bus.subscribe(eBus.MarketDataUpdate, self.onMarketDataUpdated)
eBus.event_bus.subscribe(bus_events.EventTradeTargetUpdate, self.onStrategyUpdate)
eBus.event_bus.subscribe(bus_events.EventTradeTargetDeleted, self.onTradeTargetDeleted)
def init_trade_target_pool(self):
results = SFGridTradeTarget.select()
for temp in results:
tradeTarget:SFGridTradeTarget = temp
pos = qmtv.getStockPosition(tradeTarget.stock_code)
tradeTarget.current_position = 0 if pos is None else pos.volume # type: ignore
if pos is None:
self.targetAvgPrice[tradeTarget.get_id()] = 0.0
else:
self.targetAvgPrice[tradeTarget.get_id()] = pos.avg_price
PrintLog(LogLevel.INFO, f'- [成功]获取持仓信息: {tradeTarget.stock_code} {tradeTarget.targetName()} {tradeTarget.current_position} {pos.avg_price}')
self.updateTradeTarget(tradeTarget, True) # 初始化的时候
PrintLog(LogLevel.INFO, f'- [成功]交易标的信息初始化, 共 {len(self.tradeTargetData)} 个标的')
# 收集所有市场数据用于市场监控
def onMarketDataUpdated(self, data):
for stock_code, tickData in data.items():
if stock_code in self.stockCodeIdMap:
id:int = self.stockCodeIdMap[stock_code]
self.targetMarketPrice[id] = tickData['lastPrice']
tradeTarget = self.tradeTargetData[id]
# timeStr = datetime.fromtimestamp(tickData['time']/1000)
lastPrice = float("{:.3f}".format(tickData['lastPrice']))
tradeTarget.market_price = lastPrice # type: ignore
# PrintLog(LogLevel.INFO, f'|- 市价更新[{tradeTarget.targetName()}] - {timeStr.strftime("%H:%M:%S")} 市价更新: {lastPrice}======================{id}')
self.updateTradeTarget(tradeTarget, False) # 市价更新
else:
# 非目标交易,发布市场数据更新事件用于市场监控
lastPrice = tickData['lastPrice']
# 使用用户设置的监控价格替代硬编码的10
if lastPrice == self.monitor_price or stock_code in self.listening_stock:
# 发布市场数据更新事件用于市场监控
if stock_code not in self.listening_stock:
self.listening_stock.append(stock_code)
# 更新市场监控数据用于UI显示
current_time = datetime.now().strftime("%H:%M:%S")
self.marketData[str(stock_code)] = {
'stock_name': qmtv.getInstrumentName(stock_code),
'last_price': tickData['lastPrice'],
'time': current_time
}
# 来自策略的数据更新
def onStrategyUpdate(self, target: SFGridTradeTarget):
id = target.get_id()
self.tradeTargetData[id] = target
# priceChange 用于控制是否对更新价格数据,进行交易判断
def updateTradeTarget(self, target: SFGridTradeTarget, save: bool = True):
if save:
target.save()
id = target.get_id()
# PrintLog(LogLevel.INFO, f' [序号-{id}] 股票代码: {target.stock_code}-{target.stock_name}: {target.plan_buy_price} {target.plan_sell_price}') # type: ignore
# 更新或添加数据到本地缓存
self.tradeTargetData[id] = target
if id not in self.strategy_ctrl:
self.stockCodeIdMap[target.stock_code] = id # type: ignore
self.strategy_ctrl[id] = SFGridStrategy(target) # pyright: ignore[reportArgumentType]
if id in self.targetAvgPrice:
pos = qmtv.getStockPosition(target.stock_code)
if pos is not None:
self.targetAvgPrice[id] = pos.avg_price
# UI CREATE
def create_ui(self):
"""创建UI界面"""
# 主框架(使用self作为父容器)
main_frame = ttk.Frame(self)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建工具栏
toolbar_frame = ttk.Frame(main_frame)
toolbar_frame.pack(fill=tk.X, pady=(0, 10))
# 工具栏按钮
ttk.Button(toolbar_frame, text=" 添加标的",
command=self.btnHandlerAddTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🗑 删除标的",
command=self.btnHandlerDelSelectedTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="▶️ 启动交易",
command=self.btnHandlerStartSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="⏸ 暂停交易",
command=self.btnHandlerStopSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🛠 交易设置",
command=self.btnHandlerTradeSettings, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="▣ 边栏",
command=self.btnHandlerToggleMarketMonitor, width=8).pack(side=tk.RIGHT, padx=2)
# 添加价格监控输入字段和确认按钮
ttk.Button(toolbar_frame, text="确认",
command=self.btnHandlerSetMonitorPrice, width=8).pack(side=tk.RIGHT, padx=2)
self.monitor_price_entry = ttk.Entry(toolbar_frame, width=8)
self.monitor_price_entry.insert(0, str(self.monitor_price))
self.monitor_price_entry.pack(side=tk.RIGHT, padx=2)
ttk.Label(toolbar_frame, text="价格").pack(side=tk.RIGHT, padx=(20, 2))
ttk.Label(toolbar_frame, text="监控配置").pack(side=tk.RIGHT, padx=(20, 2))
# 表格区域
self.create_tables_area(main_frame)
# 启动刷新线程
self.refresh_thread = threading.Thread(target=self.refresh_loop, daemon=True)
self.refresh_thread.start()
def refresh_loop(self):
"""刷新循环"""
while True:
self.after(0, self.refresh_table)
self.after(0, self.populate_market_table)
time.sleep(0.5) # 每0.5秒刷新一次
def create_tables_area(self, parent):
"""创建表格区域"""
# 创建主表格框架(水平排列)
tables_frame = ttk.Frame(parent)
tables_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
# 左侧交易标的区域
trade_frame = ttk.LabelFrame(tables_frame, text="交易标的详情", padding=10)
trade_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# 创建交易标的表格
self.create_trade_target_table(trade_frame)
# 右侧市场监控区域
self.market_frame = ttk.LabelFrame(tables_frame, text="市场监控", padding=10)
self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
# 创建市场监控表格
self.create_market_monitor_table(self.market_frame)
def create_trade_target_table(self, parent):
"""创建交易标的表格"""
columns = ("ID",
"股票代码", "股票名称", "市场价", "当前持仓", "建仓成本",
"平均成本", "网格匹配次数", "网格收益", "交易状态"
)
self.trade_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
# 专业化的列配置
column_configs = {
"ID": (50, tk.CENTER),
"股票代码": (80, tk.CENTER),
"股票名称": (80, tk.E),
"市场价": (70, tk.E),
"当前持仓": (80, tk.E),
"建仓成本": (60, tk.E),
"平均成本": (60, tk.E),
"网格匹配次数": (60, tk.E),
"网格收益": (60, tk.E),
"交易状态": (80, tk.CENTER)
}
for col in columns:
width, anchor = column_configs[col]
self.trade_table.heading(col, text=col)
self.trade_table.column(col, width=width, anchor=anchor) # type: ignore
# 填充数据
self.populate_trade_table()
# 滚动条
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.trade_table.yview)
self.trade_table.configure(yscrollcommand=scrollbar.set)
self.trade_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 绑定双击事件
self.trade_table.bind("<Double-1>", self.on_table_double_click)
def create_market_monitor_table(self, parent):
"""创建市场监控表格"""
columns = ("时间", "股票名称", "最新价格")
self.market_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
# 列配置
column_configs = {
"时间": (50, tk.CENTER),
"股票名称": (80, tk.CENTER),
"最新价格": (50, tk.CENTER)
}
for col in columns:
width, anchor = column_configs[col]
self.market_table.heading(col, text=col)
self.market_table.column(col, width=width, anchor=anchor) # type: ignore
# 滚动条
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.market_table.yview)
self.market_table.configure(yscrollcommand=scrollbar.set)
self.market_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 绑定双击事件
self.market_table.bind("<Double-1>", self.on_market_table_double_click)
# 填充初始数据
self.populate_market_table()
def populate_market_table(self):
"""填充市场监控表格数据"""
# 保存当前选中的项
selected_items = self.market_table.selection()
selected_values = []
for item in selected_items:
values = self.market_table.item(item)['values']
if values:
selected_values.append(values[1]) # 保存股票代码
# 清空现有数据
for item in self.market_table.get_children():
self.market_table.delete(item)
# 填充市场数据
tmp = self.marketData.copy()
for stock_code, data in tmp.items():
# 处理时间格式,仅显示 hh:mm:ss
time_str = data['time']
# 如果时间字符串包含空格,说明包含日期和时间,只取时间部分
if ' ' in time_str:
time_str = time_str.split(' ')[1]
# 确保时间格式为 hh:mm:ss,如果只有 hh:mm 则补充 :00
if ':' in time_str:
time_components = time_str.split(':')
if len(time_components) == 2:
# 只有小时和分钟,补充秒
time_str = f"{time_components[0]}:{time_components[1]}:00"
elif len(time_components) >= 3:
# 有小时、分钟和秒,只取前三个部分
time_str = f"{time_components[0]}:{time_components[1]}:{time_components[2]}"
values = [
time_str,
data['stock_name']+f"-{stock_code}",
f"{data['last_price']:.3f}",
stock_code
]
self.market_table.insert('', tk.END, values=values)
# 恢复之前选中的项
if selected_values:
for item in self.market_table.get_children():
values = self.market_table.item(item)['values']
if values and values[1] in selected_values: # 比较股票代码
self.market_table.selection_add(item)
def on_market_table_double_click(self, event):
"""市场监控表格双击事件"""
selected = self.market_table.selection()
if selected:
item = selected[0]
values = self.market_table.item(item)['values']
print(values)
stock_name = values[1]
last_price = values[2]
stock_code = values[3]
# 检查是否已在交易池中
is_in_trade_pool = any(target.stock_code == stock_code for target in self.tradeTargetData.values())
if is_in_trade_pool:
messagebox.showinfo("提示", f"{stock_code} ({stock_name}) 已在交易池中")
else:
result = messagebox.askyesno(
"添加交易标的",
f"确定要将以下股票添加到交易池吗?\n\n"
f"股票代码: {stock_code}\n"
f"股票名称: {stock_name}\n"
f"最新价格: {last_price}"
)
if result:
# 发布事件通知主控制器添加标的
self.addTradeTarget(stock_code)
def get_trade_enabled_indicator(self, target: SFGridTradeTarget) -> str:
"""获取交易状态指示器"""
if target.status == -1:
return "请做交易设置"
elif target.status >= 0:
if target.enabled:
return "▶ 运行中"
else:
return "⏸ 已停止"
def populate_trade_table(self):
"""填充交易标的表格数据"""
for id, target in self.tradeTargetData.items():
values = [
id,
target.stock_code, # "股票代码"
target.stock_name, # "股票名称"
f"{self.targetMarketPrice[id]:.3f}" if id in self.targetMarketPrice else '-', # "市场价"
target.current_position, # "当前持仓"
'-' if target.init_price is None else f"{target.init_price:.3f}", # "建仓成本"
f"{self.targetAvgPrice[id]:.3f}", # "平均成本"
target.grid_match_count, # "网格匹配次数"
f"{target.grid_total_profit:.3f}", # "网格收益"
self.get_trade_enabled_indicator(target) # type: ignore
]
self.trade_table.insert('', tk.END, values=values)
def on_table_double_click(self, event):
"""表格双击事件"""
selected = self.trade_table.selection()
if selected:
item = selected[0]
values = self.trade_table.item(item)['values']
ctrl = self.strategy_ctrl[values[0]]
PrintLog(LogLevel.DEBUG, f"双击查看详情: {values[0]} - {values[1]}")
PrintLog(LogLevel.DEBUG, f"双击查看详情 - 订单网格")
ctrl.printPendingOrder()
def get_selected_target(self):
"""获取选中的交易标的"""
selected = self.trade_table.selection()
if not selected:
messagebox.showwarning("未选中", "请先选择一个交易标的")
return None
# 获取选中行的ID
item = selected[0]
values = self.trade_table.item(item)['values']
target_id = values[0]
# 从列表中找到对应的target对象
for id in self.tradeTargetData:
if int(target_id) == id: # type: ignore
return self.tradeTargetData[id]
return None
def refresh_table(self):
"""刷新表格数据"""
# 保存当前选中的项
selected_items = self.trade_table.selection()
selected_values = []
for item in selected_items:
values = self.trade_table.item(item)['values']
if values:
selected_values.append(values[0]) # 保存ID
# 清空表格
for item in self.trade_table.get_children():
self.trade_table.delete(item)
# 重新填充
self.populate_trade_table()
# 恢复之前选中的项
if selected_values:
for item in self.trade_table.get_children():
values = self.trade_table.item(item)['values']
if values and values[0] in selected_values:
self.trade_table.selection_add(item)
# 刷新市场监控表格
self.populate_market_table()
def create_grid_view_window(self, target: SFGridTradeTarget):
"""创建网格配置查看窗口(只读)"""
# 获取顶层窗口
root = self.winfo_toplevel()
# 创建顶层窗口
view_window = tk.Toplevel(root)
view_window.title(f"网格配置查看 - {target.stock_code} ({target.stock_name})")
view_window.geometry("500x450")
view_window.resizable(False, False)
# 设置窗口模态
view_window.transient(root)
view_window.grab_set()
# 居中显示
root.update_idletasks()
x = root.winfo_x() + (root.winfo_width() // 2) - 250
y = root.winfo_y() + (root.winfo_height() // 2) - 225
view_window.geometry(f"500x450+{x}+{y}")
# 创建主框架
main_frame = ttk.Frame(view_window, padding=20)
main_frame.pack(fill=tk.BOTH, expand=True)
# 显示股票信息
info_frame = ttk.LabelFrame(main_frame, text="标的详情", padding=10)
info_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(info_frame, text=f"股票代码: {target.stock_code}").grid(row=0, column=0, sticky=tk.W, pady=2)
ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2)
ttk.Label(info_frame, text=f"状态: 已建初始仓(仅查看模式)").grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=2)
# 创建网格配置查看框架
config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=10)
config_frame.pack(fill=tk.X, pady=(0, 10))
# 基准价格
base_price_frame = ttk.Frame(config_frame)
base_price_frame.pack(fill=tk.X, pady=5)
ttk.Label(base_price_frame, text="基准价格:", width=15).pack(side=tk.LEFT)
ttk.Label(base_price_frame, text=f"{target.grid_start_price:.3f}", width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
ttk.Label(base_price_frame, text="", foreground='gray').pack(side=tk.LEFT)
# 网格大小
grid_size_frame = ttk.Frame(config_frame)
grid_size_frame.pack(fill=tk.X, pady=5)
ttk.Label(grid_size_frame, text="网格大小:", width=15).pack(side=tk.LEFT)
ttk.Label(grid_size_frame, text=f"{target.grid_size:.3f}", width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
ttk.Label(grid_size_frame, text="", foreground='gray').pack(side=tk.LEFT)
# 网格交易量
grid_volume_frame = ttk.Frame(config_frame)
grid_volume_frame.pack(fill=tk.X, pady=5)
ttk.Label(grid_volume_frame, text="网格交易量:", width=15).pack(side=tk.LEFT)
ttk.Label(grid_volume_frame, text=str(target.grid_volume), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
ttk.Label(grid_volume_frame, text="", foreground='gray').pack(side=tk.LEFT)
# 上方网格数量
upper_count_frame = ttk.Frame(config_frame)
upper_count_frame.pack(fill=tk.X, pady=5)
ttk.Label(upper_count_frame, text="上方网格数量:", width=15).pack(side=tk.LEFT)
ttk.Label(upper_count_frame, text=str(target.grid_upper_count), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
ttk.Label(upper_count_frame, text="", foreground='gray').pack(side=tk.LEFT)
# 下方网格数量
lower_count_frame = ttk.Frame(config_frame)
lower_count_frame.pack(fill=tk.X, pady=5)
ttk.Label(lower_count_frame, text="下方网格数量:", width=15).pack(side=tk.LEFT)
ttk.Label(lower_count_frame, text=str(target.grid_lower_count), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5)
ttk.Label(lower_count_frame, text="", foreground='gray').pack(side=tk.LEFT)
# 生成网格价格序列
price_grid_frame = ttk.LabelFrame(main_frame, text="网格价格序列", padding=10)
price_grid_frame.pack(fill=tk.X, pady=(0, 10))
# 计算并显示网格价格序列
price_list = target.getPriceGrid()
price_text = ", ".join([f"{price:.3f}" for price in price_list])
# 创建文本框显示网格价格序列
text_frame = ttk.Frame(price_grid_frame)
text_frame.pack(fill=tk.BOTH, expand=True)
text_widget = tk.Text(text_frame, height=4, wrap=tk.WORD)
text_widget.insert(tk.END, price_text)
text_widget.config(state=tk.DISABLED) # 只读
scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_widget.yview)
text_widget.configure(yscrollcommand=scrollbar.set)
text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 关闭按钮
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Button(button_frame, text="关闭", command=view_window.destroy).pack(side=tk.RIGHT, padx=5)
def create_grid_config_window(self, target: SFGridTradeTarget):
"""创建网格配置窗口(可编辑)"""
# 获取顶层窗口
root = self.winfo_toplevel()
# 创建顶层窗口
config_window = tk.Toplevel(root)
config_window.title(f"网格配置 - {target.stock_code} ({target.stock_name})")
config_window.geometry("550x550")
config_window.resizable(False, False)
# 设置窗口模态
config_window.transient(root)
config_window.grab_set()
# 居中显示
root.update_idletasks()
x = root.winfo_x() + (root.winfo_width() // 2) - 275
y = root.winfo_y() + (root.winfo_height() // 2) - 275
config_window.geometry(f"550x550+{x}+{y}")
# 创建主框架
main_frame = ttk.Frame(config_window, padding=20)
main_frame.pack(fill=tk.BOTH, expand=True)
# 显示股票信息
info_frame = ttk.LabelFrame(main_frame, text="标的详情", padding=10)
info_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(info_frame, text=f"股票代码: {target.stock_code}").grid(row=0, column=0, sticky=tk.W, pady=2)
ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2)
ttk.Label(info_frame, text=f"状态: 新标的(可配置模式)").grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=2)
# 创建网格配置框架
config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=15)
config_frame.pack(fill=tk.X, pady=(0, 10))
# 创建输入框字典用于保存引用
entries = {}
# 基准价格
base_price_frame = ttk.Frame(config_frame)
base_price_frame.pack(fill=tk.X, pady=5)
ttk.Label(base_price_frame, text="基准价格:", width=15).pack(side=tk.LEFT)
base_price_entry = ttk.Entry(base_price_frame, width=15)
base_price_entry.insert(0, str(target.grid_start_price))
base_price_entry.pack(side=tk.LEFT, padx=5)
ttk.Label(base_price_frame, text="", foreground='gray').pack(side=tk.LEFT)
entries['grid_start_price'] = base_price_entry
# 网格大小
grid_size_frame = ttk.Frame(config_frame)
grid_size_frame.pack(fill=tk.X, pady=5)
ttk.Label(grid_size_frame, text="网格大小:", width=15).pack(side=tk.LEFT)
grid_size_entry = ttk.Entry(grid_size_frame, width=15)
grid_size_entry.insert(0, str(target.grid_size))
grid_size_entry.pack(side=tk.LEFT, padx=5)
ttk.Label(grid_size_frame, text="", foreground='gray').pack(side=tk.LEFT)
entries['grid_size'] = grid_size_entry
# 网格交易量
grid_volume_frame = ttk.Frame(config_frame)
grid_volume_frame.pack(fill=tk.X, pady=5)
ttk.Label(grid_volume_frame, text="网格交易量:", width=15).pack(side=tk.LEFT)
grid_volume_entry = ttk.Entry(grid_volume_frame, width=15)
grid_volume_entry.insert(0, str(target.grid_volume))
grid_volume_entry.pack(side=tk.LEFT, padx=5)
ttk.Label(grid_volume_frame, text="", foreground='gray').pack(side=tk.LEFT)
entries['grid_volume'] = grid_volume_entry
# 上方网格数量
upper_count_frame = ttk.Frame(config_frame)
upper_count_frame.pack(fill=tk.X, pady=5)
ttk.Label(upper_count_frame, text="上方网格数量:", width=15).pack(side=tk.LEFT)
upper_count_entry = ttk.Entry(upper_count_frame, width=15)
upper_count_entry.insert(0, str(target.grid_upper_count))
upper_count_entry.pack(side=tk.LEFT, padx=5)
ttk.Label(upper_count_frame, text="", foreground='gray').pack(side=tk.LEFT)
entries['grid_upper_count'] = upper_count_entry
# 下方网格数量
lower_count_frame = ttk.Frame(config_frame)
lower_count_frame.pack(fill=tk.X, pady=5)
ttk.Label(lower_count_frame, text="下方网格数量:", width=15).pack(side=tk.LEFT)
lower_count_entry = ttk.Entry(lower_count_frame, width=15)
lower_count_entry.insert(0, str(target.grid_lower_count))
lower_count_entry.pack(side=tk.LEFT, padx=5)
ttk.Label(lower_count_frame, text="", foreground='gray').pack(side=tk.LEFT)
entries['grid_lower_count'] = lower_count_entry
# 预览按钮和结果显示
preview_frame = ttk.LabelFrame(main_frame, text="网格价格序列预览", padding=10)
preview_frame.pack(fill=tk.X, pady=(0, 10))
preview_result = tk.StringVar(value="点击'预览'查看生成的网格价格序列")
def calculate_grid_prices():
"""计算网格价格序列"""
try:
base_price = float(base_price_entry.get())
grid_size = float(grid_size_entry.get())
upper_count = int(upper_count_entry.get())
lower_count = int(lower_count_entry.get())
prices = []
# 计算上方网格价格
for i in range(upper_count, 0, -1):
price = base_price + grid_size * i
prices.append(round(price, 3))
# 添加基准价格
prices.append(base_price)
# 计算下方网格价格
for i in range(1, lower_count + 1):
price = base_price - grid_size * i
# 确保价格不为负
if price >= 0:
prices.append(round(price, 3))
else:
break
return prices
except ValueError:
return None
def update_preview():
"""更新网格价格序列预览"""
prices = calculate_grid_prices()
if prices:
price_str = ", ".join([str(p) for p in prices])
preview_result.set(f"网格价格序列: {price_str}")
else:
preview_result.set("参数错误,请检查输入!")
# 绑定输入变化自动预览
for entry_widget in entries.values():
entry_widget.bind("<KeyRelease>", lambda e: update_preview())
entry_widget.bind("<FocusOut>", lambda e: update_preview())
# 预览按钮
preview_button_frame = ttk.Frame(preview_frame)
preview_button_frame.pack(fill=tk.X, pady=5)
# ttk.Button(preview_button_frame, text="预览", command=update_preview).pack(side=tk.LEFT)
# 预览结果显示
preview_label = ttk.Label(preview_button_frame, textvariable=preview_result, foreground='blue')
preview_label.pack(side=tk.LEFT, padx=10)
# 初始预览
update_preview()
# 按钮框架
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=(10, 0))
def save_config():
"""保存配置"""
try:
# 获取输入值
grid_start_price = float(base_price_entry.get())
grid_size = float(grid_size_entry.get())
grid_volume = int(grid_volume_entry.get())
grid_upper_count = int(upper_count_entry.get())
grid_lower_count = int(lower_count_entry.get())
# 更新target对象(使用setattr来正确设置Peewee字段的值)
setattr(target, 'grid_start_price', grid_start_price)
setattr(target, 'grid_size', grid_size)
setattr(target, 'grid_volume', grid_volume)
setattr(target, 'grid_upper_count', grid_upper_count)
setattr(target, 'grid_lower_count', grid_lower_count)
setattr(target, 'status', 0)
# 更新策略控制器
self.updateTradeTarget(target, True) # 网格配置变更
# 关闭窗口
config_window.destroy()
# 添加日志
PrintLog(LogLevel.INFO, f"网格配置已保存: {target.stock_code} - {target.stock_name}")
messagebox.showinfo("成功", "网格配置已保存!")
except ValueError:
messagebox.showerror("错误", "输入参数有误,请检查!")
except Exception as e:
messagebox.showerror("错误", f"保存配置失败:{str(e)}")
PrintLog(LogLevel.ERROR, f"保存网格配置失败: {str(e)}")
# 保存和取消按钮
ttk.Button(button_frame, text="保存", command=save_config).pack(side=tk.RIGHT, padx=5)
ttk.Button(button_frame, text="取消", command=config_window.destroy).pack(side=tk.RIGHT, padx=5)
def decrease_grid_index(self, grid_index_var: tk.IntVar, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
"""减少网格序号"""
current_value = grid_index_var.get()
if current_value > 0:
grid_index_var.set(current_value - 1)
# 同步更新需求持仓量和持仓状态
self.update_required_position_and_status(grid_index_var.get(), target, required_position_label, position_status_label)
def increase_grid_index(self, grid_index_var: tk.IntVar, max_index: int, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
"""增加网格序号"""
current_value = grid_index_var.get()
if current_value < max_index:
grid_index_var.set(current_value + 1)
# 同步更新需求持仓量和持仓状态
self.update_required_position_and_status(grid_index_var.get(), target, required_position_label, position_status_label)
def update_position_status(self, current_position: int, required_position: int, status_label: ttk.Label):
"""更新持仓量状态提示"""
if current_position >= required_position:
status_label.config(text="持仓量充足", foreground="green")
else:
shortage = required_position - current_position
status_label.config(text=f"还需补充 {shortage} 手仓位", foreground="red")
def update_required_position_and_status(self, grid_index: int, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label):
"""更新需求持仓量和持仓状态"""
# 计算需求持仓量
required_position:int = grid_index * target.grid_volume # type: ignore
required_position_label.config(text=str(required_position))
# 更新持仓量状态
current_position = getattr(target, 'current_position')
self.update_position_status(current_position, required_position, position_status_label)
# 交易池管理
def addTradeTarget(self, stock_code: str, gridIndex: int = 1): # 新增
"""处理添加交易标的事件"""
try:
stock_name = qmtv.getInstrumentName(stock_code)
if not stock_name:
PrintLog(LogLevel.ERROR, f'无法获取股票代码 {stock_code} 的名称,请检查代码是否正确')
return
PrintLog(LogLevel.DEBUG, f'添加交易标的: {stock_code} {stock_name}')
# 检查是否已存在该标的
existing_target = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == stock_code)
if existing_target:
PrintLog(LogLevel.INFO, f'交易标的 {stock_code} {stock_name} 已存在')
return
# 刷新标的持仓
pos = qmtv.getStockPosition(stock_code) # type: ignore
new_target = SFGridTradeTarget.create(
stock_name=stock_name,
stock_code=stock_code,
current_position="0" if pos is None else str(pos.volume),
grid_index=gridIndex,
init_price=0.0,
status=-1
)
# 更新标的池
self.updateTradeTarget(new_target, True) # 新增标的,相当于也是初始化
except Exception as e:
PrintLog(LogLevel.ERROR, f'新增交易标的失败 {stock_code} {e}')
# button handlers =============================================================================================
def btnHandlerGridCorrect(self):
target = self.get_selected_target()
if not target:
return
self.create_grid_correction_window(target)
def btnHandlerToggleMarketMonitor(self):
"""切换市场监控窗口显示/隐藏"""
if self.market_monitor_visible:
# 隐藏市场监控窗口
self.market_frame.pack_forget()
self.market_monitor_visible = False
else:
# 显示市场监控窗口
self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
self.market_monitor_visible = True
def btnHandlerTradeSettings(self):
"""网格配置功能"""
target = self.get_selected_target()
if not target:
return
# 检查标的的状态,status为1时仅可查看
if target.status == -1 or target.status == 0:
self.create_grid_config_window(target)
else:
# 创建只读的网格配置查看窗口
self.create_grid_view_window(target)
def btnHandlerStartSelectedTrade(self):
"""启动选中的交易"""
target = self.get_selected_target()
if not target:
return
if target.status < 0:
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 未配置交易参数, 请做交易设置。")
return
if target.enabled: # type: ignore
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经在运行中")
return
result = messagebox.askyesno(
"确认启动",
f"确定要启动以下交易标的吗?\n\n"
f"股票代码: {target.stock_code}\n"
f"股票名称: {target.stock_name}"
)
if result:
PrintLog(LogLevel.INFO, f'启动标的交易 {target.targetName()}')
target.enabled = True # type: ignore
id = target.get_id()
if id in self.strategy_ctrl:
tradeController: SFGridStrategy = self.strategy_ctrl[target.get_id()]
tradeTarget = tradeController.enabledTrading(True)
self.tradeTargetData[id] = tradeTarget
else:
PrintLog(LogLevel.INFO, f"\t创建标的交易控制器 {target.targetName()}")
def btnHandlerStopSelectedTrade(self):
"""暂停选中的交易"""
target = self.get_selected_target()
if not target:
return
if not target.enabled: # type: ignore
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经是暂停状态")
return
result = messagebox.askyesno(
"确认暂停",
f"确定要暂停以下交易标的吗?\n\n"
f"股票代码: {target.stock_code}\n"
f"股票名称: {target.stock_name}"
)
if result:
PrintLog(LogLevel.INFO, f'暂停标的交易 {target.targetName()}')
id = target.get_id()
if id in self.strategy_ctrl:
tradeController: SFGridStrategy = self.strategy_ctrl[target.get_id()]
tradeController.enabledTrading(False)
else:
print(f"标的交易控制器不存在 {target.stock_code} {target.stock_name}\n")
def btnHandlerDelSelectedTradeTarget(self):
"""删除选中的交易标的"""
target = self.get_selected_target()
if not target:
return
result = messagebox.askyesno(
"确认删除",
f"确定要删除以下交易标的吗?\n\n"
f"股票代码: {target.stock_code}\n"
f"股票名称: {target.stock_name}\n\n"
f"⚠️ 此操作不可恢复!",
icon='warning'
)
if result:
id = target.get_id()
# try:
if id in self.strategy_ctrl:
ctrl = self.strategy_ctrl[id]
ctrl.deleteTradeTarget(target)
else:
self.onTradeTargetDeleted(target)
PrintLog(LogLevel.INFO, f"已发送删除请求: {target.stock_code} - {target.stock_name}")
def onTradeTargetDeleted(self, target: SFGridTradeTarget):
id = target.get_id()
del self.tradeTargetData[id]
del self.strategy_ctrl[id]
del self.stockCodeIdMap[target.stock_code] # type: ignore
def btnHandlerAddTradeTarget(self):
"""添加新的交易标的"""
# 获取顶层窗口
root = self.winfo_toplevel()
# 创建顶层窗口
add_window = tk.Toplevel(root)
add_window.title("添加交易标的")
add_window.geometry("400x150")
add_window.resizable(False, False)
# 设置窗口模态
add_window.transient(root)
add_window.grab_set()
# 居中显示
root.update_idletasks()
x = root.winfo_x() + (root.winfo_width() // 2) - 200
y = root.winfo_y() + (root.winfo_height() // 2) - 75
add_window.geometry(f"400x150+{x}+{y}")
# 创建输入框架
input_frame = ttk.Frame(add_window, padding=20)
input_frame.pack(fill=tk.BOTH, expand=True)
# 股票代码输入
ttk.Label(input_frame, text="股票代码:").grid(row=0, column=0, sticky=tk.W, pady=5)
stock_code_entry = ttk.Entry(input_frame, width=30)
stock_code_entry.grid(row=0, column=1, pady=5, padx=(10, 0))
stock_code_entry.focus()
# 按钮框架
button_frame = ttk.Frame(input_frame)
button_frame.grid(row=1, column=0, columnspan=2, pady=20)
def confirm_add():
stock_code = stock_code_entry.get().strip()
if not stock_code:
messagebox.showwarning("输入错误", "请输入股票代码")
return
# 发布事件通知主控制器添加标的
self.addTradeTarget(stock_code)
add_window.destroy()
def cancel_add():
add_window.destroy()
# 确认和取消按钮
ttk.Button(button_frame, text="确认", command=confirm_add, width=10).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="取消", command=cancel_add, width=10).pack(side=tk.LEFT, padx=5)
# 绑定回车键确认
stock_code_entry.bind('<Return>', lambda event: confirm_add())
PrintLog(LogLevel.INFO, "点击添加交易标的按钮")
def btnHandlerSetMonitorPrice(self):
"""设置监控价格"""
try:
# 获取输入的价格
price_str = self.monitor_price_entry.get()
new_price = float(price_str)
# 更新监控价格
self.monitor_price = new_price
# 清空当前监控的数据
self.marketData.clear()
self.listening_stock.clear()
# 清空市场监控表格
for item in self.market_table.get_children():
self.market_table.delete(item)
PrintLog(LogLevel.INFO, f"监控价格已更新为: {new_price}")
except ValueError:
messagebox.showerror("错误", "请输入有效的数字")
+149
View File
@@ -0,0 +1,149 @@
# coding:utf-8
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import configparser
from core.main_ui import MainWindow
import config as sdConstants
from core.qmt import qmtv
class ConfigWindow:
def __init__(self, root):
self.root = root
self.root.title("系统配置")
self.root.geometry("500x250")
self.root.resizable(False, False)
# 居中显示
self.root.withdraw() # 先隐藏窗口
self.root.update_idletasks()
x = (self.root.winfo_screenwidth() // 2) - (500 // 2)
y = (self.root.winfo_screenheight() // 2) - (250 // 2)
self.root.geometry(f"500x250+{x}+{y}")
self.root.deiconify() # 再显示窗口
self.miniQMTPath = tk.StringVar()
self.account_no = tk.StringVar()
self.create_widgets()
def create_widgets(self):
# 创建主框架
main_frame = ttk.Frame(self.root, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
# miniQMT路径配置
path_frame = ttk.Frame(main_frame)
path_frame.pack(fill=tk.X, pady=5)
path_label = ttk.Label(path_frame, text="miniQMT路径:")
path_label.pack(side=tk.LEFT)
path_entry = ttk.Entry(path_frame, textvariable=self.miniQMTPath, width=40)
path_entry.pack(side=tk.LEFT, padx=(10, 5), fill=tk.X, expand=True)
browse_btn = ttk.Button(path_frame, text="浏览", command=self.browse_folder)
browse_btn.pack(side=tk.LEFT)
# 资金账号配置
account_frame = ttk.Frame(main_frame)
account_frame.pack(fill=tk.X, pady=5)
account_label = ttk.Label(account_frame, text="资金账号:")
account_label.pack(side=tk.LEFT)
account_entry = ttk.Entry(account_frame, textvariable=self.account_no, width=40)
account_entry.pack(side=tk.LEFT, padx=(10, 0))
# 说明文本
info_label = ttk.Label(
main_frame,
text="请配置miniQMT的userdata_mini路径和资金账号\n路径示例: D:/Programs/DTQMT/userdata_mini",
foreground="gray"
)
info_label.pack(pady=10)
# 按钮框架
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=10)
save_btn = ttk.Button(button_frame, text="保存配置", command=self.save_config)
save_btn.pack(side=tk.RIGHT)
cancel_btn = ttk.Button(button_frame, text="取消", command=self.root.destroy)
cancel_btn.pack(side=tk.RIGHT, padx=(0, 10))
def browse_folder(self):
folder_selected = filedialog.askdirectory()
if folder_selected:
self.miniQMTPath.set(folder_selected)
def save_config(self):
mini_qmt_path = self.miniQMTPath.get().strip()
account_number = self.account_no.get().strip()
# 检查miniQMT路径
if not mini_qmt_path:
messagebox.showerror("错误", "请选择miniQMT路径")
return
if not os.path.exists(mini_qmt_path):
messagebox.showerror("错误", "miniQMT路径不存在")
return
# 检查账号
if not account_number:
messagebox.showerror("错误", "请输入资金账号")
return
# 保存配置
try:
sdConstants.save_config(mini_qmt_path.replace('\\', '/'), account_number)
messagebox.showinfo("成功", "配置已保存")
self.root.destroy()
except Exception as e:
messagebox.showerror("错误", f"保存配置失败: {str(e)}")
def check_and_create_config():
"""检查配置文件,如果不存在则打开配置窗口"""
root = tk.Tk()
config_window = ConfigWindow(root)
root.mainloop()
def initialize_system():
"""初始化系统"""
try:
while True:
# 初始化配置
if sdConstants.exist_config() and sdConstants.initConfig():
# 初始化qmtv
qmtv.init_qmtv()
connected = qmtv.connect()
if connected:
# 连接成功,启动主窗口
window = MainWindow(sdConstants.log_level)
window.run()
break
else:
option = messagebox.askokcancel("连接失败", "QMT连接失败,请检查")
if option:
check_and_create_config()
else:
break
else:
option = messagebox.askokcancel("错误", "请检查配置")
if option:
check_and_create_config()
else:
break
except Exception as e:
messagebox.showerror("错误", f"系统初始化失败: {str(e)}")
if __name__ == "__main__":
import tkinter as tk
root = tk.Tk()
app = MainBoardWindow(root)
app.run()
# initialize_system()
View File
View File
File diff suppressed because it is too large Load Diff
-98
View File
@@ -1,98 +0,0 @@
# 统一网格逻辑
## 核心规则
对任意 `grid_index`,两个方向各挂一单:
| 方向 | 条件 | 价格 | 含义 |
|------|------|------|------|
| 卖出(上移) | `grid_index > 0` | `grid[grid_index - 1]` | 涨回到上一格时卖出获利 |
| 买入(下移) | `grid_index < len(grid)-1` | `grid[grid_index + 1]` | 跌到下一格时补仓 |
不需要"建仓"概念,`grid_index=0` 自然表示空仓。
## 流程图
```mermaid
flowchart TD
START["refreshGridOrder()"] --> GUARD{"isMarketActive AND enabled ?"}
GUARD -->|No| EXIT0["跳过不下单"]
GUARD -->|Yes| QUERY["查询未成交订单<br/>queryPendingOrder()"]
QUERY --> IDX["currentIdx = grid_index"]
IDX --> SELL{"currentIdx > 0 ?"}
SELL -->|"No<br/>(空仓,无持仓可卖)"| BUY
SELL -->|"Yes"| SELL_IDX["sellIdx = currentIdx - 1<br/>卖价 = grid[sellIdx]"]
SELL_IDX --> SELL_EXIST{"已有同 remark 卖单?"}
SELL_EXIST -->|No| SELL_CHECK{"卖价 > 涨停价 ?"}
SELL_CHECK -->|Yes| SELL_SKIP["跳过(超出涨停)"]
SELL_CHECK -->|No| SELL_PLACE["挂卖出单<br/>orderGrid[sellIdx] = seq"]
SELL_EXIST -->|Yes| SELL_DUP["跳过(已挂单)"]
SELL_SKIP --> BUY
SELL_PLACE --> BUY
SELL_DUP --> BUY
BUY{"currentIdx < len(grid)-1 ?"}
BUY -->|"No<br/>(已到最低价)"| EXIT["结束"]
BUY -->|"Yes"| BUY_IDX["buyIdx = currentIdx + 1<br/>买价 = grid[buyIdx]"]
BUY_IDX --> BUY_EXIST{"已有同 remark 买单?"}
BUY_EXIST -->|No| BUY_CHECK{"买价 < 跌停价 ?"}
BUY_CHECK -->|Yes| BUY_SKIP["跳过(低于跌停)"]
BUY_CHECK -->|No| BUY_PLACE["挂买入单<br/>orderGrid[buyIdx] = seq"]
BUY_EXIST -->|Yes| BUY_DUP["跳过(已挂单)"]
BUY_SKIP --> EXIT
BUY_PLACE --> EXIT
BUY_DUP --> EXIT
```
## 三种典型状态
```
grid = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
↑ ↑ ↑ ↑
0 1 2 3 ...
grid_index=0(空仓):
┌────┬────┬────┬────┐
│ 11 │ 10 │ 9 │ 8 │ ...
└────┴────┴────┴────┘
sell=无 buy=10 ← 第一笔买单
grid_index=1(持1份@10元):
┌────┬────┬────┬────┐
│ 11 │ 10 │ 9 │ 8 │ ...
└────┴────┴────┴────┘
sell=11 → buy=9 →
grid_index=3(持3份@8,9,10元):
┌────┬────┬────┬────┐
│ 11 │ 10 │ 9 │ 8 │ ...
└────┴────┴────┴────┘
↑ sell=9 buy=7 →
当前位置=3
成交后处理(onOrderTrade:
卖单成交 gridIdx < currentIdx → grid_index -= 1(上移,赚差价)
买单成交 gridIdx > currentIdx → grid_index += 1(下移,补仓)
然后 refreshGridOrder → 在新位置重新挂单
```
## 和之前的区别
| | 之前 | 之后 |
|---|---|---|
| 分支数 | 2 个(status=0 / status=1 | 1 个(统一网格逻辑) |
| 空仓第一笔 | INIT 单 @ grid[0]=11 | 普通买单 @ grid[1]=10 |
| grid[0]=11 的用途 | 建仓买入 | 永远只卖不买 |
| 状态字段 | status + grid_index | 仅 grid_index |
+259
View File
@@ -0,0 +1,259 @@
from kuanke.wizard import *
from jqdata import *
import pandas as pd
import numpy as np
# ==================== 初始化 ====================
def initialize(context):
set_params(context)
# 开启防未来函数
set_option('avoid_future_data', True)
# 用真实价格交易
set_option('use_real_price', True)
# 过滤order中低于error级别的日志
log.set_level('order', 'error')
log.set_level('system', 'error')
log.set_level('strategy', 'debug')
set_benchmark('000001.XSHG')
set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0002, close_commission=0.0002, min_commission=5), type='stock')
set_slippage(FixedSlippage(0.01))
run_daily(before_trading, '9:30')
# -------------------- 参数设置 --------------------
def set_params(context):
context.max_price = 6
context.min_price = 5.01
context.grid_base_min = 1 # 最小价格
context.grid_base_max = 5 # 建仓价格
context.grid_interval = 0.5 # 下跌n元加仓
context.profit_target = 0.5 # 上涨n元清仓
context.min_stocks = 10
context.max_stocks = 25
context.base_max_stocks = 25
context.max_layers = 7
context.base_position_pct = 0.15
context.max_position_pct = 0.15
context.target_usage = 0.98
context.reserve_ratio = 0.02
context.first_round_max = 10
context.add_batch_size = 3
context.add_cash_threshold = 0.4
g.stock_pool = []
g.grid_info = {}
g.monitoring_stocks = set()
g.first_round_done = False
# ==================== 盘前 ====================
def before_trading(context):
january_clear(context)
if context.current_dt.month == 1:
return
stock_pool = get_stock_pool(context)
g.stock_pool = stock_pool
g.monitoring_stocks.update([s for s in stock_pool if s not in g.grid_info])
g.first_round_done = len(g.grid_info) >= context.first_round_max
# -------------------- 股票池 --------------------
def get_stock_pool(context):
# 1. 全部 A 股(不含退市)
df_sec = get_all_securities(types=['stock'], date=context.previous_date)
codes = list(df_sec.index)
# 2. 过滤 ST、科创板、北交所
def is_valid(code):
name = df_sec.loc[code, 'display_name']
if 'ST' in name or '退' in name or 'st' in name:
return False
if code.startswith('688'): # 科创板
return False
if code.startswith('83') or code.startswith('87') or code.startswith('9'): # 北交所
return False
return True
codes = [c for c in codes if is_valid(c)]
if not codes:
return []
# 3. 过滤停牌 & 价格区间
try:
price_df = get_price(codes,
end_date=context.current_dt,
count=1,
fields=['pre_close'],
panel=False)
if price_df is None or price_df.empty:
return []
# 过滤价格区间
price_df = price_df[
(price_df['pre_close'].notna()) &
(price_df['pre_close'] >= context.min_price) &
(price_df['pre_close'] <= context.max_price)
]
valid_codes = price_df['code'].tolist()
except Exception as e:
log.error(f"获取价格数据失败: {e}")
return []
if not valid_codes:
return []
# 4. 过滤停牌(开盘价缺失)
try:
open_df = get_price(valid_codes,
end_date=context.current_dt,
count=1,
fields=['open'],
panel=False)
if open_df is None or open_df.empty:
return []
# 过滤掉开盘价为空的股票
open_df = open_df[open_df['open'].notna()]
final_codes = open_df['code'].tolist()
except Exception as e:
log.error(f"获取开盘价数据失败: {e}")
return []
return final_codes
# -------------------- 一月清仓 --------------------
def january_clear(context):
if context.current_dt.month == 1:
log.info("进入1月,执行年度清仓...")
for stock in list(context.portfolio.positions.keys()):
order_target(stock, 0)
if stock in g.grid_info:
del g.grid_info[stock]
g.monitoring_stocks.add(stock)
# ==================== 盘中 ====================
def handle_data(context, data):
if context.current_dt.month == 1:
return
manage_positions(context, data)
usage = (context.portfolio.total_value - context.portfolio.available_cash) / context.portfolio.total_value
dynamic_max = get_dynamic_max_stocks(context)
if len(g.grid_info) < dynamic_max and usage < context.target_usage:
try_build_new(context, data)
# -------------------- 动态上限 --------------------
def get_dynamic_max_stocks(context):
return context.max_stocks if g.first_round_done else context.first_round_max
# -------------------- 建仓 --------------------
def try_build_new(context, data):
position_pct = context.max_position_pct
dynamic_max = get_dynamic_max_stocks(context)
count = 0
for stock in list(g.monitoring_stocks):
if len(g.grid_info) >= dynamic_max or count >= 3:
break
price = data[stock].close
if context.grid_base_min <= price <= context.grid_base_max:
total_value = context.portfolio.total_value
stock_amount = total_value * position_pct
grid = GridInfo(price, stock_amount, context.max_layers, context.grid_interval, context.profit_target)
layer_amount = grid.get_layer_amount(0)
buy_amount = int(layer_amount / price / 100) * 100
if buy_amount > 0:
order(stock, buy_amount)
grid.add_position(price, buy_amount, 0)
g.grid_info[stock] = grid
g.monitoring_stocks.discard(stock)
count += 1
log.info(f"[建仓] {stock} 价格{price:.2f} 数量{buy_amount}")
# -------------------- 管理持仓 --------------------
def manage_positions(context, data):
for stock, grid in list(g.grid_info.items()):
price = data[stock].close
# 止盈
sellable = grid.get_sellable_positions(price)
if sellable:
for idx, pos in reversed(sellable):
order(stock, -pos['amount'])
grid.remove_position(idx)
profit = (price - pos['price']) * pos['amount']
log.info(f"[止盈] {stock} 盈利{profit:.2f}")
# 加仓
layer = grid.should_add_layer(price)
if layer is not None:
layer_amount = grid.get_layer_amount(layer)
buy_amount = int(layer_amount / price / 100) * 100
if buy_amount > 0:
order(stock, buy_amount)
grid.add_position(price, buy_amount, layer)
log.info(f"[加仓] {stock} 层级{layer} 数量{buy_amount}")
else:
log.info(f"[加仓失败] {stock} 层级{layer} 金额不足")
# 清仓
if len(grid.positions) == 0:
del g.grid_info[stock]
g.monitoring_stocks.add(stock)
log.info(f"[清仓] {stock}")
# ==================== 盘后 ====================
def after_trading_end(context):
log.info(f"持仓数:{len(g.grid_info)},监控数:{len(g.monitoring_stocks)}")
# ==================== 网格类 ====================
class GridInfo:
def __init__(self, base_price, total_amount, max_layers, interval, profit_target):
self.base_price = float(base_price)
self.total_amount = float(total_amount)
self.max_layers = int(max_layers)
self.interval = float(interval)
self.profit_target = float(profit_target)
self.layer_prices = {i: base_price - i * interval for i in range(self.max_layers)}
self.layer_weights = self._calc_weights()
self.positions = []
def _calc_weights(self):
weights = {i: 1.0 + 0.05 * i for i in range(self.max_layers)}
total = sum(list(weights.values()))
return {k: v / total for k, v in weights.items()}
def get_layer_amount(self, layer):
return self.total_amount * self.layer_weights[layer]
def add_position(self, price, amount, layer):
self.positions.append({'price': price, 'amount': amount, 'layer': layer})
def get_sellable_positions(self, current_price):
return [(i, p) for i, p in enumerate(self.positions) if current_price >= p['price'] + self.profit_target]
def remove_position(self, index):
return self.positions.pop(index)
def should_add_layer(self, current_price):
for layer in range(self.max_layers):
target = self.layer_prices[layer]
diff = abs(current_price - target)
# 获取该层级的所有持仓
layer_positions = [p for p in self.positions if p['layer'] == layer]
has_position = len(layer_positions) > 0
if diff <= 0.1 and not has_position:
return layer
return None
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File
-38
View File
@@ -1,38 +0,0 @@
date,cash,stock_value,total_value,positions,total_shares,month
2023-05-31,60000.0,0.0,60000.0,0,0,2023-05
2023-06-30,60000.0,0.0,60000.0,0,0,2023-06
2023-07-31,60000.0,0.0,60000.0,0,0,2023-07
2023-08-31,60000.0,0.0,60000.0,0,0,2023-08
2023-09-30,60000.0,0.0,60000.0,0,0,2023-09
2023-10-31,60000.0,0.0,60000.0,0,0,2023-10
2023-11-30,60000.0,0.0,60000.0,0,0,2023-11
2023-12-31,60000.0,0.0,60000.0,0,0,2023-12
2024-01-31,60000.0,0.0,60000.0,0,0,2024-01
2024-02-29,60000.0,0.0,60000.0,0,0,2024-02
2024-03-31,60000.0,0.0,60000.0,0,0,2024-03
2024-04-30,60000.0,0.0,60000.0,0,0,2024-04
2024-05-31,60000.0,0.0,60000.0,0,0,2024-05
2024-06-30,60000.0,0.0,60000.0,0,0,2024-06
2024-07-31,60000.0,0.0,60000.0,0,0,2024-07
2024-08-31,60000.0,0.0,60000.0,0,0,2024-08
2024-09-30,60000.0,0.0,60000.0,0,0,2024-09
2024-10-31,60000.0,0.0,60000.0,0,0,2024-10
2024-11-30,60000.0,0.0,60000.0,0,0,2024-11
2024-12-31,60000.0,0.0,60000.0,0,0,2024-12
2025-01-31,60000.0,0.0,60000.0,0,0,2025-01
2025-02-28,60000.0,0.0,60000.0,0,0,2025-02
2025-03-31,60000.0,0.0,60000.0,0,0,2025-03
2025-04-30,60000.0,0.0,60000.0,0,0,2025-04
2025-05-31,60000.0,0.0,60000.0,0,0,2025-05
2025-06-30,60000.0,0.0,60000.0,0,0,2025-06
2025-07-31,60000.0,0.0,60000.0,0,0,2025-07
2025-08-31,60000.0,0.0,60000.0,0,0,2025-08
2025-09-30,60000.0,0.0,60000.0,0,0,2025-09
2025-10-31,60000.0,0.0,60000.0,0,0,2025-10
2025-11-30,60000.0,0.0,60000.0,0,0,2025-11
2025-12-31,60000.0,0.0,60000.0,0,0,2025-12
2026-01-31,60000.0,0.0,60000.0,0,0,2026-01
2026-02-28,60000.0,0.0,60000.0,0,0,2026-02
2026-03-31,60000.0,0.0,60000.0,0,0,2026-03
2026-04-30,60000.0,0.0,60000.0,0,0,2026-04
2026-05-27,60000.0,0.0,60000.0,0,0,2026-05
1 date cash stock_value total_value positions total_shares month
2 2023-05-31 60000.0 0.0 60000.0 0 0 2023-05
3 2023-06-30 60000.0 0.0 60000.0 0 0 2023-06
4 2023-07-31 60000.0 0.0 60000.0 0 0 2023-07
5 2023-08-31 60000.0 0.0 60000.0 0 0 2023-08
6 2023-09-30 60000.0 0.0 60000.0 0 0 2023-09
7 2023-10-31 60000.0 0.0 60000.0 0 0 2023-10
8 2023-11-30 60000.0 0.0 60000.0 0 0 2023-11
9 2023-12-31 60000.0 0.0 60000.0 0 0 2023-12
10 2024-01-31 60000.0 0.0 60000.0 0 0 2024-01
11 2024-02-29 60000.0 0.0 60000.0 0 0 2024-02
12 2024-03-31 60000.0 0.0 60000.0 0 0 2024-03
13 2024-04-30 60000.0 0.0 60000.0 0 0 2024-04
14 2024-05-31 60000.0 0.0 60000.0 0 0 2024-05
15 2024-06-30 60000.0 0.0 60000.0 0 0 2024-06
16 2024-07-31 60000.0 0.0 60000.0 0 0 2024-07
17 2024-08-31 60000.0 0.0 60000.0 0 0 2024-08
18 2024-09-30 60000.0 0.0 60000.0 0 0 2024-09
19 2024-10-31 60000.0 0.0 60000.0 0 0 2024-10
20 2024-11-30 60000.0 0.0 60000.0 0 0 2024-11
21 2024-12-31 60000.0 0.0 60000.0 0 0 2024-12
22 2025-01-31 60000.0 0.0 60000.0 0 0 2025-01
23 2025-02-28 60000.0 0.0 60000.0 0 0 2025-02
24 2025-03-31 60000.0 0.0 60000.0 0 0 2025-03
25 2025-04-30 60000.0 0.0 60000.0 0 0 2025-04
26 2025-05-31 60000.0 0.0 60000.0 0 0 2025-05
27 2025-06-30 60000.0 0.0 60000.0 0 0 2025-06
28 2025-07-31 60000.0 0.0 60000.0 0 0 2025-07
29 2025-08-31 60000.0 0.0 60000.0 0 0 2025-08
30 2025-09-30 60000.0 0.0 60000.0 0 0 2025-09
31 2025-10-31 60000.0 0.0 60000.0 0 0 2025-10
32 2025-11-30 60000.0 0.0 60000.0 0 0 2025-11
33 2025-12-31 60000.0 0.0 60000.0 0 0 2025-12
34 2026-01-31 60000.0 0.0 60000.0 0 0 2026-01
35 2026-02-28 60000.0 0.0 60000.0 0 0 2026-02
36 2026-03-31 60000.0 0.0 60000.0 0 0 2026-03
37 2026-04-30 60000.0 0.0 60000.0 0 0 2026-04
38 2026-05-27 60000.0 0.0 60000.0 0 0 2026-05
-1
View File
@@ -1 +0,0 @@
-158
View File
@@ -1,158 +0,0 @@
date,cash,stock_value,total_value,positions,year
2023-04-28,60000.0,0.0,60000.0,10,2023
2023-05-05,58400.0,1618.0,60018.0,10,2023
2023-05-12,55400.0,4570.0,59970.0,10,2023
2023-05-19,52200.0,7428.0,59628.0,10,2023
2023-05-26,45200.0,14102.0,59302.0,10,2023
2023-06-02,49800.0,11204.0,61004.0,10,2023
2023-06-09,55600.0,5996.0,61596.0,10,2023
2023-06-16,55600.0,5988.0,61588.0,10,2023
2023-06-23,53800.0,7522.0,61322.0,10,2023
2023-06-30,49200.0,11602.0,60802.0,10,2023
2023-07-07,51200.0,9542.0,60742.0,10,2023
2023-07-14,48400.0,12576.0,60976.0,10,2023
2023-07-21,50200.0,10406.0,60606.0,10,2023
2023-07-28,50200.0,10268.0,60468.0,10,2023
2023-08-04,50200.0,10622.0,60822.0,10,2023
2023-08-11,50200.0,10078.0,60278.0,10,2023
2023-08-18,47400.0,12614.0,60014.0,10,2023
2023-08-25,46200.0,13572.0,59772.0,10,2023
2023-09-01,46200.0,14042.0,60242.0,10,2023
2023-09-08,46800.0,12942.0,59742.0,10,2023
2023-09-15,45200.0,14398.0,59598.0,10,2023
2023-09-22,42600.0,17276.0,59876.0,10,2023
2023-09-29,42600.0,17430.0,60030.0,10,2023
2023-10-06,42600.0,17430.0,60030.0,10,2023
2023-10-13,42600.0,16750.0,59350.0,10,2023
2023-10-20,41000.0,17366.0,58366.0,10,2023
2023-10-27,39600.0,18870.0,58470.0,10,2023
2023-11-03,42600.0,17234.0,59834.0,10,2023
2023-11-10,45400.0,15998.0,61398.0,10,2023
2023-11-17,45400.0,16098.0,61498.0,10,2023
2023-11-24,47000.0,14652.0,61652.0,10,2023
2023-12-01,45800.0,16434.0,62234.0,10,2023
2023-12-08,48800.0,13898.0,62698.0,10,2023
2023-12-15,50800.0,12312.0,63112.0,10,2023
2023-12-22,45400.0,15928.0,61328.0,10,2023
2023-12-29,41000.0,20256.0,61256.0,10,2023
2024-01-05,42800.0,17966.0,60766.0,10,2024
2024-01-12,40000.0,20254.0,60254.0,10,2024
2024-01-19,37600.0,22196.0,59796.0,10,2024
2024-01-26,35000.0,25716.0,60716.0,10,2024
2024-02-02,25600.0,31044.0,56644.0,10,2024
2024-02-09,19400.0,36784.0,56184.0,10,2024
2024-02-16,19400.0,36784.0,56184.0,10,2024
2024-02-23,29400.0,33418.0,62818.0,10,2024
2024-03-01,35200.0,28470.0,63670.0,10,2024
2024-03-08,36400.0,26802.0,63202.0,10,2024
2024-03-15,40800.0,23672.0,64472.0,10,2024
2024-03-22,48600.0,19494.0,68094.0,10,2024
2024-03-29,48800.0,17936.0,66736.0,10,2024
2024-04-05,44800.0,21374.0,66174.0,10,2024
2024-04-12,42200.0,22450.0,64650.0,10,2024
2024-04-19,35000.0,27966.0,62966.0,10,2024
2024-04-26,36600.0,28114.0,64714.0,10,2024
2024-05-03,39000.0,27576.0,66576.0,10,2024
2024-05-10,42200.0,24400.0,66600.0,10,2024
2024-05-17,39800.0,26702.0,66502.0,10,2024
2024-05-24,38200.0,27704.0,65904.0,10,2024
2024-05-31,38400.0,26486.0,64886.0,10,2024
2024-06-07,32200.0,30776.0,62976.0,10,2024
2024-06-14,32200.0,31802.0,64002.0,10,2024
2024-06-21,31000.0,31084.0,62084.0,10,2024
2024-06-28,29000.0,32180.0,61180.0,10,2024
2024-07-05,29000.0,32292.0,61292.0,10,2024
2024-07-12,27200.0,33578.0,60778.0,10,2024
2024-07-19,27200.0,33172.0,60372.0,10,2024
2024-07-26,24200.0,35970.0,60170.0,10,2024
2024-08-02,24200.0,36992.0,61192.0,10,2024
2024-08-09,26000.0,34954.0,60954.0,10,2024
2024-08-16,26000.0,34780.0,60780.0,10,2024
2024-08-23,24400.0,35044.0,59444.0,10,2024
2024-08-30,23400.0,37774.0,61174.0,10,2024
2024-09-06,23400.0,36744.0,60144.0,10,2024
2024-09-13,23400.0,36430.0,59830.0,10,2024
2024-09-20,25200.0,35478.0,60678.0,10,2024
2024-09-27,30200.0,35280.0,65480.0,10,2024
2024-10-04,37000.0,32120.0,69120.0,10,2024
2024-10-11,40600.0,26824.0,67424.0,10,2024
2024-10-18,40600.0,28430.0,69030.0,10,2024
2024-10-25,43600.0,27342.0,70942.0,10,2024
2024-11-01,49600.0,23190.0,72790.0,10,2024
2024-11-08,53200.0,21184.0,74384.0,10,2024
2024-11-15,56000.0,19820.0,75820.0,10,2024
2024-11-22,59400.0,16790.0,76190.0,10,2024
2024-11-29,63600.0,14610.0,78210.0,10,2024
2024-12-06,63800.0,15148.0,78948.0,10,2024
2024-12-13,70600.0,9146.0,79746.0,10,2024
2024-12-20,67800.0,11830.0,79630.0,10,2024
2024-12-27,63400.0,14588.0,77988.0,10,2024
2025-01-03,55800.0,20000.0,75800.0,10,2025
2025-01-10,54400.0,21344.0,75744.0,10,2025
2025-01-17,57400.0,20314.0,77714.0,10,2025
2025-01-24,57400.0,20416.0,77816.0,10,2025
2025-01-31,57400.0,20178.0,77578.0,10,2025
2025-02-07,57400.0,21214.0,78614.0,10,2025
2025-02-14,63600.0,16530.0,80130.0,10,2025
2025-02-21,65000.0,14804.0,79804.0,10,2025
2025-02-28,62400.0,16588.0,78988.0,10,2025
2025-03-07,62400.0,16784.0,79184.0,10,2025
2025-03-14,62400.0,17590.0,79990.0,10,2025
2025-03-21,62400.0,16742.0,79142.0,10,2025
2025-03-28,62400.0,16480.0,78880.0,10,2025
2025-04-04,59800.0,18538.0,78338.0,10,2025
2025-04-11,55200.0,22474.0,77674.0,10,2025
2025-04-18,58600.0,19992.0,78592.0,10,2025
2025-04-25,58800.0,20036.0,78836.0,10,2025
2025-05-02,57200.0,21798.0,78998.0,10,2025
2025-05-09,59000.0,20540.0,79540.0,10,2025
2025-05-16,59000.0,20316.0,79316.0,10,2025
2025-05-23,60800.0,18556.0,79356.0,10,2025
2025-05-30,60800.0,18834.0,79634.0,10,2025
2025-06-06,60800.0,19254.0,80054.0,10,2025
2025-06-13,62000.0,18528.0,80528.0,10,2025
2025-06-20,63200.0,16976.0,80176.0,10,2025
2025-06-27,63200.0,17596.0,80796.0,10,2025
2025-07-04,63200.0,17758.0,80958.0,10,2025
2025-07-11,63200.0,18198.0,81398.0,10,2025
2025-07-18,64800.0,16488.0,81288.0,10,2025
2025-07-25,64800.0,16934.0,81734.0,10,2025
2025-08-01,66800.0,15168.0,81968.0,10,2025
2025-08-08,66800.0,14908.0,81708.0,10,2025
2025-08-15,66800.0,14766.0,81566.0,10,2025
2025-08-22,71200.0,11406.0,82606.0,10,2025
2025-08-29,71200.0,11004.0,82204.0,10,2025
2025-09-05,68400.0,13804.0,82204.0,10,2025
2025-09-12,70200.0,12362.0,82562.0,10,2025
2025-09-19,68400.0,13864.0,82264.0,10,2025
2025-09-26,67400.0,14434.0,81834.0,10,2025
2025-10-03,67400.0,14374.0,81774.0,10,2025
2025-10-10,65800.0,15922.0,81722.0,10,2025
2025-10-17,65800.0,15226.0,81026.0,10,2025
2025-10-24,67800.0,13844.0,81644.0,10,2025
2025-10-31,67800.0,13968.0,81768.0,10,2025
2025-11-07,67800.0,14122.0,81922.0,10,2025
2025-11-14,67800.0,14130.0,81930.0,10,2025
2025-11-21,66000.0,15954.0,81954.0,10,2025
2025-11-28,66000.0,16312.0,82312.0,10,2025
2025-12-05,66000.0,15682.0,81682.0,10,2025
2025-12-12,66000.0,15122.0,81122.0,10,2025
2025-12-19,66000.0,15268.0,81268.0,10,2025
2025-12-26,66000.0,15350.0,81350.0,10,2025
2026-01-02,67400.0,14250.0,81650.0,10,2026
2026-01-09,69200.0,13998.0,83198.0,10,2026
2026-01-16,73400.0,10432.0,83832.0,10,2026
2026-01-23,73400.0,10660.0,84060.0,10,2026
2026-01-30,72400.0,11408.0,83808.0,10,2026
2026-02-06,71000.0,12648.0,83648.0,10,2026
2026-02-13,71000.0,12800.0,83800.0,10,2026
2026-02-20,71000.0,12800.0,83800.0,10,2026
2026-02-27,69800.0,13764.0,83564.0,10,2026
2026-03-06,68200.0,14498.0,82698.0,10,2026
2026-03-13,68200.0,14434.0,82634.0,10,2026
2026-03-20,66600.0,15290.0,81890.0,10,2026
2026-03-27,63000.0,18856.0,81856.0,10,2026
2026-04-03,62200.0,18540.0,80740.0,10,2026
2026-04-10,60600.0,21340.0,81940.0,10,2026
2026-04-17,60600.0,21260.0,81860.0,10,2026
2026-04-24,62200.0,19876.0,82076.0,10,2026
1 date cash stock_value total_value positions year
2 2023-04-28 60000.0 0.0 60000.0 10 2023
3 2023-05-05 58400.0 1618.0 60018.0 10 2023
4 2023-05-12 55400.0 4570.0 59970.0 10 2023
5 2023-05-19 52200.0 7428.0 59628.0 10 2023
6 2023-05-26 45200.0 14102.0 59302.0 10 2023
7 2023-06-02 49800.0 11204.0 61004.0 10 2023
8 2023-06-09 55600.0 5996.0 61596.0 10 2023
9 2023-06-16 55600.0 5988.0 61588.0 10 2023
10 2023-06-23 53800.0 7522.0 61322.0 10 2023
11 2023-06-30 49200.0 11602.0 60802.0 10 2023
12 2023-07-07 51200.0 9542.0 60742.0 10 2023
13 2023-07-14 48400.0 12576.0 60976.0 10 2023
14 2023-07-21 50200.0 10406.0 60606.0 10 2023
15 2023-07-28 50200.0 10268.0 60468.0 10 2023
16 2023-08-04 50200.0 10622.0 60822.0 10 2023
17 2023-08-11 50200.0 10078.0 60278.0 10 2023
18 2023-08-18 47400.0 12614.0 60014.0 10 2023
19 2023-08-25 46200.0 13572.0 59772.0 10 2023
20 2023-09-01 46200.0 14042.0 60242.0 10 2023
21 2023-09-08 46800.0 12942.0 59742.0 10 2023
22 2023-09-15 45200.0 14398.0 59598.0 10 2023
23 2023-09-22 42600.0 17276.0 59876.0 10 2023
24 2023-09-29 42600.0 17430.0 60030.0 10 2023
25 2023-10-06 42600.0 17430.0 60030.0 10 2023
26 2023-10-13 42600.0 16750.0 59350.0 10 2023
27 2023-10-20 41000.0 17366.0 58366.0 10 2023
28 2023-10-27 39600.0 18870.0 58470.0 10 2023
29 2023-11-03 42600.0 17234.0 59834.0 10 2023
30 2023-11-10 45400.0 15998.0 61398.0 10 2023
31 2023-11-17 45400.0 16098.0 61498.0 10 2023
32 2023-11-24 47000.0 14652.0 61652.0 10 2023
33 2023-12-01 45800.0 16434.0 62234.0 10 2023
34 2023-12-08 48800.0 13898.0 62698.0 10 2023
35 2023-12-15 50800.0 12312.0 63112.0 10 2023
36 2023-12-22 45400.0 15928.0 61328.0 10 2023
37 2023-12-29 41000.0 20256.0 61256.0 10 2023
38 2024-01-05 42800.0 17966.0 60766.0 10 2024
39 2024-01-12 40000.0 20254.0 60254.0 10 2024
40 2024-01-19 37600.0 22196.0 59796.0 10 2024
41 2024-01-26 35000.0 25716.0 60716.0 10 2024
42 2024-02-02 25600.0 31044.0 56644.0 10 2024
43 2024-02-09 19400.0 36784.0 56184.0 10 2024
44 2024-02-16 19400.0 36784.0 56184.0 10 2024
45 2024-02-23 29400.0 33418.0 62818.0 10 2024
46 2024-03-01 35200.0 28470.0 63670.0 10 2024
47 2024-03-08 36400.0 26802.0 63202.0 10 2024
48 2024-03-15 40800.0 23672.0 64472.0 10 2024
49 2024-03-22 48600.0 19494.0 68094.0 10 2024
50 2024-03-29 48800.0 17936.0 66736.0 10 2024
51 2024-04-05 44800.0 21374.0 66174.0 10 2024
52 2024-04-12 42200.0 22450.0 64650.0 10 2024
53 2024-04-19 35000.0 27966.0 62966.0 10 2024
54 2024-04-26 36600.0 28114.0 64714.0 10 2024
55 2024-05-03 39000.0 27576.0 66576.0 10 2024
56 2024-05-10 42200.0 24400.0 66600.0 10 2024
57 2024-05-17 39800.0 26702.0 66502.0 10 2024
58 2024-05-24 38200.0 27704.0 65904.0 10 2024
59 2024-05-31 38400.0 26486.0 64886.0 10 2024
60 2024-06-07 32200.0 30776.0 62976.0 10 2024
61 2024-06-14 32200.0 31802.0 64002.0 10 2024
62 2024-06-21 31000.0 31084.0 62084.0 10 2024
63 2024-06-28 29000.0 32180.0 61180.0 10 2024
64 2024-07-05 29000.0 32292.0 61292.0 10 2024
65 2024-07-12 27200.0 33578.0 60778.0 10 2024
66 2024-07-19 27200.0 33172.0 60372.0 10 2024
67 2024-07-26 24200.0 35970.0 60170.0 10 2024
68 2024-08-02 24200.0 36992.0 61192.0 10 2024
69 2024-08-09 26000.0 34954.0 60954.0 10 2024
70 2024-08-16 26000.0 34780.0 60780.0 10 2024
71 2024-08-23 24400.0 35044.0 59444.0 10 2024
72 2024-08-30 23400.0 37774.0 61174.0 10 2024
73 2024-09-06 23400.0 36744.0 60144.0 10 2024
74 2024-09-13 23400.0 36430.0 59830.0 10 2024
75 2024-09-20 25200.0 35478.0 60678.0 10 2024
76 2024-09-27 30200.0 35280.0 65480.0 10 2024
77 2024-10-04 37000.0 32120.0 69120.0 10 2024
78 2024-10-11 40600.0 26824.0 67424.0 10 2024
79 2024-10-18 40600.0 28430.0 69030.0 10 2024
80 2024-10-25 43600.0 27342.0 70942.0 10 2024
81 2024-11-01 49600.0 23190.0 72790.0 10 2024
82 2024-11-08 53200.0 21184.0 74384.0 10 2024
83 2024-11-15 56000.0 19820.0 75820.0 10 2024
84 2024-11-22 59400.0 16790.0 76190.0 10 2024
85 2024-11-29 63600.0 14610.0 78210.0 10 2024
86 2024-12-06 63800.0 15148.0 78948.0 10 2024
87 2024-12-13 70600.0 9146.0 79746.0 10 2024
88 2024-12-20 67800.0 11830.0 79630.0 10 2024
89 2024-12-27 63400.0 14588.0 77988.0 10 2024
90 2025-01-03 55800.0 20000.0 75800.0 10 2025
91 2025-01-10 54400.0 21344.0 75744.0 10 2025
92 2025-01-17 57400.0 20314.0 77714.0 10 2025
93 2025-01-24 57400.0 20416.0 77816.0 10 2025
94 2025-01-31 57400.0 20178.0 77578.0 10 2025
95 2025-02-07 57400.0 21214.0 78614.0 10 2025
96 2025-02-14 63600.0 16530.0 80130.0 10 2025
97 2025-02-21 65000.0 14804.0 79804.0 10 2025
98 2025-02-28 62400.0 16588.0 78988.0 10 2025
99 2025-03-07 62400.0 16784.0 79184.0 10 2025
100 2025-03-14 62400.0 17590.0 79990.0 10 2025
101 2025-03-21 62400.0 16742.0 79142.0 10 2025
102 2025-03-28 62400.0 16480.0 78880.0 10 2025
103 2025-04-04 59800.0 18538.0 78338.0 10 2025
104 2025-04-11 55200.0 22474.0 77674.0 10 2025
105 2025-04-18 58600.0 19992.0 78592.0 10 2025
106 2025-04-25 58800.0 20036.0 78836.0 10 2025
107 2025-05-02 57200.0 21798.0 78998.0 10 2025
108 2025-05-09 59000.0 20540.0 79540.0 10 2025
109 2025-05-16 59000.0 20316.0 79316.0 10 2025
110 2025-05-23 60800.0 18556.0 79356.0 10 2025
111 2025-05-30 60800.0 18834.0 79634.0 10 2025
112 2025-06-06 60800.0 19254.0 80054.0 10 2025
113 2025-06-13 62000.0 18528.0 80528.0 10 2025
114 2025-06-20 63200.0 16976.0 80176.0 10 2025
115 2025-06-27 63200.0 17596.0 80796.0 10 2025
116 2025-07-04 63200.0 17758.0 80958.0 10 2025
117 2025-07-11 63200.0 18198.0 81398.0 10 2025
118 2025-07-18 64800.0 16488.0 81288.0 10 2025
119 2025-07-25 64800.0 16934.0 81734.0 10 2025
120 2025-08-01 66800.0 15168.0 81968.0 10 2025
121 2025-08-08 66800.0 14908.0 81708.0 10 2025
122 2025-08-15 66800.0 14766.0 81566.0 10 2025
123 2025-08-22 71200.0 11406.0 82606.0 10 2025
124 2025-08-29 71200.0 11004.0 82204.0 10 2025
125 2025-09-05 68400.0 13804.0 82204.0 10 2025
126 2025-09-12 70200.0 12362.0 82562.0 10 2025
127 2025-09-19 68400.0 13864.0 82264.0 10 2025
128 2025-09-26 67400.0 14434.0 81834.0 10 2025
129 2025-10-03 67400.0 14374.0 81774.0 10 2025
130 2025-10-10 65800.0 15922.0 81722.0 10 2025
131 2025-10-17 65800.0 15226.0 81026.0 10 2025
132 2025-10-24 67800.0 13844.0 81644.0 10 2025
133 2025-10-31 67800.0 13968.0 81768.0 10 2025
134 2025-11-07 67800.0 14122.0 81922.0 10 2025
135 2025-11-14 67800.0 14130.0 81930.0 10 2025
136 2025-11-21 66000.0 15954.0 81954.0 10 2025
137 2025-11-28 66000.0 16312.0 82312.0 10 2025
138 2025-12-05 66000.0 15682.0 81682.0 10 2025
139 2025-12-12 66000.0 15122.0 81122.0 10 2025
140 2025-12-19 66000.0 15268.0 81268.0 10 2025
141 2025-12-26 66000.0 15350.0 81350.0 10 2025
142 2026-01-02 67400.0 14250.0 81650.0 10 2026
143 2026-01-09 69200.0 13998.0 83198.0 10 2026
144 2026-01-16 73400.0 10432.0 83832.0 10 2026
145 2026-01-23 73400.0 10660.0 84060.0 10 2026
146 2026-01-30 72400.0 11408.0 83808.0 10 2026
147 2026-02-06 71000.0 12648.0 83648.0 10 2026
148 2026-02-13 71000.0 12800.0 83800.0 10 2026
149 2026-02-20 71000.0 12800.0 83800.0 10 2026
150 2026-02-27 69800.0 13764.0 83564.0 10 2026
151 2026-03-06 68200.0 14498.0 82698.0 10 2026
152 2026-03-13 68200.0 14434.0 82634.0 10 2026
153 2026-03-20 66600.0 15290.0 81890.0 10 2026
154 2026-03-27 63000.0 18856.0 81856.0 10 2026
155 2026-04-03 62200.0 18540.0 80740.0 10 2026
156 2026-04-10 60600.0 21340.0 81940.0 10 2026
157 2026-04-17 60600.0 21260.0 81860.0 10 2026
158 2026-04-24 62200.0 19876.0 82076.0 10 2026
-727
View File
@@ -1,727 +0,0 @@
date,cash,holding_market_value,total_asset
2023-05-04,28289.86,33283.08,61572.94
2023-05-05,29353.86,32311.96,61665.82
2023-05-08,29553.86,32215.16,61769.020000000004
2023-05-09,27953.86,32879.84,60833.7
2023-05-10,26153.86,35374.479999999996,61528.34
2023-05-11,29753.86,33225.380000000005,62979.240000000005
2023-05-12,24353.86,37200.04,61553.9
2023-05-15,25667.86,35345.6,61013.46
2023-05-16,20867.86,38643.44,59511.3
2023-05-17,17667.86,42281.4,59949.26
2023-05-18,17667.86,43420.1,61087.96
2023-05-19,19999.86,40128.84,60128.7
2023-05-22,19136.34,40407.96,59544.3
2023-05-23,19136.34,39661.66,58798.0
2023-05-24,19136.34,39738.92,58875.259999999995
2023-05-25,17736.34,40531.479999999996,58267.81999999999
2023-05-26,17736.34,41098.4,58834.740000000005
2023-05-29,14136.34,44736.28,58872.619999999995
2023-05-30,14336.34,45939.42,60275.759999999995
2023-05-31,14336.34,45642.6,59978.94
2023-06-01,20433.92,41484.86,61918.78
2023-06-02,22233.92,40253.44,62487.36
2023-06-05,20683.899999999998,42477.36,63161.259999999995
2023-06-06,24283.899999999998,37810.38,62094.28
2023-06-07,22683.899999999998,40119.560000000005,62803.46000000001
2023-06-08,21083.899999999998,40887.58,61971.479999999996
2023-06-09,23083.899999999998,39432.4,62516.3
2023-06-12,23083.899999999998,39468.780000000006,62552.68000000001
2023-06-13,23083.899999999998,40270.58,63354.479999999996
2023-06-14,24883.899999999998,38792.08,63675.979999999996
2023-06-15,26883.899999999998,36531.32000000001,63415.22
2023-06-16,27171.899999999998,36662.04,63833.94
2023-06-19,30577.359999999997,33518.72,64096.08
2023-06-20,28977.359999999997,35500.700000000004,64478.06
2023-06-21,27177.359999999997,35306.82,62484.17999999999
2023-06-26,25315.46,35218.7,60534.159999999996
2023-06-27,20515.46,40464.58,60980.04
2023-06-28,20515.46,39961.2,60476.659999999996
2023-06-29,20515.46,40167.38,60682.84
2023-06-30,20515.46,40323.66,60839.12
2023-07-03,28555.76,32259.06,60814.82
2023-07-04,28555.76,32140.9,60696.66
2023-07-05,28555.76,31760.359999999997,60316.119999999995
2023-07-06,28555.76,32029.4,60585.16
2023-07-07,27155.76,33151.0,60306.759999999995
2023-07-10,30335.76,30607.5,60943.259999999995
2023-07-11,30335.76,29987.239999999998,60323.0
2023-07-12,27135.76,32715.800000000003,59851.56
2023-07-13,28735.76,32062.980000000003,60798.740000000005
2023-07-14,29091.76,31705.800000000003,60797.56
2023-07-17,27259.08,33179.8,60438.880000000005
2023-07-18,27259.08,33110.299999999996,60369.38
2023-07-19,27259.08,33071.82,60330.9
2023-07-20,27259.08,32716.16,59975.240000000005
2023-07-21,27608.82,32509.1,60117.92
2023-07-24,29909.840000000004,30081.24,59991.08
2023-07-25,29909.840000000004,30276.039999999997,60185.880000000005
2023-07-26,31797.840000000004,27988.0,59785.840000000004
2023-07-27,31797.840000000004,27916.859999999997,59714.7
2023-07-28,30397.840000000004,29960.780000000002,60358.62000000001
2023-07-31,31034.120000000003,29691.679999999997,60725.8
2023-08-01,31326.78,29226.62,60553.399999999994
2023-08-02,33126.78,27590.860000000004,60717.64
2023-08-03,31326.78,29124.66,60451.44
2023-08-04,31326.78,29340.219999999998,60667.0
2023-08-07,28402.46,32404.219999999998,60806.67999999999
2023-08-08,28402.46,32277.72,60680.18
2023-08-09,28602.46,31887.659999999996,60490.119999999995
2023-08-10,28602.46,32160.539999999997,60763.0
2023-08-11,28602.46,31713.8,60316.259999999995
2023-08-14,30216.02,30586.119999999995,60802.14
2023-08-15,30216.02,30597.48,60813.5
2023-08-16,32216.02,28753.199999999997,60969.22
2023-08-17,30416.02,31062.44,61478.46
2023-08-18,33136.700000000004,28257.34,61394.04000000001
2023-08-21,31336.700000000004,30055.36,61392.060000000005
2023-08-22,29536.700000000004,32326.899999999998,61863.600000000006
2023-08-23,29536.700000000004,31548.5,61085.200000000004
2023-08-24,29536.700000000004,31743.98,61280.68000000001
2023-08-25,26136.700000000004,34300.62,60437.32000000001
2023-08-28,29708.780000000006,31279.519999999997,60988.3
2023-08-29,30196.780000000006,32665.699999999997,62862.48
2023-08-30,32196.780000000006,31247.879999999997,63444.66
2023-08-31,34196.780000000006,29057.66,63254.44
2023-09-01,33946.780000000006,29311.820000000003,63258.600000000006
2023-09-04,34135.520000000004,29560.14,63695.66
2023-09-05,34509.780000000006,29185.22,63695.00000000001
2023-09-06,32709.780000000006,30561.239999999998,63271.020000000004
2023-09-07,33082.920000000006,30092.64,63175.560000000005
2023-09-08,29682.920000000006,33243.98,62926.90000000001
2023-09-11,28129.100000000006,35377.44,63506.54000000001
2023-09-12,30129.100000000006,33934.32,64063.420000000006
2023-09-13,31929.100000000006,32089.72,64018.82000000001
2023-09-14,31929.100000000006,31721.36,63650.46000000001
2023-09-15,33929.100000000006,29926.239999999998,63855.340000000004
2023-09-18,35984.3,28165.4,64149.700000000004
2023-09-19,36370.3,27593.84,63964.14
2023-09-20,36917.340000000004,27446.239999999998,64363.58
2023-09-21,35117.340000000004,29112.16,64229.5
2023-09-22,35117.340000000004,29932.44,65049.78
2023-09-25,35673.340000000004,30188.22,65861.56
2023-09-26,33873.340000000004,31843.26,65716.6
2023-09-27,33873.340000000004,32595.0,66468.34
2023-09-28,39873.340000000004,27709.4,67582.74
2023-10-09,41247.340000000004,26008.479999999996,67255.82
2023-10-10,43605.08,24212.319999999996,67817.4
2023-10-11,43805.08,23993.4,67798.48000000001
2023-10-12,44083.08,23915.480000000003,67998.56
2023-10-13,44083.08,23683.52,67766.6
2023-10-16,43689.08,23793.599999999995,67482.68
2023-10-17,42089.08,25216.94,67306.02
2023-10-18,38489.08,28344.4,66833.48000000001
2023-10-19,36689.08,29789.940000000002,66479.02
2023-10-20,33289.08,32655.699999999997,65944.78
2023-10-23,31442.520000000004,33523.66,64966.18000000001
2023-10-24,29642.520000000004,36040.58,65683.1
2023-10-25,33642.520000000004,33017.58,66660.1
2023-10-26,34288.520000000004,33149.32,67437.84
2023-10-27,36088.520000000004,31940.920000000002,68029.44
2023-10-30,37285.62,32011.34,69296.96
2023-10-31,39938.9,29558.3,69497.2
2023-11-01,38429.700000000004,30972.719999999994,69402.42
2023-11-02,40429.700000000004,28891.5,69321.20000000001
2023-11-03,40429.700000000004,29377.8,69807.5
2023-11-06,41176.26,29848.48,71024.74
2023-11-07,45432.26,26553.34,71985.6
2023-11-08,45782.26,26698.7,72480.96
2023-11-09,48079.740000000005,24910.94,72990.68000000001
2023-11-10,46479.740000000005,25900.26,72380.0
2023-11-13,48501.3,24292.92,72794.22
2023-11-14,46701.3,26683.64,73384.94
2023-11-15,46957.340000000004,26097.679999999997,73055.02
2023-11-16,47157.340000000004,26279.88,73437.22
2023-11-17,49341.340000000004,24523.559999999998,73864.9
2023-11-20,51584.740000000005,23124.5,74709.24
2023-11-21,51940.740000000005,22778.399999999998,74719.14
2023-11-22,49054.740000000005,26431.32,75486.06
2023-11-23,50182.740000000005,25830.2,76012.94
2023-11-24,49700.740000000005,27047.760000000002,76748.5
2023-11-27,52958.340000000004,24999.68,77958.02
2023-11-28,46244.340000000004,30826.699999999997,77071.04000000001
2023-11-29,41844.340000000004,33468.94,75313.28
2023-11-30,37244.340000000004,37343.26,74587.6
2023-12-01,33892.340000000004,39456.38,73348.72
2023-12-04,29451.340000000004,43211.36,72662.70000000001
2023-12-05,36811.340000000004,40589.1,77400.44
2023-12-06,43811.340000000004,34448.659999999996,78260.0
2023-12-07,49721.340000000004,30807.46,80528.8
2023-12-08,50831.340000000004,30194.3,81025.64
2023-12-11,41721.94,37976.44,79698.38
2023-12-12,41967.94,38844.28,80812.22
2023-12-13,41967.94,40205.56,82173.5
2023-12-14,48057.94,35801.66,83859.6
2023-12-15,52387.94,31884.0,84271.94
2023-12-18,54779.94,31022.0,85801.94
2023-12-19,56379.94,28964.0,85343.94
2023-12-20,58379.94,28260.0,86639.94
2023-12-21,60909.94,24672.0,85581.94
2023-12-22,52909.94,32946.0,85855.94
2023-12-25,54909.94,32782.0,87691.94
2023-12-26,54909.94,33254.0,88163.94
2023-12-27,57197.94,31222.0,88419.94
2023-12-28,55761.94,33084.0,88845.94
2023-12-29,56387.94,33956.0,90343.94
2024-01-02,62813.94,29938.0,92751.94
2024-01-03,65205.94,27608.0,92813.94
2024-01-04,63405.94,29272.0,92677.94
2024-01-05,56405.94,34602.0,91007.94
2024-01-08,58405.94,31830.0,90235.94
2024-01-09,48405.94,40204.0,88609.94
2024-01-10,43605.94,45832.0,89437.94
2024-01-11,45405.94,44096.0,89501.94
2024-01-12,42405.94,42438.0,84843.94
2024-01-15,35605.94,46922.0,82527.94
2024-01-16,34005.94,52684.0,86689.94
2024-01-17,37205.94,49538.0,86743.94
2024-01-18,39005.94,49904.0,88909.94
2024-01-19,44405.94,46458.0,90863.94
2024-01-22,45355.94,42936.0,88291.94
2024-01-23,45355.94,44114.0,89469.94
2024-01-24,47895.94,42334.0,90229.94
2024-01-25,46695.94,42518.0,89213.94
2024-01-26,43543.94,44782.0,88325.94
2024-01-29,39113.94,44472.0,83585.94
2024-01-30,33513.94,50754.0,84267.94
2024-01-31,40113.94,46114.0,86227.94
2024-02-01,38705.94,44274.0,82979.94
2024-02-02,27305.940000000002,52690.0,79995.94
2024-02-05,22445.940000000002,55230.0,77675.94
2024-02-06,27245.940000000002,56198.0,83443.94
2024-02-07,36845.94,46666.0,83511.94
2024-02-08,31419.940000000002,51926.0,83345.94
2024-02-19,36311.94,48834.0,85145.94
2024-02-20,39861.28,47910.66,87771.94
2024-02-21,39861.28,48018.96,87880.23999999999
2024-02-22,41861.28,45818.24,87679.51999999999
2024-02-23,41861.28,44551.96,86413.23999999999
2024-02-26,45030.24,41690.74,86720.98
2024-02-27,48777.32,41789.439999999995,90566.76
2024-02-28,49161.3,39334.85999999999,88496.16
2024-02-29,45761.3,44599.96,90361.26000000001
2024-03-01,49575.3,40919.28,90494.58
2024-03-04,52677.94,37579.03999999999,90256.98
2024-03-05,51277.94,38087.04,89364.98000000001
2024-03-06,53277.94,36919.08,90197.02
2024-03-07,53677.94,35949.98,89627.92000000001
2024-03-08,53677.94,35737.8,89415.74
2024-03-11,57519.94,32557.06,90077.0
2024-03-12,59847.920000000006,30947.68,90795.6
2024-03-13,59847.920000000006,30429.56,90277.48000000001
2024-03-14,60047.920000000006,30268.980000000003,90316.90000000001
2024-03-15,60047.920000000006,31026.780000000002,91074.70000000001
2024-03-18,62559.920000000006,29269.68,91829.6
2024-03-19,62905.560000000005,29223.520000000004,92129.08000000002
2024-03-20,65165.560000000005,27477.68,92643.24
2024-03-21,65873.84,26703.800000000003,92577.64
2024-03-22,65873.84,26076.62,91950.45999999999
2024-03-25,61065.36,29863.06,90928.42
2024-03-26,59265.36,31568.499999999996,90833.86
2024-03-27,55865.36,34504.659999999996,90370.01999999999
2024-03-28,60401.36,30693.719999999998,91095.08
2024-03-29,60401.36,30781.6,91182.95999999999
2024-04-01,66053.02,26153.920000000002,92206.94
2024-04-02,66617.02,25795.5,92412.52
2024-04-03,65179.58,26641.44,91821.02
2024-04-08,63035.58,27350.300000000003,90385.88
2024-04-09,61435.58,29304.18,90739.76000000001
2024-04-10,56635.58,33710.5,90346.08
2024-04-11,56903.58,32962.08,89865.66
2024-04-12,56903.58,32507.18,89410.76000000001
2024-04-15,45899.58,40168.12,86067.70000000001
2024-04-16,36699.58,44627.16,81326.74
2024-04-17,33499.58,52978.979999999996,86478.56
2024-04-18,33499.58,53139.6,86639.18
2024-04-19,33499.58,52126.22,85625.8
2024-04-22,35961.58,49286.22,85247.8
2024-04-23,39761.58,47887.979999999996,87649.56
2024-04-24,41361.58,47785.3,89146.88
2024-04-25,41161.58,47469.36,88630.94
2024-04-26,42761.58,46693.66,89455.24
2024-04-29,46142.72,44846.68,90989.4
2024-04-30,51342.72,39697.380000000005,91040.1
2024-05-06,59221.060000000005,32499.46,91720.52
2024-05-07,59421.060000000005,32774.7,92195.76000000001
2024-05-08,59805.060000000005,31778.78,91583.84
2024-05-09,58005.060000000005,33495.740000000005,91500.80000000002
2024-05-10,56205.060000000005,34632.66,90837.72
2024-05-13,54405.060000000005,35785.520000000004,90190.58000000002
2024-05-14,56405.060000000005,34677.5,91082.56
2024-05-15,56791.060000000005,34668.46,91459.52
2024-05-16,58591.060000000005,33183.68,91774.74
2024-05-17,60869.060000000005,31647.16,92516.22
2024-05-20,60891.060000000005,31465.64,92356.70000000001
2024-05-21,61091.060000000005,31509.34,92600.40000000001
2024-05-22,61405.060000000005,31812.32,93217.38
2024-05-23,61405.060000000005,32031.88,93436.94
2024-05-24,59605.060000000005,32880.44,92485.5
2024-05-27,59387.50000000001,33578.0,92965.5
2024-05-28,63387.50000000001,29672.0,93059.5
2024-05-29,61787.50000000001,31334.0,93121.5
2024-05-30,64141.5,28916.0,93057.5
2024-05-31,64141.5,29130.0,93271.5
2024-06-03,59560.399999999994,32518.96,92079.35999999999
2024-06-04,57760.399999999994,33912.6,91673.0
2024-06-05,56160.399999999994,34984.96,91145.35999999999
2024-06-06,47960.399999999994,40566.12,88526.51999999999
2024-06-07,47960.399999999994,42130.56,90090.95999999999
2024-06-11,50552.399999999994,40529.64,91082.04
2024-06-12,54152.399999999994,37975.64,92128.04
2024-06-13,56152.399999999994,36528.12,92680.51999999999
2024-06-14,56152.399999999994,36035.36,92187.76
2024-06-17,58145.759999999995,33718.0,91863.76
2024-06-18,60345.759999999995,32524.0,92869.76
2024-06-19,64277.759999999995,29616.0,93893.76
2024-06-20,59921.759999999995,33686.0,93607.76
2024-06-21,62239.759999999995,31984.0,94223.76
2024-06-24,56927.759999999995,35350.0,92277.76
2024-06-25,53927.759999999995,37340.0,91267.76
2024-06-26,52527.759999999995,40366.0,92893.76
2024-06-27,52527.759999999995,39326.0,91853.76
2024-06-28,52527.759999999995,40070.0,92597.76
2024-07-01,57551.759999999995,35500.0,93051.76
2024-07-02,57551.759999999995,36244.0,93795.76
2024-07-03,57551.759999999995,36146.0,93697.76
2024-07-04,55751.759999999995,36268.0,92019.76
2024-07-05,52351.759999999995,40466.0,92817.76
2024-07-08,53531.759999999995,38242.0,91773.76
2024-07-09,52891.759999999995,40390.0,93281.76
2024-07-10,51451.719999999994,41648.04,93099.76
2024-07-11,51451.719999999994,42945.86,94397.57999999999
2024-07-12,51451.719999999994,42662.64,94114.35999999999
2024-07-15,52778.87999999999,40392.92,93171.79999999999
2024-07-16,52778.87999999999,41218.92,93997.79999999999
2024-07-17,57090.87999999999,36641.42,93732.29999999999
2024-07-18,55290.87999999999,38284.96,93575.84
2024-07-19,55290.87999999999,38665.44,93956.31999999999
2024-07-22,60996.87999999999,34089.5,95086.37999999999
2024-07-23,60996.87999999999,33558.479999999996,94555.35999999999
2024-07-24,62796.87999999999,31740.760000000002,94537.63999999998
2024-07-25,61256.87999999999,33841.24,95098.12
2024-07-26,61456.87999999999,33980.7,95437.57999999999
2024-07-29,59777.73999999999,35923.119999999995,95700.85999999999
2024-07-30,61777.73999999999,34529.72,96307.45999999999
2024-07-31,64101.73999999999,33334.0,97435.73999999999
2024-08-01,65125.73999999999,32018.0,97143.73999999999
2024-08-02,61925.73999999999,34468.0,96393.73999999999
2024-08-05,62605.73999999999,32892.0,95497.73999999999
2024-08-06,62605.73999999999,34194.0,96799.73999999999
2024-08-07,64405.73999999999,32874.0,97279.73999999999
2024-08-08,64605.73999999999,31938.0,96543.73999999999
2024-08-09,63005.73999999999,33154.0,96159.73999999999
2024-08-12,61315.73999999999,34852.0,96167.73999999999
2024-08-13,59515.73999999999,36410.0,95925.73999999999
2024-08-14,59515.73999999999,35968.0,95483.73999999999
2024-08-15,58515.73999999999,37140.0,95655.73999999999
2024-08-16,56715.73999999999,39058.0,95773.73999999999
2024-08-19,56764.19999999999,38072.64,94836.84
2024-08-20,56764.19999999999,38040.52,94804.71999999999
2024-08-21,55164.19999999999,38808.92,93973.12
2024-08-22,53564.19999999999,39192.04,92756.23999999999
2024-08-23,55364.19999999999,37672.44,93036.63999999998
2024-08-26,56940.25999999999,36384.899999999994,93325.15999999997
2024-08-27,57540.25999999999,35327.28,92867.53999999998
2024-08-28,52940.25999999999,39837.560000000005,92777.81999999999
2024-08-29,51892.25999999999,41458.62,93350.87999999999
2024-08-30,52183.51999999999,42771.06,94954.57999999999
2024-09-02,56710.33999999999,37626.06,94336.4
2024-09-03,58710.33999999999,36281.9,94992.23999999999
2024-09-04,58710.33999999999,35629.16,94339.5
2024-09-05,58710.33999999999,36421.92,95132.25999999998
2024-09-06,58710.33999999999,35878.58,94588.91999999998
2024-09-09,58906.33999999999,35322.54000000001,94228.88
2024-09-10,57306.33999999999,37236.6,94542.93999999999
2024-09-11,57306.33999999999,36978.36,94284.69999999998
2024-09-12,62642.33999999999,31378.359999999997,94020.69999999998
2024-09-13,62642.33999999999,30741.5,93383.84
2024-09-18,58008.74,34825.06,92833.79999999999
2024-09-19,58359.32,35364.84,93724.16
2024-09-20,58359.32,34882.94,93242.26000000001
2024-09-23,59825.32,33529.54,93354.86
2024-09-24,59825.32,35009.0,94834.32
2024-09-25,62217.32,33031.08,95248.4
2024-09-26,62723.32000000001,33992.1,96715.42000000001
2024-09-27,69235.32,29121.08,98356.40000000001
2024-09-30,80349.32,21369.96,101719.28
2024-10-08,85077.28,19476.0,104553.28
2024-10-09,77483.28,25692.0,103175.28
2024-10-10,65483.28,35852.0,101335.28
2024-10-11,56083.28,43112.0,99195.28
2024-10-14,54845.28,47550.0,102395.28
2024-10-15,62245.28,40032.0,102277.28
2024-10-16,64429.28,38264.0,102693.28
2024-10-17,71101.28,33832.0,104933.28
2024-10-18,71101.28,35670.0,106771.28
2024-10-21,81899.28,27310.0,109209.28
2024-10-22,78499.28,29890.0,108389.28
2024-10-23,79145.28,31210.0,110355.28
2024-10-24,83457.28,27304.0,110761.28
2024-10-25,87055.28,25324.0,112379.28
2024-10-28,82369.28,32322.0,114691.28
2024-10-29,94723.28,21330.0,116053.28
2024-10-30,93926.66,22730.62,116657.28
2024-10-31,92874.66,23630.56,116505.22
2024-11-01,90986.66,26929.52,117916.18000000001
2024-11-04,88286.66,31429.6,119716.26000000001
2024-11-05,99332.66,22827.28,122159.94
2024-11-06,100274.82,22557.2,122832.02
2024-11-07,97836.82,26869.02,124705.84000000001
2024-11-08,100636.82,23667.66,124304.48000000001
2024-11-11,97436.82,27560.78,124997.6
2024-11-12,97436.82,27386.06,124822.88
2024-11-13,95636.82,29058.3,124695.12000000001
2024-11-14,97636.82,26820.22,124457.04000000001
2024-11-15,96557.20000000001,28499.719999999998,125056.92000000001
2024-11-18,99403.46,26804.239999999998,126207.70000000001
2024-11-19,87403.46,38321.020000000004,125724.48000000001
2024-11-20,91665.46,36476.68,128142.14000000001
2024-11-21,97525.46,31670.04,129195.5
2024-11-22,102164.48000000001,26932.02,129096.50000000001
2024-11-25,100326.48000000001,29415.52,129742.00000000001
2024-11-26,98726.48000000001,30748.68,129475.16
2024-11-27,97126.48000000001,32619.879999999997,129746.36000000002
2024-11-28,101950.1,29081.36,131031.46
2024-11-29,101950.1,29600.280000000002,131550.38
2024-12-02,108792.78,23990.58,132783.36
2024-12-03,105192.78,27452.98,132645.76
2024-12-04,103392.78,27856.059999999998,131248.84
2024-12-05,103392.78,28402.48,131795.26
2024-12-06,103392.78,28411.32,131804.1
2024-12-09,97964.12,32072.58,130036.7
2024-12-10,96164.12,34057.399999999994,130221.51999999999
2024-12-11,94564.12,35279.64,129843.76
2024-12-12,94844.12,35000.0,129844.12
2024-12-13,96844.12,33198.0,130042.12
2024-12-16,92244.12,37306.0,129550.12
2024-12-17,90444.12,36824.0,127268.12
2024-12-18,85644.12,41096.0,126740.12
2024-12-19,82444.12,45412.0,127856.12
2024-12-20,82444.12,44764.0,127208.12
2024-12-23,86146.12,38902.0,125048.12
2024-12-24,88234.12,37016.0,125250.12
2024-12-25,83634.12,39390.0,123024.12
2024-12-26,85918.12,38396.0,124314.12
2024-12-27,89318.12,35982.0,125300.12
2024-12-30,84450.12,39132.0,123582.12
2024-12-31,86250.12,38410.0,124660.12
2025-01-02,85608.12,39822.0,125430.12
2025-01-03,87408.12,37578.0,124986.12
2025-01-06,87180.12,36526.0,123706.12
2025-01-07,85580.12,39196.0,124776.12
2025-01-08,85580.12,39276.0,124856.12
2025-01-09,85580.12,40108.0,125688.12
2025-01-10,85580.12,38096.0,123676.12
2025-01-13,86472.12,36632.0,123104.12
2025-01-14,90534.12,35542.0,126076.12
2025-01-15,92964.12,33396.0,126360.12
2025-01-16,92964.12,33474.0,126438.12
2025-01-17,92964.12,32718.0,125682.12
2025-01-20,92444.12,33334.0,125778.12
2025-01-21,90644.12,34562.0,125206.12
2025-01-22,88844.12,35200.0,124044.12
2025-01-23,92964.12,31104.0,124068.12
2025-01-24,92964.12,30942.0,123906.12
2025-01-27,93264.12,29920.0,123184.12
2025-02-05,95550.12,28902.0,124452.12
2025-02-06,95386.12,30334.0,125720.12
2025-02-07,99682.12,27098.0,126780.12
2025-02-10,101682.12,26282.0,127964.12
2025-02-11,102255.36,25700.760000000002,127956.12
2025-02-12,106503.36,22042.74,128546.1
2025-02-13,106503.36,21696.8,128200.16
2025-02-14,106503.36,21418.78,127922.14
2025-02-17,106503.36,22392.739999999998,128896.1
2025-02-18,106863.36,21594.6,128457.95999999999
2025-02-19,107549.72,21399.64,128949.36
2025-02-20,105749.72,23389.28,129139.0
2025-02-21,105749.72,23782.4,129532.12
2025-02-24,107749.72,22060.94,129810.66
2025-02-25,106050.72,23940.18,129990.9
2025-02-26,106050.72,24078.079999999998,130128.8
2025-02-27,104528.72,25574.48,130103.2
2025-02-28,102728.72,26542.96,129271.68
2025-03-03,99240.72,30762.88,130003.6
2025-03-04,104273.36,27485.84,131759.2
2025-03-05,106519.36,25571.420000000002,132090.78
2025-03-06,103171.36,29194.32,132365.68
2025-03-07,105425.36,27040.4,132465.76
2025-03-10,105425.36,27684.64,133110.0
2025-03-11,105757.36,27476.72,133234.08000000002
2025-03-12,107757.36,26166.48,133923.84
2025-03-13,110301.82,23848.38,134150.2
2025-03-14,115013.82,19782.36,134796.18
2025-03-17,115373.82,19713.06,135086.88
2025-03-18,116287.82,19132.559999999998,135420.38
2025-03-19,112687.82,22446.72,135134.54
2025-03-20,111223.82,24020.72,135244.54
2025-03-21,109423.82,25211.120000000003,134634.94
2025-03-24,102104.94,32562.0,134666.94
2025-03-25,104460.1,30420.84,134880.94
2025-03-26,106460.1,29221.84,135681.94
2025-03-27,106660.1,28888.8,135548.9
2025-03-28,103822.1,31484.3,135306.4
2025-03-31,100434.62000000001,34067.08,134501.7
2025-04-01,101400.62000000001,32980.64,134381.26
2025-04-02,101600.62000000001,32563.239999999998,134163.86000000002
2025-04-03,99800.62000000001,34518.32,134318.94
2025-04-07,87000.62000000001,41837.2,128837.82
2025-04-08,80800.62000000001,49853.04,130653.66
2025-04-09,88600.62000000001,46010.08,134610.7
2025-04-10,98000.62000000001,38655.6,136656.22
2025-04-11,99600.62000000001,37063.76,136664.38
2025-04-14,106254.62000000001,31782.72,138037.34000000003
2025-04-15,104654.62000000001,33317.28,137971.90000000002
2025-04-16,106747.54000000001,31677.72,138425.26
2025-04-17,108747.54000000001,30180.46,138928.0
2025-04-18,109341.54000000001,30561.36,139902.90000000002
2025-04-21,107815.54000000001,32425.2,140240.74000000002
2025-04-22,107815.54000000001,31336.8,139152.34
2025-04-23,109415.54000000001,30456.64,139872.18
2025-04-24,109657.54000000001,29465.66,139123.2
2025-04-25,106835.54000000001,32281.18,139116.72
2025-04-28,106345.54000000001,32082.7,138428.24000000002
2025-04-29,102745.54000000001,35483.0,138228.54
2025-04-30,102745.54000000001,35759.3,138504.84000000003
2025-05-06,104510.84000000001,35040.0,139550.84000000003
2025-05-07,107283.84000000001,32649.0,139932.84000000003
2025-05-08,104437.84000000001,35792.24,140230.08000000002
2025-05-09,102637.84000000001,37585.88,140223.72
2025-05-12,103367.84000000001,37894.72,141262.56
2025-05-13,103367.84000000001,37302.54,140670.38
2025-05-14,103763.84000000001,36662.0,140425.84000000003
2025-05-15,101963.84000000001,38680.0,140643.84000000003
2025-05-16,101963.84000000001,38900.0,140863.84000000003
2025-05-19,101963.84000000001,39840.0,141803.84000000003
2025-05-20,105763.84000000001,36696.0,142459.84000000003
2025-05-21,107363.84000000001,35314.0,142677.84000000003
2025-05-22,109423.84000000001,33250.0,142673.84000000003
2025-05-23,109423.84000000001,32758.0,142181.84000000003
2025-05-26,110715.84000000001,32416.0,143131.84000000003
2025-05-27,114765.84000000001,28756.0,143521.84000000003
2025-05-28,118565.84000000001,24386.0,142951.84000000003
2025-05-29,118565.84000000001,24558.0,143123.84000000003
2025-05-30,115165.84000000001,27606.0,142771.84000000003
2025-06-03,113691.88,29479.86,143171.74
2025-06-04,117691.88,26375.22,144067.1
2025-06-05,118367.88,25461.82,143829.7
2025-06-06,118567.88,25523.98,144091.86000000002
2025-06-09,118468.06,26145.58,144613.64
2025-06-10,119242.06,26013.68,145255.74
2025-06-11,115642.06,29543.460000000003,145185.52
2025-06-12,119442.06,26731.8,146173.86
2025-06-13,121827.45999999999,23271.14,145098.59999999998
2025-06-16,118827.45999999999,26558.000000000004,145385.46
2025-06-17,119851.45999999999,25974.019999999997,145825.47999999998
2025-06-18,118051.45999999999,27032.4,145083.86
2025-06-19,112651.45999999999,31251.42,143902.88
2025-06-20,109651.45999999999,33717.020000000004,143368.47999999998
2025-06-23,108051.45999999999,36004.66,144056.12
2025-06-24,108051.45999999999,36450.659999999996,144502.12
2025-06-25,108567.45999999999,36190.72,144758.18
2025-06-26,108767.45999999999,36286.82,145054.28
2025-06-27,108767.45999999999,36246.7,145014.15999999997
2025-06-30,108321.45999999999,37252.479999999996,145573.94
2025-07-01,115200.18,31736.68,146936.86
2025-07-02,117302.28,30046.74,147349.02
2025-07-03,117302.28,29892.84,147195.12
2025-07-04,117302.28,29383.94,146686.22
2025-07-07,117586.28,29861.88,147448.16
2025-07-08,117586.28,29913.9,147500.18
2025-07-09,117912.28,29733.94,147646.22
2025-07-10,118112.28,29558.0,147670.28
2025-07-11,116312.28,31418.0,147730.28
2025-07-14,114316.28,33108.0,147424.28
2025-07-15,112716.28,34186.0,146902.28
2025-07-16,114716.28,32426.0,147142.28
2025-07-17,114960.2,32414.08,147374.28
2025-07-18,113160.2,33946.38,147106.58
2025-07-21,116704.2,30680.16,147384.36
2025-07-22,116980.59999999999,30577.559999999998,147558.15999999997
2025-07-23,116980.59999999999,30195.579999999998,147176.18
2025-07-24,115180.59999999999,32133.800000000003,147314.4
2025-07-25,115180.59999999999,32153.94,147334.53999999998
2025-07-28,115180.59999999999,32243.62,147424.22
2025-07-29,115180.59999999999,31921.92,147102.52
2025-07-30,115180.59999999999,31987.88,147168.47999999998
2025-07-31,113580.59999999999,33084.16,146664.76
2025-08-01,113316.59999999999,33357.8,146674.4
2025-08-04,116562.59999999999,30359.96,146922.56
2025-08-05,114762.59999999999,32258.28,147020.88
2025-08-06,114762.59999999999,32168.1,146930.69999999998
2025-08-07,114762.59999999999,32379.9,147142.5
2025-08-08,119086.59999999999,28390.0,147476.59999999998
2025-08-11,119218.59999999999,28752.0,147970.59999999998
2025-08-12,117418.59999999999,30502.0,147920.59999999998
2025-08-13,121062.59999999999,26722.0,147784.59999999998
2025-08-14,117662.59999999999,29492.0,147154.59999999998
2025-08-15,119662.59999999999,28798.0,148460.59999999998
2025-08-18,123252.59999999999,26458.0,149710.59999999998
2025-08-19,122090.59999999999,27948.0,150038.59999999998
2025-08-20,120290.59999999999,29984.0,150274.59999999998
2025-08-21,126290.59999999999,24298.0,150588.59999999998
2025-08-22,126652.59999999999,23932.0,150584.59999999998
2025-08-25,126652.59999999999,24162.0,150814.59999999998
2025-08-26,126852.59999999999,24104.0,150956.59999999998
2025-08-27,123646.59999999999,26804.0,150450.59999999998
2025-08-28,118640.59999999999,32296.0,150936.59999999998
2025-08-29,116840.59999999999,33900.0,150740.59999999998
2025-09-01,122140.01999999999,29354.24,151494.25999999998
2025-09-02,120726.01999999999,30304.0,151030.02
2025-09-03,117326.01999999999,32868.0,150194.02
2025-09-04,114126.01999999999,35582.0,149708.02
2025-09-05,114126.01999999999,36400.0,150526.02
2025-09-08,114126.01999999999,36670.0,150796.02
2025-09-09,114126.01999999999,35764.0,149890.02
2025-09-10,114126.01999999999,36264.0,150390.02
2025-09-11,116326.01999999999,34852.0,151178.02
2025-09-12,116580.01999999999,34638.0,151218.02
2025-09-15,116840.01999999999,34224.0,151064.02
2025-09-16,116840.01999999999,34266.0,151106.02
2025-09-17,116840.01999999999,34016.0,150856.02
2025-09-18,115522.01999999999,34918.0,150440.02
2025-09-19,115522.01999999999,34548.0,150070.02
2025-09-22,116524.01999999999,33574.0,150098.02
2025-09-23,111924.01999999999,37476.0,149400.02
2025-09-24,112264.01999999999,37360.0,149624.02
2025-09-25,112264.01999999999,36962.0,149226.02
2025-09-26,112264.01999999999,36744.0,149008.02
2025-09-29,116726.01999999999,32582.0,149308.02
2025-09-30,116726.01999999999,32560.0,149286.02
2025-10-09,118370.01999999999,30188.0,148558.02
2025-10-10,116570.01999999999,31992.0,148562.02
2025-10-13,112600.01999999999,36462.0,149062.02
2025-10-14,112600.01999999999,36110.0,148710.02
2025-10-15,112600.01999999999,36570.0,149170.02
2025-10-16,114600.01999999999,34026.0,148626.02
2025-10-17,113000.01999999999,34894.0,147894.02
2025-10-20,112998.01999999999,35922.0,148920.02
2025-10-21,114998.01999999999,34854.0,149852.02
2025-10-22,114998.01999999999,34606.0,149604.02
2025-10-23,113198.01999999999,36532.0,149730.02
2025-10-24,113198.01999999999,36844.0,150042.02
2025-10-27,116430.01999999999,33332.0,149762.02
2025-10-28,116430.01999999999,33280.0,149710.02
2025-10-29,116430.01999999999,33934.0,150364.02
2025-10-30,116430.01999999999,33806.0,150236.02
2025-10-31,116430.01999999999,34148.0,150578.02
2025-11-03,121148.01999999999,29008.0,150156.02
2025-11-04,120108.01999999999,30110.0,150218.02
2025-11-05,120108.01999999999,30552.0,150660.02
2025-11-06,118308.01999999999,32286.0,150594.02
2025-11-07,118702.01999999999,31342.0,150044.02
2025-11-10,118702.01999999999,31688.0,150390.02
2025-11-11,123102.01999999999,27790.0,150892.02
2025-11-12,118426.01999999999,32408.0,150834.02
2025-11-13,119016.01999999999,32752.0,151768.02
2025-11-14,119016.01999999999,33214.0,152230.02
2025-11-17,123270.01999999999,29114.0,152384.02
2025-11-18,119670.01999999999,31876.0,151546.02
2025-11-19,118070.01999999999,33268.0,151338.02
2025-11-20,116470.01999999999,34108.0,150578.02
2025-11-21,109870.01999999999,38562.0,148432.02
2025-11-24,109938.01999999999,38512.0,148450.02
2025-11-25,111938.01999999999,37762.0,149700.02
2025-11-26,111938.01999999999,37170.0,149108.02
2025-11-27,111938.01999999999,37172.0,149110.02
2025-11-28,111938.01999999999,37834.0,149772.02
2025-12-01,115825.68,33942.38,149768.06
2025-12-02,115825.68,33994.38,149820.06
2025-12-03,117825.68,31736.9,149562.58
2025-12-04,112425.68,36213.44,148639.12
2025-12-05,109425.68,40135.04,149560.72
2025-12-08,112603.68,37374.88,149978.56
2025-12-09,116729.68,33392.68,150122.36
2025-12-10,117313.68,33230.36,150544.03999999998
2025-12-11,115769.68,34872.0,150641.68
2025-12-12,115769.68,35290.0,151059.68
2025-12-15,118523.68,33424.0,151947.68
2025-12-16,118723.68,32706.0,151429.68
2025-12-17,111723.68,39646.0,151369.68
2025-12-18,111723.68,39308.0,151031.68
2025-12-19,110123.68,41948.0,152071.68
2025-12-22,116025.68,36470.0,152495.68
2025-12-23,116225.68,36004.0,152229.68
2025-12-24,115195.68,37596.0,152791.68
2025-12-25,115571.68,37138.0,152709.68
2025-12-26,115371.68,37724.0,153095.68
2025-12-29,118263.68,34576.0,152839.68
2025-12-30,116959.68,35302.0,152261.68
2025-12-31,115159.68,36376.0,151535.68
2026-01-05,114658.76,36630.8,151289.56
2026-01-06,113458.76,38219.44,151678.2
2026-01-07,113854.76,37808.0,151662.76
2026-01-08,112054.76,40598.0,152652.76
2026-01-09,113854.76,39384.0,153238.76
2026-01-12,114728.76,38852.0,153580.76
2026-01-13,120616.76,32848.0,153464.76
2026-01-14,121016.76,32938.0,153954.76
2026-01-15,121216.76,32146.0,153362.76
2026-01-16,117816.76,34904.0,152720.76
2026-01-19,118854.76,34260.0,153114.76
2026-01-20,117516.76,35734.0,153250.76
2026-01-21,120376.76,34212.0,154588.76
2026-01-22,120376.76,34816.0,155192.76
2026-01-23,120376.76,35548.0,155924.76
2026-01-26,119168.76,36470.0,155638.76
2026-01-27,119368.76,36470.0,155838.76
2026-01-28,119368.76,35508.0,154876.76
2026-01-29,117768.76,37012.0,154780.76
2026-01-30,118104.76,36686.0,154790.76
2026-02-02,123830.76,30870.0,154700.76
2026-02-03,123830.76,31876.000000000004,155706.76
2026-02-04,123830.76,31492.0,155322.76
2026-02-05,123830.76,31724.0,155554.76
2026-02-06,123830.76,32184.0,156014.76
2026-02-09,124128.76,32940.0,157068.76
2026-02-10,130252.76000000001,27476.0,157728.76
2026-02-11,128452.76000000001,29178.0,157630.76
2026-02-12,130452.76000000001,27600.0,158052.76
2026-02-13,130652.76000000001,27454.0,158106.76
2026-02-24,129478.76000000001,29166.0,158644.76
2026-02-25,129678.76000000001,29674.0,159352.76
2026-02-26,129878.76000000001,29368.0,159246.76
2026-02-27,128078.76000000001,31448.0,159526.76
2026-03-02,126568.76000000001,31928.0,158496.76
2026-03-03,123168.76000000001,33314.0,156482.76
2026-03-04,121530.76000000001,35378.0,156908.76
2026-03-05,123530.76000000001,33996.0,157526.76
2026-03-06,121930.76000000001,35786.0,157716.76
2026-03-09,120130.76000000001,37128.0,157258.76
2026-03-10,122130.76000000001,36208.0,158338.76
2026-03-11,122498.76000000001,35428.0,157926.76
2026-03-12,122498.76000000001,34782.0,157280.76
2026-03-13,122818.76000000001,33916.0,156734.76
2026-03-16,119866.98000000001,36893.56,156760.54
2026-03-17,120266.98000000001,35880.16,156147.14
2026-03-18,120266.98000000001,36240.88,156507.86000000002
2026-03-19,115666.98000000001,39534.56,155201.54
2026-03-20,114066.98000000001,39770.48,153837.46000000002
2026-03-23,112544.98000000001,40740.8,153285.78000000003
2026-03-24,111144.98000000001,44227.28,155372.26
2026-03-25,113144.98000000001,43183.8,156328.78000000003
2026-03-26,114944.98000000001,40874.96,155819.94
2026-03-27,114944.98000000001,41492.52,156437.5
2026-03-30,118332.98000000001,38077.28,156410.26
2026-03-31,118332.98000000001,37634.52,155967.5
2026-04-01,121430.98000000001,35512.04,156943.02000000002
2026-04-02,121430.98000000001,34988.72,156419.7
2026-04-03,118420.98000000001,37343.56,155764.54
2026-04-07,117714.98000000001,38003.479999999996,155718.46000000002
2026-04-08,117980.98000000001,39241.32,157222.30000000002
2026-04-09,117980.98000000001,38689.08,156670.06
2026-04-10,118180.98000000001,38458.6,156639.58000000002
2026-04-13,116651.58000000002,39420.0,156071.58000000002
2026-04-14,115051.58000000002,40588.0,155639.58000000002
2026-04-15,114451.58000000002,40568.0,155019.58000000002
2026-04-16,113051.58000000002,41612.0,154663.58000000002
2026-04-17,111851.58000000002,42540.0,154391.58000000002
2026-04-20,111939.58000000002,43314.0,155253.58000000002
2026-04-21,111939.58000000002,43070.0,155009.58000000002
2026-04-22,111939.58000000002,42160.0,154099.58000000002
2026-04-23,110939.58000000002,41660.0,152599.58000000002
2026-04-24,109899.58000000002,42320.0,152219.58000000002
2026-04-27,113003.58000000002,39480.0,152483.58000000002
2026-04-28,112317.58000000002,38134.0,150451.58000000002
2026-04-29,113317.58000000002,37898.0,151215.58000000002
2026-04-30,111717.58000000002,39436.0,151153.58000000002
1 date cash holding_market_value total_asset
2 2023-05-04 28289.86 33283.08 61572.94
3 2023-05-05 29353.86 32311.96 61665.82
4 2023-05-08 29553.86 32215.16 61769.020000000004
5 2023-05-09 27953.86 32879.84 60833.7
6 2023-05-10 26153.86 35374.479999999996 61528.34
7 2023-05-11 29753.86 33225.380000000005 62979.240000000005
8 2023-05-12 24353.86 37200.04 61553.9
9 2023-05-15 25667.86 35345.6 61013.46
10 2023-05-16 20867.86 38643.44 59511.3
11 2023-05-17 17667.86 42281.4 59949.26
12 2023-05-18 17667.86 43420.1 61087.96
13 2023-05-19 19999.86 40128.84 60128.7
14 2023-05-22 19136.34 40407.96 59544.3
15 2023-05-23 19136.34 39661.66 58798.0
16 2023-05-24 19136.34 39738.92 58875.259999999995
17 2023-05-25 17736.34 40531.479999999996 58267.81999999999
18 2023-05-26 17736.34 41098.4 58834.740000000005
19 2023-05-29 14136.34 44736.28 58872.619999999995
20 2023-05-30 14336.34 45939.42 60275.759999999995
21 2023-05-31 14336.34 45642.6 59978.94
22 2023-06-01 20433.92 41484.86 61918.78
23 2023-06-02 22233.92 40253.44 62487.36
24 2023-06-05 20683.899999999998 42477.36 63161.259999999995
25 2023-06-06 24283.899999999998 37810.38 62094.28
26 2023-06-07 22683.899999999998 40119.560000000005 62803.46000000001
27 2023-06-08 21083.899999999998 40887.58 61971.479999999996
28 2023-06-09 23083.899999999998 39432.4 62516.3
29 2023-06-12 23083.899999999998 39468.780000000006 62552.68000000001
30 2023-06-13 23083.899999999998 40270.58 63354.479999999996
31 2023-06-14 24883.899999999998 38792.08 63675.979999999996
32 2023-06-15 26883.899999999998 36531.32000000001 63415.22
33 2023-06-16 27171.899999999998 36662.04 63833.94
34 2023-06-19 30577.359999999997 33518.72 64096.08
35 2023-06-20 28977.359999999997 35500.700000000004 64478.06
36 2023-06-21 27177.359999999997 35306.82 62484.17999999999
37 2023-06-26 25315.46 35218.7 60534.159999999996
38 2023-06-27 20515.46 40464.58 60980.04
39 2023-06-28 20515.46 39961.2 60476.659999999996
40 2023-06-29 20515.46 40167.38 60682.84
41 2023-06-30 20515.46 40323.66 60839.12
42 2023-07-03 28555.76 32259.06 60814.82
43 2023-07-04 28555.76 32140.9 60696.66
44 2023-07-05 28555.76 31760.359999999997 60316.119999999995
45 2023-07-06 28555.76 32029.4 60585.16
46 2023-07-07 27155.76 33151.0 60306.759999999995
47 2023-07-10 30335.76 30607.5 60943.259999999995
48 2023-07-11 30335.76 29987.239999999998 60323.0
49 2023-07-12 27135.76 32715.800000000003 59851.56
50 2023-07-13 28735.76 32062.980000000003 60798.740000000005
51 2023-07-14 29091.76 31705.800000000003 60797.56
52 2023-07-17 27259.08 33179.8 60438.880000000005
53 2023-07-18 27259.08 33110.299999999996 60369.38
54 2023-07-19 27259.08 33071.82 60330.9
55 2023-07-20 27259.08 32716.16 59975.240000000005
56 2023-07-21 27608.82 32509.1 60117.92
57 2023-07-24 29909.840000000004 30081.24 59991.08
58 2023-07-25 29909.840000000004 30276.039999999997 60185.880000000005
59 2023-07-26 31797.840000000004 27988.0 59785.840000000004
60 2023-07-27 31797.840000000004 27916.859999999997 59714.7
61 2023-07-28 30397.840000000004 29960.780000000002 60358.62000000001
62 2023-07-31 31034.120000000003 29691.679999999997 60725.8
63 2023-08-01 31326.78 29226.62 60553.399999999994
64 2023-08-02 33126.78 27590.860000000004 60717.64
65 2023-08-03 31326.78 29124.66 60451.44
66 2023-08-04 31326.78 29340.219999999998 60667.0
67 2023-08-07 28402.46 32404.219999999998 60806.67999999999
68 2023-08-08 28402.46 32277.72 60680.18
69 2023-08-09 28602.46 31887.659999999996 60490.119999999995
70 2023-08-10 28602.46 32160.539999999997 60763.0
71 2023-08-11 28602.46 31713.8 60316.259999999995
72 2023-08-14 30216.02 30586.119999999995 60802.14
73 2023-08-15 30216.02 30597.48 60813.5
74 2023-08-16 32216.02 28753.199999999997 60969.22
75 2023-08-17 30416.02 31062.44 61478.46
76 2023-08-18 33136.700000000004 28257.34 61394.04000000001
77 2023-08-21 31336.700000000004 30055.36 61392.060000000005
78 2023-08-22 29536.700000000004 32326.899999999998 61863.600000000006
79 2023-08-23 29536.700000000004 31548.5 61085.200000000004
80 2023-08-24 29536.700000000004 31743.98 61280.68000000001
81 2023-08-25 26136.700000000004 34300.62 60437.32000000001
82 2023-08-28 29708.780000000006 31279.519999999997 60988.3
83 2023-08-29 30196.780000000006 32665.699999999997 62862.48
84 2023-08-30 32196.780000000006 31247.879999999997 63444.66
85 2023-08-31 34196.780000000006 29057.66 63254.44
86 2023-09-01 33946.780000000006 29311.820000000003 63258.600000000006
87 2023-09-04 34135.520000000004 29560.14 63695.66
88 2023-09-05 34509.780000000006 29185.22 63695.00000000001
89 2023-09-06 32709.780000000006 30561.239999999998 63271.020000000004
90 2023-09-07 33082.920000000006 30092.64 63175.560000000005
91 2023-09-08 29682.920000000006 33243.98 62926.90000000001
92 2023-09-11 28129.100000000006 35377.44 63506.54000000001
93 2023-09-12 30129.100000000006 33934.32 64063.420000000006
94 2023-09-13 31929.100000000006 32089.72 64018.82000000001
95 2023-09-14 31929.100000000006 31721.36 63650.46000000001
96 2023-09-15 33929.100000000006 29926.239999999998 63855.340000000004
97 2023-09-18 35984.3 28165.4 64149.700000000004
98 2023-09-19 36370.3 27593.84 63964.14
99 2023-09-20 36917.340000000004 27446.239999999998 64363.58
100 2023-09-21 35117.340000000004 29112.16 64229.5
101 2023-09-22 35117.340000000004 29932.44 65049.78
102 2023-09-25 35673.340000000004 30188.22 65861.56
103 2023-09-26 33873.340000000004 31843.26 65716.6
104 2023-09-27 33873.340000000004 32595.0 66468.34
105 2023-09-28 39873.340000000004 27709.4 67582.74
106 2023-10-09 41247.340000000004 26008.479999999996 67255.82
107 2023-10-10 43605.08 24212.319999999996 67817.4
108 2023-10-11 43805.08 23993.4 67798.48000000001
109 2023-10-12 44083.08 23915.480000000003 67998.56
110 2023-10-13 44083.08 23683.52 67766.6
111 2023-10-16 43689.08 23793.599999999995 67482.68
112 2023-10-17 42089.08 25216.94 67306.02
113 2023-10-18 38489.08 28344.4 66833.48000000001
114 2023-10-19 36689.08 29789.940000000002 66479.02
115 2023-10-20 33289.08 32655.699999999997 65944.78
116 2023-10-23 31442.520000000004 33523.66 64966.18000000001
117 2023-10-24 29642.520000000004 36040.58 65683.1
118 2023-10-25 33642.520000000004 33017.58 66660.1
119 2023-10-26 34288.520000000004 33149.32 67437.84
120 2023-10-27 36088.520000000004 31940.920000000002 68029.44
121 2023-10-30 37285.62 32011.34 69296.96
122 2023-10-31 39938.9 29558.3 69497.2
123 2023-11-01 38429.700000000004 30972.719999999994 69402.42
124 2023-11-02 40429.700000000004 28891.5 69321.20000000001
125 2023-11-03 40429.700000000004 29377.8 69807.5
126 2023-11-06 41176.26 29848.48 71024.74
127 2023-11-07 45432.26 26553.34 71985.6
128 2023-11-08 45782.26 26698.7 72480.96
129 2023-11-09 48079.740000000005 24910.94 72990.68000000001
130 2023-11-10 46479.740000000005 25900.26 72380.0
131 2023-11-13 48501.3 24292.92 72794.22
132 2023-11-14 46701.3 26683.64 73384.94
133 2023-11-15 46957.340000000004 26097.679999999997 73055.02
134 2023-11-16 47157.340000000004 26279.88 73437.22
135 2023-11-17 49341.340000000004 24523.559999999998 73864.9
136 2023-11-20 51584.740000000005 23124.5 74709.24
137 2023-11-21 51940.740000000005 22778.399999999998 74719.14
138 2023-11-22 49054.740000000005 26431.32 75486.06
139 2023-11-23 50182.740000000005 25830.2 76012.94
140 2023-11-24 49700.740000000005 27047.760000000002 76748.5
141 2023-11-27 52958.340000000004 24999.68 77958.02
142 2023-11-28 46244.340000000004 30826.699999999997 77071.04000000001
143 2023-11-29 41844.340000000004 33468.94 75313.28
144 2023-11-30 37244.340000000004 37343.26 74587.6
145 2023-12-01 33892.340000000004 39456.38 73348.72
146 2023-12-04 29451.340000000004 43211.36 72662.70000000001
147 2023-12-05 36811.340000000004 40589.1 77400.44
148 2023-12-06 43811.340000000004 34448.659999999996 78260.0
149 2023-12-07 49721.340000000004 30807.46 80528.8
150 2023-12-08 50831.340000000004 30194.3 81025.64
151 2023-12-11 41721.94 37976.44 79698.38
152 2023-12-12 41967.94 38844.28 80812.22
153 2023-12-13 41967.94 40205.56 82173.5
154 2023-12-14 48057.94 35801.66 83859.6
155 2023-12-15 52387.94 31884.0 84271.94
156 2023-12-18 54779.94 31022.0 85801.94
157 2023-12-19 56379.94 28964.0 85343.94
158 2023-12-20 58379.94 28260.0 86639.94
159 2023-12-21 60909.94 24672.0 85581.94
160 2023-12-22 52909.94 32946.0 85855.94
161 2023-12-25 54909.94 32782.0 87691.94
162 2023-12-26 54909.94 33254.0 88163.94
163 2023-12-27 57197.94 31222.0 88419.94
164 2023-12-28 55761.94 33084.0 88845.94
165 2023-12-29 56387.94 33956.0 90343.94
166 2024-01-02 62813.94 29938.0 92751.94
167 2024-01-03 65205.94 27608.0 92813.94
168 2024-01-04 63405.94 29272.0 92677.94
169 2024-01-05 56405.94 34602.0 91007.94
170 2024-01-08 58405.94 31830.0 90235.94
171 2024-01-09 48405.94 40204.0 88609.94
172 2024-01-10 43605.94 45832.0 89437.94
173 2024-01-11 45405.94 44096.0 89501.94
174 2024-01-12 42405.94 42438.0 84843.94
175 2024-01-15 35605.94 46922.0 82527.94
176 2024-01-16 34005.94 52684.0 86689.94
177 2024-01-17 37205.94 49538.0 86743.94
178 2024-01-18 39005.94 49904.0 88909.94
179 2024-01-19 44405.94 46458.0 90863.94
180 2024-01-22 45355.94 42936.0 88291.94
181 2024-01-23 45355.94 44114.0 89469.94
182 2024-01-24 47895.94 42334.0 90229.94
183 2024-01-25 46695.94 42518.0 89213.94
184 2024-01-26 43543.94 44782.0 88325.94
185 2024-01-29 39113.94 44472.0 83585.94
186 2024-01-30 33513.94 50754.0 84267.94
187 2024-01-31 40113.94 46114.0 86227.94
188 2024-02-01 38705.94 44274.0 82979.94
189 2024-02-02 27305.940000000002 52690.0 79995.94
190 2024-02-05 22445.940000000002 55230.0 77675.94
191 2024-02-06 27245.940000000002 56198.0 83443.94
192 2024-02-07 36845.94 46666.0 83511.94
193 2024-02-08 31419.940000000002 51926.0 83345.94
194 2024-02-19 36311.94 48834.0 85145.94
195 2024-02-20 39861.28 47910.66 87771.94
196 2024-02-21 39861.28 48018.96 87880.23999999999
197 2024-02-22 41861.28 45818.24 87679.51999999999
198 2024-02-23 41861.28 44551.96 86413.23999999999
199 2024-02-26 45030.24 41690.74 86720.98
200 2024-02-27 48777.32 41789.439999999995 90566.76
201 2024-02-28 49161.3 39334.85999999999 88496.16
202 2024-02-29 45761.3 44599.96 90361.26000000001
203 2024-03-01 49575.3 40919.28 90494.58
204 2024-03-04 52677.94 37579.03999999999 90256.98
205 2024-03-05 51277.94 38087.04 89364.98000000001
206 2024-03-06 53277.94 36919.08 90197.02
207 2024-03-07 53677.94 35949.98 89627.92000000001
208 2024-03-08 53677.94 35737.8 89415.74
209 2024-03-11 57519.94 32557.06 90077.0
210 2024-03-12 59847.920000000006 30947.68 90795.6
211 2024-03-13 59847.920000000006 30429.56 90277.48000000001
212 2024-03-14 60047.920000000006 30268.980000000003 90316.90000000001
213 2024-03-15 60047.920000000006 31026.780000000002 91074.70000000001
214 2024-03-18 62559.920000000006 29269.68 91829.6
215 2024-03-19 62905.560000000005 29223.520000000004 92129.08000000002
216 2024-03-20 65165.560000000005 27477.68 92643.24
217 2024-03-21 65873.84 26703.800000000003 92577.64
218 2024-03-22 65873.84 26076.62 91950.45999999999
219 2024-03-25 61065.36 29863.06 90928.42
220 2024-03-26 59265.36 31568.499999999996 90833.86
221 2024-03-27 55865.36 34504.659999999996 90370.01999999999
222 2024-03-28 60401.36 30693.719999999998 91095.08
223 2024-03-29 60401.36 30781.6 91182.95999999999
224 2024-04-01 66053.02 26153.920000000002 92206.94
225 2024-04-02 66617.02 25795.5 92412.52
226 2024-04-03 65179.58 26641.44 91821.02
227 2024-04-08 63035.58 27350.300000000003 90385.88
228 2024-04-09 61435.58 29304.18 90739.76000000001
229 2024-04-10 56635.58 33710.5 90346.08
230 2024-04-11 56903.58 32962.08 89865.66
231 2024-04-12 56903.58 32507.18 89410.76000000001
232 2024-04-15 45899.58 40168.12 86067.70000000001
233 2024-04-16 36699.58 44627.16 81326.74
234 2024-04-17 33499.58 52978.979999999996 86478.56
235 2024-04-18 33499.58 53139.6 86639.18
236 2024-04-19 33499.58 52126.22 85625.8
237 2024-04-22 35961.58 49286.22 85247.8
238 2024-04-23 39761.58 47887.979999999996 87649.56
239 2024-04-24 41361.58 47785.3 89146.88
240 2024-04-25 41161.58 47469.36 88630.94
241 2024-04-26 42761.58 46693.66 89455.24
242 2024-04-29 46142.72 44846.68 90989.4
243 2024-04-30 51342.72 39697.380000000005 91040.1
244 2024-05-06 59221.060000000005 32499.46 91720.52
245 2024-05-07 59421.060000000005 32774.7 92195.76000000001
246 2024-05-08 59805.060000000005 31778.78 91583.84
247 2024-05-09 58005.060000000005 33495.740000000005 91500.80000000002
248 2024-05-10 56205.060000000005 34632.66 90837.72
249 2024-05-13 54405.060000000005 35785.520000000004 90190.58000000002
250 2024-05-14 56405.060000000005 34677.5 91082.56
251 2024-05-15 56791.060000000005 34668.46 91459.52
252 2024-05-16 58591.060000000005 33183.68 91774.74
253 2024-05-17 60869.060000000005 31647.16 92516.22
254 2024-05-20 60891.060000000005 31465.64 92356.70000000001
255 2024-05-21 61091.060000000005 31509.34 92600.40000000001
256 2024-05-22 61405.060000000005 31812.32 93217.38
257 2024-05-23 61405.060000000005 32031.88 93436.94
258 2024-05-24 59605.060000000005 32880.44 92485.5
259 2024-05-27 59387.50000000001 33578.0 92965.5
260 2024-05-28 63387.50000000001 29672.0 93059.5
261 2024-05-29 61787.50000000001 31334.0 93121.5
262 2024-05-30 64141.5 28916.0 93057.5
263 2024-05-31 64141.5 29130.0 93271.5
264 2024-06-03 59560.399999999994 32518.96 92079.35999999999
265 2024-06-04 57760.399999999994 33912.6 91673.0
266 2024-06-05 56160.399999999994 34984.96 91145.35999999999
267 2024-06-06 47960.399999999994 40566.12 88526.51999999999
268 2024-06-07 47960.399999999994 42130.56 90090.95999999999
269 2024-06-11 50552.399999999994 40529.64 91082.04
270 2024-06-12 54152.399999999994 37975.64 92128.04
271 2024-06-13 56152.399999999994 36528.12 92680.51999999999
272 2024-06-14 56152.399999999994 36035.36 92187.76
273 2024-06-17 58145.759999999995 33718.0 91863.76
274 2024-06-18 60345.759999999995 32524.0 92869.76
275 2024-06-19 64277.759999999995 29616.0 93893.76
276 2024-06-20 59921.759999999995 33686.0 93607.76
277 2024-06-21 62239.759999999995 31984.0 94223.76
278 2024-06-24 56927.759999999995 35350.0 92277.76
279 2024-06-25 53927.759999999995 37340.0 91267.76
280 2024-06-26 52527.759999999995 40366.0 92893.76
281 2024-06-27 52527.759999999995 39326.0 91853.76
282 2024-06-28 52527.759999999995 40070.0 92597.76
283 2024-07-01 57551.759999999995 35500.0 93051.76
284 2024-07-02 57551.759999999995 36244.0 93795.76
285 2024-07-03 57551.759999999995 36146.0 93697.76
286 2024-07-04 55751.759999999995 36268.0 92019.76
287 2024-07-05 52351.759999999995 40466.0 92817.76
288 2024-07-08 53531.759999999995 38242.0 91773.76
289 2024-07-09 52891.759999999995 40390.0 93281.76
290 2024-07-10 51451.719999999994 41648.04 93099.76
291 2024-07-11 51451.719999999994 42945.86 94397.57999999999
292 2024-07-12 51451.719999999994 42662.64 94114.35999999999
293 2024-07-15 52778.87999999999 40392.92 93171.79999999999
294 2024-07-16 52778.87999999999 41218.92 93997.79999999999
295 2024-07-17 57090.87999999999 36641.42 93732.29999999999
296 2024-07-18 55290.87999999999 38284.96 93575.84
297 2024-07-19 55290.87999999999 38665.44 93956.31999999999
298 2024-07-22 60996.87999999999 34089.5 95086.37999999999
299 2024-07-23 60996.87999999999 33558.479999999996 94555.35999999999
300 2024-07-24 62796.87999999999 31740.760000000002 94537.63999999998
301 2024-07-25 61256.87999999999 33841.24 95098.12
302 2024-07-26 61456.87999999999 33980.7 95437.57999999999
303 2024-07-29 59777.73999999999 35923.119999999995 95700.85999999999
304 2024-07-30 61777.73999999999 34529.72 96307.45999999999
305 2024-07-31 64101.73999999999 33334.0 97435.73999999999
306 2024-08-01 65125.73999999999 32018.0 97143.73999999999
307 2024-08-02 61925.73999999999 34468.0 96393.73999999999
308 2024-08-05 62605.73999999999 32892.0 95497.73999999999
309 2024-08-06 62605.73999999999 34194.0 96799.73999999999
310 2024-08-07 64405.73999999999 32874.0 97279.73999999999
311 2024-08-08 64605.73999999999 31938.0 96543.73999999999
312 2024-08-09 63005.73999999999 33154.0 96159.73999999999
313 2024-08-12 61315.73999999999 34852.0 96167.73999999999
314 2024-08-13 59515.73999999999 36410.0 95925.73999999999
315 2024-08-14 59515.73999999999 35968.0 95483.73999999999
316 2024-08-15 58515.73999999999 37140.0 95655.73999999999
317 2024-08-16 56715.73999999999 39058.0 95773.73999999999
318 2024-08-19 56764.19999999999 38072.64 94836.84
319 2024-08-20 56764.19999999999 38040.52 94804.71999999999
320 2024-08-21 55164.19999999999 38808.92 93973.12
321 2024-08-22 53564.19999999999 39192.04 92756.23999999999
322 2024-08-23 55364.19999999999 37672.44 93036.63999999998
323 2024-08-26 56940.25999999999 36384.899999999994 93325.15999999997
324 2024-08-27 57540.25999999999 35327.28 92867.53999999998
325 2024-08-28 52940.25999999999 39837.560000000005 92777.81999999999
326 2024-08-29 51892.25999999999 41458.62 93350.87999999999
327 2024-08-30 52183.51999999999 42771.06 94954.57999999999
328 2024-09-02 56710.33999999999 37626.06 94336.4
329 2024-09-03 58710.33999999999 36281.9 94992.23999999999
330 2024-09-04 58710.33999999999 35629.16 94339.5
331 2024-09-05 58710.33999999999 36421.92 95132.25999999998
332 2024-09-06 58710.33999999999 35878.58 94588.91999999998
333 2024-09-09 58906.33999999999 35322.54000000001 94228.88
334 2024-09-10 57306.33999999999 37236.6 94542.93999999999
335 2024-09-11 57306.33999999999 36978.36 94284.69999999998
336 2024-09-12 62642.33999999999 31378.359999999997 94020.69999999998
337 2024-09-13 62642.33999999999 30741.5 93383.84
338 2024-09-18 58008.74 34825.06 92833.79999999999
339 2024-09-19 58359.32 35364.84 93724.16
340 2024-09-20 58359.32 34882.94 93242.26000000001
341 2024-09-23 59825.32 33529.54 93354.86
342 2024-09-24 59825.32 35009.0 94834.32
343 2024-09-25 62217.32 33031.08 95248.4
344 2024-09-26 62723.32000000001 33992.1 96715.42000000001
345 2024-09-27 69235.32 29121.08 98356.40000000001
346 2024-09-30 80349.32 21369.96 101719.28
347 2024-10-08 85077.28 19476.0 104553.28
348 2024-10-09 77483.28 25692.0 103175.28
349 2024-10-10 65483.28 35852.0 101335.28
350 2024-10-11 56083.28 43112.0 99195.28
351 2024-10-14 54845.28 47550.0 102395.28
352 2024-10-15 62245.28 40032.0 102277.28
353 2024-10-16 64429.28 38264.0 102693.28
354 2024-10-17 71101.28 33832.0 104933.28
355 2024-10-18 71101.28 35670.0 106771.28
356 2024-10-21 81899.28 27310.0 109209.28
357 2024-10-22 78499.28 29890.0 108389.28
358 2024-10-23 79145.28 31210.0 110355.28
359 2024-10-24 83457.28 27304.0 110761.28
360 2024-10-25 87055.28 25324.0 112379.28
361 2024-10-28 82369.28 32322.0 114691.28
362 2024-10-29 94723.28 21330.0 116053.28
363 2024-10-30 93926.66 22730.62 116657.28
364 2024-10-31 92874.66 23630.56 116505.22
365 2024-11-01 90986.66 26929.52 117916.18000000001
366 2024-11-04 88286.66 31429.6 119716.26000000001
367 2024-11-05 99332.66 22827.28 122159.94
368 2024-11-06 100274.82 22557.2 122832.02
369 2024-11-07 97836.82 26869.02 124705.84000000001
370 2024-11-08 100636.82 23667.66 124304.48000000001
371 2024-11-11 97436.82 27560.78 124997.6
372 2024-11-12 97436.82 27386.06 124822.88
373 2024-11-13 95636.82 29058.3 124695.12000000001
374 2024-11-14 97636.82 26820.22 124457.04000000001
375 2024-11-15 96557.20000000001 28499.719999999998 125056.92000000001
376 2024-11-18 99403.46 26804.239999999998 126207.70000000001
377 2024-11-19 87403.46 38321.020000000004 125724.48000000001
378 2024-11-20 91665.46 36476.68 128142.14000000001
379 2024-11-21 97525.46 31670.04 129195.5
380 2024-11-22 102164.48000000001 26932.02 129096.50000000001
381 2024-11-25 100326.48000000001 29415.52 129742.00000000001
382 2024-11-26 98726.48000000001 30748.68 129475.16
383 2024-11-27 97126.48000000001 32619.879999999997 129746.36000000002
384 2024-11-28 101950.1 29081.36 131031.46
385 2024-11-29 101950.1 29600.280000000002 131550.38
386 2024-12-02 108792.78 23990.58 132783.36
387 2024-12-03 105192.78 27452.98 132645.76
388 2024-12-04 103392.78 27856.059999999998 131248.84
389 2024-12-05 103392.78 28402.48 131795.26
390 2024-12-06 103392.78 28411.32 131804.1
391 2024-12-09 97964.12 32072.58 130036.7
392 2024-12-10 96164.12 34057.399999999994 130221.51999999999
393 2024-12-11 94564.12 35279.64 129843.76
394 2024-12-12 94844.12 35000.0 129844.12
395 2024-12-13 96844.12 33198.0 130042.12
396 2024-12-16 92244.12 37306.0 129550.12
397 2024-12-17 90444.12 36824.0 127268.12
398 2024-12-18 85644.12 41096.0 126740.12
399 2024-12-19 82444.12 45412.0 127856.12
400 2024-12-20 82444.12 44764.0 127208.12
401 2024-12-23 86146.12 38902.0 125048.12
402 2024-12-24 88234.12 37016.0 125250.12
403 2024-12-25 83634.12 39390.0 123024.12
404 2024-12-26 85918.12 38396.0 124314.12
405 2024-12-27 89318.12 35982.0 125300.12
406 2024-12-30 84450.12 39132.0 123582.12
407 2024-12-31 86250.12 38410.0 124660.12
408 2025-01-02 85608.12 39822.0 125430.12
409 2025-01-03 87408.12 37578.0 124986.12
410 2025-01-06 87180.12 36526.0 123706.12
411 2025-01-07 85580.12 39196.0 124776.12
412 2025-01-08 85580.12 39276.0 124856.12
413 2025-01-09 85580.12 40108.0 125688.12
414 2025-01-10 85580.12 38096.0 123676.12
415 2025-01-13 86472.12 36632.0 123104.12
416 2025-01-14 90534.12 35542.0 126076.12
417 2025-01-15 92964.12 33396.0 126360.12
418 2025-01-16 92964.12 33474.0 126438.12
419 2025-01-17 92964.12 32718.0 125682.12
420 2025-01-20 92444.12 33334.0 125778.12
421 2025-01-21 90644.12 34562.0 125206.12
422 2025-01-22 88844.12 35200.0 124044.12
423 2025-01-23 92964.12 31104.0 124068.12
424 2025-01-24 92964.12 30942.0 123906.12
425 2025-01-27 93264.12 29920.0 123184.12
426 2025-02-05 95550.12 28902.0 124452.12
427 2025-02-06 95386.12 30334.0 125720.12
428 2025-02-07 99682.12 27098.0 126780.12
429 2025-02-10 101682.12 26282.0 127964.12
430 2025-02-11 102255.36 25700.760000000002 127956.12
431 2025-02-12 106503.36 22042.74 128546.1
432 2025-02-13 106503.36 21696.8 128200.16
433 2025-02-14 106503.36 21418.78 127922.14
434 2025-02-17 106503.36 22392.739999999998 128896.1
435 2025-02-18 106863.36 21594.6 128457.95999999999
436 2025-02-19 107549.72 21399.64 128949.36
437 2025-02-20 105749.72 23389.28 129139.0
438 2025-02-21 105749.72 23782.4 129532.12
439 2025-02-24 107749.72 22060.94 129810.66
440 2025-02-25 106050.72 23940.18 129990.9
441 2025-02-26 106050.72 24078.079999999998 130128.8
442 2025-02-27 104528.72 25574.48 130103.2
443 2025-02-28 102728.72 26542.96 129271.68
444 2025-03-03 99240.72 30762.88 130003.6
445 2025-03-04 104273.36 27485.84 131759.2
446 2025-03-05 106519.36 25571.420000000002 132090.78
447 2025-03-06 103171.36 29194.32 132365.68
448 2025-03-07 105425.36 27040.4 132465.76
449 2025-03-10 105425.36 27684.64 133110.0
450 2025-03-11 105757.36 27476.72 133234.08000000002
451 2025-03-12 107757.36 26166.48 133923.84
452 2025-03-13 110301.82 23848.38 134150.2
453 2025-03-14 115013.82 19782.36 134796.18
454 2025-03-17 115373.82 19713.06 135086.88
455 2025-03-18 116287.82 19132.559999999998 135420.38
456 2025-03-19 112687.82 22446.72 135134.54
457 2025-03-20 111223.82 24020.72 135244.54
458 2025-03-21 109423.82 25211.120000000003 134634.94
459 2025-03-24 102104.94 32562.0 134666.94
460 2025-03-25 104460.1 30420.84 134880.94
461 2025-03-26 106460.1 29221.84 135681.94
462 2025-03-27 106660.1 28888.8 135548.9
463 2025-03-28 103822.1 31484.3 135306.4
464 2025-03-31 100434.62000000001 34067.08 134501.7
465 2025-04-01 101400.62000000001 32980.64 134381.26
466 2025-04-02 101600.62000000001 32563.239999999998 134163.86000000002
467 2025-04-03 99800.62000000001 34518.32 134318.94
468 2025-04-07 87000.62000000001 41837.2 128837.82
469 2025-04-08 80800.62000000001 49853.04 130653.66
470 2025-04-09 88600.62000000001 46010.08 134610.7
471 2025-04-10 98000.62000000001 38655.6 136656.22
472 2025-04-11 99600.62000000001 37063.76 136664.38
473 2025-04-14 106254.62000000001 31782.72 138037.34000000003
474 2025-04-15 104654.62000000001 33317.28 137971.90000000002
475 2025-04-16 106747.54000000001 31677.72 138425.26
476 2025-04-17 108747.54000000001 30180.46 138928.0
477 2025-04-18 109341.54000000001 30561.36 139902.90000000002
478 2025-04-21 107815.54000000001 32425.2 140240.74000000002
479 2025-04-22 107815.54000000001 31336.8 139152.34
480 2025-04-23 109415.54000000001 30456.64 139872.18
481 2025-04-24 109657.54000000001 29465.66 139123.2
482 2025-04-25 106835.54000000001 32281.18 139116.72
483 2025-04-28 106345.54000000001 32082.7 138428.24000000002
484 2025-04-29 102745.54000000001 35483.0 138228.54
485 2025-04-30 102745.54000000001 35759.3 138504.84000000003
486 2025-05-06 104510.84000000001 35040.0 139550.84000000003
487 2025-05-07 107283.84000000001 32649.0 139932.84000000003
488 2025-05-08 104437.84000000001 35792.24 140230.08000000002
489 2025-05-09 102637.84000000001 37585.88 140223.72
490 2025-05-12 103367.84000000001 37894.72 141262.56
491 2025-05-13 103367.84000000001 37302.54 140670.38
492 2025-05-14 103763.84000000001 36662.0 140425.84000000003
493 2025-05-15 101963.84000000001 38680.0 140643.84000000003
494 2025-05-16 101963.84000000001 38900.0 140863.84000000003
495 2025-05-19 101963.84000000001 39840.0 141803.84000000003
496 2025-05-20 105763.84000000001 36696.0 142459.84000000003
497 2025-05-21 107363.84000000001 35314.0 142677.84000000003
498 2025-05-22 109423.84000000001 33250.0 142673.84000000003
499 2025-05-23 109423.84000000001 32758.0 142181.84000000003
500 2025-05-26 110715.84000000001 32416.0 143131.84000000003
501 2025-05-27 114765.84000000001 28756.0 143521.84000000003
502 2025-05-28 118565.84000000001 24386.0 142951.84000000003
503 2025-05-29 118565.84000000001 24558.0 143123.84000000003
504 2025-05-30 115165.84000000001 27606.0 142771.84000000003
505 2025-06-03 113691.88 29479.86 143171.74
506 2025-06-04 117691.88 26375.22 144067.1
507 2025-06-05 118367.88 25461.82 143829.7
508 2025-06-06 118567.88 25523.98 144091.86000000002
509 2025-06-09 118468.06 26145.58 144613.64
510 2025-06-10 119242.06 26013.68 145255.74
511 2025-06-11 115642.06 29543.460000000003 145185.52
512 2025-06-12 119442.06 26731.8 146173.86
513 2025-06-13 121827.45999999999 23271.14 145098.59999999998
514 2025-06-16 118827.45999999999 26558.000000000004 145385.46
515 2025-06-17 119851.45999999999 25974.019999999997 145825.47999999998
516 2025-06-18 118051.45999999999 27032.4 145083.86
517 2025-06-19 112651.45999999999 31251.42 143902.88
518 2025-06-20 109651.45999999999 33717.020000000004 143368.47999999998
519 2025-06-23 108051.45999999999 36004.66 144056.12
520 2025-06-24 108051.45999999999 36450.659999999996 144502.12
521 2025-06-25 108567.45999999999 36190.72 144758.18
522 2025-06-26 108767.45999999999 36286.82 145054.28
523 2025-06-27 108767.45999999999 36246.7 145014.15999999997
524 2025-06-30 108321.45999999999 37252.479999999996 145573.94
525 2025-07-01 115200.18 31736.68 146936.86
526 2025-07-02 117302.28 30046.74 147349.02
527 2025-07-03 117302.28 29892.84 147195.12
528 2025-07-04 117302.28 29383.94 146686.22
529 2025-07-07 117586.28 29861.88 147448.16
530 2025-07-08 117586.28 29913.9 147500.18
531 2025-07-09 117912.28 29733.94 147646.22
532 2025-07-10 118112.28 29558.0 147670.28
533 2025-07-11 116312.28 31418.0 147730.28
534 2025-07-14 114316.28 33108.0 147424.28
535 2025-07-15 112716.28 34186.0 146902.28
536 2025-07-16 114716.28 32426.0 147142.28
537 2025-07-17 114960.2 32414.08 147374.28
538 2025-07-18 113160.2 33946.38 147106.58
539 2025-07-21 116704.2 30680.16 147384.36
540 2025-07-22 116980.59999999999 30577.559999999998 147558.15999999997
541 2025-07-23 116980.59999999999 30195.579999999998 147176.18
542 2025-07-24 115180.59999999999 32133.800000000003 147314.4
543 2025-07-25 115180.59999999999 32153.94 147334.53999999998
544 2025-07-28 115180.59999999999 32243.62 147424.22
545 2025-07-29 115180.59999999999 31921.92 147102.52
546 2025-07-30 115180.59999999999 31987.88 147168.47999999998
547 2025-07-31 113580.59999999999 33084.16 146664.76
548 2025-08-01 113316.59999999999 33357.8 146674.4
549 2025-08-04 116562.59999999999 30359.96 146922.56
550 2025-08-05 114762.59999999999 32258.28 147020.88
551 2025-08-06 114762.59999999999 32168.1 146930.69999999998
552 2025-08-07 114762.59999999999 32379.9 147142.5
553 2025-08-08 119086.59999999999 28390.0 147476.59999999998
554 2025-08-11 119218.59999999999 28752.0 147970.59999999998
555 2025-08-12 117418.59999999999 30502.0 147920.59999999998
556 2025-08-13 121062.59999999999 26722.0 147784.59999999998
557 2025-08-14 117662.59999999999 29492.0 147154.59999999998
558 2025-08-15 119662.59999999999 28798.0 148460.59999999998
559 2025-08-18 123252.59999999999 26458.0 149710.59999999998
560 2025-08-19 122090.59999999999 27948.0 150038.59999999998
561 2025-08-20 120290.59999999999 29984.0 150274.59999999998
562 2025-08-21 126290.59999999999 24298.0 150588.59999999998
563 2025-08-22 126652.59999999999 23932.0 150584.59999999998
564 2025-08-25 126652.59999999999 24162.0 150814.59999999998
565 2025-08-26 126852.59999999999 24104.0 150956.59999999998
566 2025-08-27 123646.59999999999 26804.0 150450.59999999998
567 2025-08-28 118640.59999999999 32296.0 150936.59999999998
568 2025-08-29 116840.59999999999 33900.0 150740.59999999998
569 2025-09-01 122140.01999999999 29354.24 151494.25999999998
570 2025-09-02 120726.01999999999 30304.0 151030.02
571 2025-09-03 117326.01999999999 32868.0 150194.02
572 2025-09-04 114126.01999999999 35582.0 149708.02
573 2025-09-05 114126.01999999999 36400.0 150526.02
574 2025-09-08 114126.01999999999 36670.0 150796.02
575 2025-09-09 114126.01999999999 35764.0 149890.02
576 2025-09-10 114126.01999999999 36264.0 150390.02
577 2025-09-11 116326.01999999999 34852.0 151178.02
578 2025-09-12 116580.01999999999 34638.0 151218.02
579 2025-09-15 116840.01999999999 34224.0 151064.02
580 2025-09-16 116840.01999999999 34266.0 151106.02
581 2025-09-17 116840.01999999999 34016.0 150856.02
582 2025-09-18 115522.01999999999 34918.0 150440.02
583 2025-09-19 115522.01999999999 34548.0 150070.02
584 2025-09-22 116524.01999999999 33574.0 150098.02
585 2025-09-23 111924.01999999999 37476.0 149400.02
586 2025-09-24 112264.01999999999 37360.0 149624.02
587 2025-09-25 112264.01999999999 36962.0 149226.02
588 2025-09-26 112264.01999999999 36744.0 149008.02
589 2025-09-29 116726.01999999999 32582.0 149308.02
590 2025-09-30 116726.01999999999 32560.0 149286.02
591 2025-10-09 118370.01999999999 30188.0 148558.02
592 2025-10-10 116570.01999999999 31992.0 148562.02
593 2025-10-13 112600.01999999999 36462.0 149062.02
594 2025-10-14 112600.01999999999 36110.0 148710.02
595 2025-10-15 112600.01999999999 36570.0 149170.02
596 2025-10-16 114600.01999999999 34026.0 148626.02
597 2025-10-17 113000.01999999999 34894.0 147894.02
598 2025-10-20 112998.01999999999 35922.0 148920.02
599 2025-10-21 114998.01999999999 34854.0 149852.02
600 2025-10-22 114998.01999999999 34606.0 149604.02
601 2025-10-23 113198.01999999999 36532.0 149730.02
602 2025-10-24 113198.01999999999 36844.0 150042.02
603 2025-10-27 116430.01999999999 33332.0 149762.02
604 2025-10-28 116430.01999999999 33280.0 149710.02
605 2025-10-29 116430.01999999999 33934.0 150364.02
606 2025-10-30 116430.01999999999 33806.0 150236.02
607 2025-10-31 116430.01999999999 34148.0 150578.02
608 2025-11-03 121148.01999999999 29008.0 150156.02
609 2025-11-04 120108.01999999999 30110.0 150218.02
610 2025-11-05 120108.01999999999 30552.0 150660.02
611 2025-11-06 118308.01999999999 32286.0 150594.02
612 2025-11-07 118702.01999999999 31342.0 150044.02
613 2025-11-10 118702.01999999999 31688.0 150390.02
614 2025-11-11 123102.01999999999 27790.0 150892.02
615 2025-11-12 118426.01999999999 32408.0 150834.02
616 2025-11-13 119016.01999999999 32752.0 151768.02
617 2025-11-14 119016.01999999999 33214.0 152230.02
618 2025-11-17 123270.01999999999 29114.0 152384.02
619 2025-11-18 119670.01999999999 31876.0 151546.02
620 2025-11-19 118070.01999999999 33268.0 151338.02
621 2025-11-20 116470.01999999999 34108.0 150578.02
622 2025-11-21 109870.01999999999 38562.0 148432.02
623 2025-11-24 109938.01999999999 38512.0 148450.02
624 2025-11-25 111938.01999999999 37762.0 149700.02
625 2025-11-26 111938.01999999999 37170.0 149108.02
626 2025-11-27 111938.01999999999 37172.0 149110.02
627 2025-11-28 111938.01999999999 37834.0 149772.02
628 2025-12-01 115825.68 33942.38 149768.06
629 2025-12-02 115825.68 33994.38 149820.06
630 2025-12-03 117825.68 31736.9 149562.58
631 2025-12-04 112425.68 36213.44 148639.12
632 2025-12-05 109425.68 40135.04 149560.72
633 2025-12-08 112603.68 37374.88 149978.56
634 2025-12-09 116729.68 33392.68 150122.36
635 2025-12-10 117313.68 33230.36 150544.03999999998
636 2025-12-11 115769.68 34872.0 150641.68
637 2025-12-12 115769.68 35290.0 151059.68
638 2025-12-15 118523.68 33424.0 151947.68
639 2025-12-16 118723.68 32706.0 151429.68
640 2025-12-17 111723.68 39646.0 151369.68
641 2025-12-18 111723.68 39308.0 151031.68
642 2025-12-19 110123.68 41948.0 152071.68
643 2025-12-22 116025.68 36470.0 152495.68
644 2025-12-23 116225.68 36004.0 152229.68
645 2025-12-24 115195.68 37596.0 152791.68
646 2025-12-25 115571.68 37138.0 152709.68
647 2025-12-26 115371.68 37724.0 153095.68
648 2025-12-29 118263.68 34576.0 152839.68
649 2025-12-30 116959.68 35302.0 152261.68
650 2025-12-31 115159.68 36376.0 151535.68
651 2026-01-05 114658.76 36630.8 151289.56
652 2026-01-06 113458.76 38219.44 151678.2
653 2026-01-07 113854.76 37808.0 151662.76
654 2026-01-08 112054.76 40598.0 152652.76
655 2026-01-09 113854.76 39384.0 153238.76
656 2026-01-12 114728.76 38852.0 153580.76
657 2026-01-13 120616.76 32848.0 153464.76
658 2026-01-14 121016.76 32938.0 153954.76
659 2026-01-15 121216.76 32146.0 153362.76
660 2026-01-16 117816.76 34904.0 152720.76
661 2026-01-19 118854.76 34260.0 153114.76
662 2026-01-20 117516.76 35734.0 153250.76
663 2026-01-21 120376.76 34212.0 154588.76
664 2026-01-22 120376.76 34816.0 155192.76
665 2026-01-23 120376.76 35548.0 155924.76
666 2026-01-26 119168.76 36470.0 155638.76
667 2026-01-27 119368.76 36470.0 155838.76
668 2026-01-28 119368.76 35508.0 154876.76
669 2026-01-29 117768.76 37012.0 154780.76
670 2026-01-30 118104.76 36686.0 154790.76
671 2026-02-02 123830.76 30870.0 154700.76
672 2026-02-03 123830.76 31876.000000000004 155706.76
673 2026-02-04 123830.76 31492.0 155322.76
674 2026-02-05 123830.76 31724.0 155554.76
675 2026-02-06 123830.76 32184.0 156014.76
676 2026-02-09 124128.76 32940.0 157068.76
677 2026-02-10 130252.76000000001 27476.0 157728.76
678 2026-02-11 128452.76000000001 29178.0 157630.76
679 2026-02-12 130452.76000000001 27600.0 158052.76
680 2026-02-13 130652.76000000001 27454.0 158106.76
681 2026-02-24 129478.76000000001 29166.0 158644.76
682 2026-02-25 129678.76000000001 29674.0 159352.76
683 2026-02-26 129878.76000000001 29368.0 159246.76
684 2026-02-27 128078.76000000001 31448.0 159526.76
685 2026-03-02 126568.76000000001 31928.0 158496.76
686 2026-03-03 123168.76000000001 33314.0 156482.76
687 2026-03-04 121530.76000000001 35378.0 156908.76
688 2026-03-05 123530.76000000001 33996.0 157526.76
689 2026-03-06 121930.76000000001 35786.0 157716.76
690 2026-03-09 120130.76000000001 37128.0 157258.76
691 2026-03-10 122130.76000000001 36208.0 158338.76
692 2026-03-11 122498.76000000001 35428.0 157926.76
693 2026-03-12 122498.76000000001 34782.0 157280.76
694 2026-03-13 122818.76000000001 33916.0 156734.76
695 2026-03-16 119866.98000000001 36893.56 156760.54
696 2026-03-17 120266.98000000001 35880.16 156147.14
697 2026-03-18 120266.98000000001 36240.88 156507.86000000002
698 2026-03-19 115666.98000000001 39534.56 155201.54
699 2026-03-20 114066.98000000001 39770.48 153837.46000000002
700 2026-03-23 112544.98000000001 40740.8 153285.78000000003
701 2026-03-24 111144.98000000001 44227.28 155372.26
702 2026-03-25 113144.98000000001 43183.8 156328.78000000003
703 2026-03-26 114944.98000000001 40874.96 155819.94
704 2026-03-27 114944.98000000001 41492.52 156437.5
705 2026-03-30 118332.98000000001 38077.28 156410.26
706 2026-03-31 118332.98000000001 37634.52 155967.5
707 2026-04-01 121430.98000000001 35512.04 156943.02000000002
708 2026-04-02 121430.98000000001 34988.72 156419.7
709 2026-04-03 118420.98000000001 37343.56 155764.54
710 2026-04-07 117714.98000000001 38003.479999999996 155718.46000000002
711 2026-04-08 117980.98000000001 39241.32 157222.30000000002
712 2026-04-09 117980.98000000001 38689.08 156670.06
713 2026-04-10 118180.98000000001 38458.6 156639.58000000002
714 2026-04-13 116651.58000000002 39420.0 156071.58000000002
715 2026-04-14 115051.58000000002 40588.0 155639.58000000002
716 2026-04-15 114451.58000000002 40568.0 155019.58000000002
717 2026-04-16 113051.58000000002 41612.0 154663.58000000002
718 2026-04-17 111851.58000000002 42540.0 154391.58000000002
719 2026-04-20 111939.58000000002 43314.0 155253.58000000002
720 2026-04-21 111939.58000000002 43070.0 155009.58000000002
721 2026-04-22 111939.58000000002 42160.0 154099.58000000002
722 2026-04-23 110939.58000000002 41660.0 152599.58000000002
723 2026-04-24 109899.58000000002 42320.0 152219.58000000002
724 2026-04-27 113003.58000000002 39480.0 152483.58000000002
725 2026-04-28 112317.58000000002 38134.0 150451.58000000002
726 2026-04-29 113317.58000000002 37898.0 151215.58000000002
727 2026-04-30 111717.58000000002 39436.0 151153.58000000002
-37
View File
@@ -1,37 +0,0 @@
date,cash,stock_value,total_value,positions,month
2023-05-31,17736.34,41098.4,58834.74,10,2023-05
2023-06-30,20515.46,40323.66,60839.12,10,2023-06
2023-07-31,30397.84,29960.78,60358.62,9,2023-07
2023-08-31,26136.7,34300.62,60437.32,10,2023-08
2023-09-30,39873.34,27709.4,67582.74,10,2023-09
2023-10-31,36088.52,31940.92,68029.44,10,2023-10
2023-11-30,49700.74,27047.76,76748.5,10,2023-11
2023-12-31,56387.94,33956.0,90343.94,10,2023-12
2024-01-31,43543.94,44782.0,88325.94,10,2024-01
2024-02-29,41861.28,44551.96,86413.24,10,2024-02
2024-03-31,60401.36,30781.6,91182.96,9,2024-03
2024-04-30,51342.72,39697.38,91040.1,10,2024-04
2024-05-31,65813.5,29130.0,94943.5,9,2024-05
2024-06-30,52527.76,40070.0,92597.76,10,2024-06
2024-07-31,61456.88,33980.7,95437.58,10,2024-07
2024-08-31,52183.52,42771.06,94954.58,10,2024-08
2024-09-30,80349.32,21369.96,101719.28,10,2024-09
2024-10-31,87055.28,25324.0,112379.28,10,2024-10
2024-11-30,101950.1,29600.28,131550.38,10,2024-11
2024-12-31,89318.12,35982.0,125300.12,8,2024-12
2025-01-31,95062.12,29920.0,124982.12,9,2025-01
2025-02-28,102728.72,26542.96,129271.68,9,2025-02
2025-03-31,103822.1,31484.3,135306.4,10,2025-03
2025-04-30,102745.54,35759.3,138504.84,10,2025-04
2025-05-31,115165.84,27606.0,142771.84,8,2025-05
2025-06-30,108767.46,36246.7,145014.16,9,2025-06
2025-07-31,115180.6,32153.94,147334.54,10,2025-07
2025-08-31,116840.6,33900.0,150740.6,10,2025-08
2025-09-30,116726.02,32560.0,149286.02,10,2025-09
2025-10-31,116430.02,34148.0,150578.02,10,2025-10
2025-11-30,111938.02,37834.0,149772.02,10,2025-11
2025-12-31,115159.68,36376.0,151535.68,10,2025-12
2026-01-31,118104.76,36686.0,154790.76,10,2026-01
2026-02-28,128078.76,31448.0,159526.76,10,2026-02
2026-03-31,114944.98,41492.52,156437.5,10,2026-03
2026-04-30,111717.58,39436.0,151153.58,10,2026-04
1 date cash stock_value total_value positions month
2 2023-05-31 17736.34 41098.4 58834.74 10 2023-05
3 2023-06-30 20515.46 40323.66 60839.12 10 2023-06
4 2023-07-31 30397.84 29960.78 60358.62 9 2023-07
5 2023-08-31 26136.7 34300.62 60437.32 10 2023-08
6 2023-09-30 39873.34 27709.4 67582.74 10 2023-09
7 2023-10-31 36088.52 31940.92 68029.44 10 2023-10
8 2023-11-30 49700.74 27047.76 76748.5 10 2023-11
9 2023-12-31 56387.94 33956.0 90343.94 10 2023-12
10 2024-01-31 43543.94 44782.0 88325.94 10 2024-01
11 2024-02-29 41861.28 44551.96 86413.24 10 2024-02
12 2024-03-31 60401.36 30781.6 91182.96 9 2024-03
13 2024-04-30 51342.72 39697.38 91040.1 10 2024-04
14 2024-05-31 65813.5 29130.0 94943.5 9 2024-05
15 2024-06-30 52527.76 40070.0 92597.76 10 2024-06
16 2024-07-31 61456.88 33980.7 95437.58 10 2024-07
17 2024-08-31 52183.52 42771.06 94954.58 10 2024-08
18 2024-09-30 80349.32 21369.96 101719.28 10 2024-09
19 2024-10-31 87055.28 25324.0 112379.28 10 2024-10
20 2024-11-30 101950.1 29600.28 131550.38 10 2024-11
21 2024-12-31 89318.12 35982.0 125300.12 8 2024-12
22 2025-01-31 95062.12 29920.0 124982.12 9 2025-01
23 2025-02-28 102728.72 26542.96 129271.68 9 2025-02
24 2025-03-31 103822.1 31484.3 135306.4 10 2025-03
25 2025-04-30 102745.54 35759.3 138504.84 10 2025-04
26 2025-05-31 115165.84 27606.0 142771.84 8 2025-05
27 2025-06-30 108767.46 36246.7 145014.16 9 2025-06
28 2025-07-31 115180.6 32153.94 147334.54 10 2025-07
29 2025-08-31 116840.6 33900.0 150740.6 10 2025-08
30 2025-09-30 116726.02 32560.0 149286.02 10 2025-09
31 2025-10-31 116430.02 34148.0 150578.02 10 2025-10
32 2025-11-30 111938.02 37834.0 149772.02 10 2025-11
33 2025-12-31 115159.68 36376.0 151535.68 10 2025-12
34 2026-01-31 118104.76 36686.0 154790.76 10 2026-01
35 2026-02-28 128078.76 31448.0 159526.76 10 2026-02
36 2026-03-31 114944.98 41492.52 156437.5 10 2026-03
37 2026-04-30 111717.58 39436.0 151153.58 10 2026-04
-21
View File
@@ -1,21 +0,0 @@
{
"version": "v6.7r2",
"rank_model": "lambdarank (56-dim v6.7)",
"backtest_start": "2023-05-01",
"backtest_end": "2026-04-30",
"n_weeks": 154,
"n_stocks_in_pool": 5378,
"top_n": 10,
"shares_per_grid": 200,
"initial_cash": 60000.0,
"final_total_value": 151153.58,
"total_return_pct": 151.92,
"annual_return_pct": 36.1,
"annual_sharpe": 1.7404,
"max_drawdown_pct": -12.1,
"weekly_win_rate_pct": 59.48,
"n_trades_buy": 1144,
"n_trades_sell": 866,
"realized_pnl_total": 95495.58,
"n_lifecycles": 431
}
File diff suppressed because it is too large Load Diff
-155
View File
@@ -1,155 +0,0 @@
date,cash,stock_value,total_value,positions,year
2023-05-05,29353.86,32311.96,61665.82,10,2023
2023-05-12,24353.86,37200.04,61553.9,10,2023
2023-05-19,19999.86,40128.84,60128.7,10,2023
2023-05-26,17736.34,41098.4,58834.74,10,2023
2023-06-02,22233.92,40253.44,62487.36,10,2023
2023-06-09,23083.9,39432.4,62516.3,10,2023
2023-06-16,27171.9,36662.04,63833.94,10,2023
2023-06-21,27177.36,35306.82,62484.18,10,2023
2023-06-30,20515.46,40323.66,60839.12,10,2023
2023-07-07,27155.76,33151.0,60306.76,10,2023
2023-07-14,29091.76,31705.8,60797.56,10,2023
2023-07-21,29114.82,32509.1,61623.92,9,2023
2023-07-28,30397.84,29960.78,60358.62,9,2023
2023-08-04,31326.78,29340.22,60667.0,10,2023
2023-08-11,28602.46,31713.8,60316.26,10,2023
2023-08-18,33136.7,28257.34,61394.04,10,2023
2023-08-25,26136.7,34300.62,60437.32,10,2023
2023-09-01,33946.78,29311.82,63258.6,10,2023
2023-09-08,29682.92,33243.98,62926.9,10,2023
2023-09-15,33929.1,29926.24,63855.34,10,2023
2023-09-22,35117.34,29932.44,65049.78,10,2023
2023-09-28,39873.34,27709.4,67582.74,10,2023
2023-10-13,44083.08,23683.52,67766.6,10,2023
2023-10-20,33289.08,32655.7,65944.78,10,2023
2023-10-27,36088.52,31940.92,68029.44,10,2023
2023-11-03,40429.7,29377.8,69807.5,10,2023
2023-11-10,46479.74,25900.26,72380.0,10,2023
2023-11-17,49341.34,24523.56,73864.9,10,2023
2023-11-24,49700.74,27047.76,76748.5,10,2023
2023-12-01,33892.34,39456.38,73348.72,10,2023
2023-12-08,50831.34,30194.3,81025.64,10,2023
2023-12-15,52387.94,31884.0,84271.94,10,2023
2023-12-22,52909.94,32946.0,85855.94,10,2023
2023-12-29,56387.94,33956.0,90343.94,10,2023
2024-01-05,56405.94,34602.0,91007.94,10,2024
2024-01-12,42405.94,42438.0,84843.94,10,2024
2024-01-19,44405.94,46458.0,90863.94,10,2024
2024-01-26,43543.94,44782.0,88325.94,10,2024
2024-02-02,27305.94,52690.0,79995.94,10,2024
2024-02-08,31419.94,51926.0,83345.94,10,2024
2024-02-23,41861.28,44551.96,86413.24,10,2024
2024-03-01,49575.3,40919.28,90494.58,10,2024
2024-03-08,53677.94,35737.8,89415.74,10,2024
2024-03-15,60047.92,31026.78,91074.7,10,2024
2024-03-22,65873.84,26076.62,91950.46,10,2024
2024-03-29,60401.36,30781.6,91182.96,9,2024
2024-04-03,66965.58,26641.44,93607.02,9,2024
2024-04-12,56903.58,32507.18,89410.76,10,2024
2024-04-19,33499.58,52126.22,85625.8,10,2024
2024-04-26,42761.58,46693.66,89455.24,10,2024
2024-04-30,51342.72,39697.38,91040.1,10,2024
2024-05-10,56205.06,34632.66,90837.72,10,2024
2024-05-17,60869.06,31647.16,92516.22,10,2024
2024-05-24,59605.06,32880.44,92485.5,10,2024
2024-05-31,65813.5,29130.0,94943.5,9,2024
2024-06-07,47960.4,42130.56,90090.96,10,2024
2024-06-14,56152.4,36035.36,92187.76,10,2024
2024-06-21,62239.76,31984.0,94223.76,10,2024
2024-06-28,52527.76,40070.0,92597.76,10,2024
2024-07-05,52351.76,40466.0,92817.76,10,2024
2024-07-12,51451.72,42662.64,94114.36,10,2024
2024-07-19,55290.88,38665.44,93956.32,10,2024
2024-07-26,61456.88,33980.7,95437.58,10,2024
2024-08-02,61925.74,34468.0,96393.74,10,2024
2024-08-09,63005.74,33154.0,96159.74,10,2024
2024-08-16,56715.74,39058.0,95773.74,10,2024
2024-08-23,55364.2,37672.44,93036.64,10,2024
2024-08-30,52183.52,42771.06,94954.58,10,2024
2024-09-06,60506.34,35878.58,96384.92,9,2024
2024-09-13,62642.34,30741.5,93383.84,8,2024
2024-09-20,58359.32,34882.94,93242.26,10,2024
2024-09-27,69235.32,29121.08,98356.4,10,2024
2024-09-30,80349.32,21369.96,101719.28,10,2024
2024-10-11,56083.28,43112.0,99195.28,10,2024
2024-10-18,71101.28,35670.0,106771.28,10,2024
2024-10-25,87055.28,25324.0,112379.28,10,2024
2024-11-01,90986.66,26929.52,117916.18,10,2024
2024-11-08,100636.82,23667.66,124304.48,10,2024
2024-11-15,96557.2,28499.72,125056.92,10,2024
2024-11-22,102164.48,26932.02,129096.5,10,2024
2024-11-29,101950.1,29600.28,131550.38,10,2024
2024-12-06,103392.78,28411.32,131804.1,10,2024
2024-12-13,96844.12,33198.0,130042.12,10,2024
2024-12-20,82444.12,44764.0,127208.12,10,2024
2024-12-27,89318.12,35982.0,125300.12,8,2024
2025-01-03,87408.12,37578.0,124986.12,10,2025
2025-01-10,85580.12,38096.0,123676.12,10,2025
2025-01-17,92964.12,32718.0,125682.12,9,2025
2025-01-24,97680.12,30942.0,128622.12,8,2025
2025-01-27,95062.12,29920.0,124982.12,9,2025
2025-02-07,99682.12,27098.0,126780.12,9,2025
2025-02-14,106503.36,21418.78,127922.14,9,2025
2025-02-21,105749.72,23782.4,129532.12,9,2025
2025-02-28,102728.72,26542.96,129271.68,9,2025
2025-03-07,105425.36,27040.4,132465.76,10,2025
2025-03-14,115013.82,19782.36,134796.18,9,2025
2025-03-21,109423.82,25211.12,134634.94,9,2025
2025-03-28,103822.1,31484.3,135306.4,10,2025
2025-04-03,99800.62,34518.32,134318.94,10,2025
2025-04-11,99600.62,37063.76,136664.38,10,2025
2025-04-18,109341.54,30561.36,139902.9,10,2025
2025-04-25,106835.54,32281.18,139116.72,10,2025
2025-04-30,102745.54,35759.3,138504.84,10,2025
2025-05-09,102637.84,37585.88,140223.72,10,2025
2025-05-16,101963.84,38900.0,140863.84,10,2025
2025-05-23,109423.84,32758.0,142181.84,9,2025
2025-05-30,115165.84,27606.0,142771.84,8,2025
2025-06-06,118567.88,25523.98,144091.86,10,2025
2025-06-13,121827.46,23271.14,145098.6,9,2025
2025-06-20,109651.46,33717.02,143368.48,9,2025
2025-06-27,108767.46,36246.7,145014.16,9,2025
2025-07-04,117302.28,29383.94,146686.22,10,2025
2025-07-11,116312.28,31418.0,147730.28,10,2025
2025-07-18,113160.2,33946.38,147106.58,10,2025
2025-07-25,115180.6,32153.94,147334.54,10,2025
2025-08-01,113316.6,33357.8,146674.4,10,2025
2025-08-08,119086.6,28390.0,147476.6,10,2025
2025-08-15,119662.6,28798.0,148460.6,9,2025
2025-08-22,126652.6,23932.0,150584.6,10,2025
2025-08-29,116840.6,33900.0,150740.6,10,2025
2025-09-05,114126.02,36400.0,150526.02,10,2025
2025-09-12,116580.02,34638.0,151218.02,10,2025
2025-09-19,115522.02,34548.0,150070.02,10,2025
2025-09-26,115364.02,36744.0,152108.02,9,2025
2025-09-30,116726.02,32560.0,149286.02,10,2025
2025-10-10,116570.02,31992.0,148562.02,10,2025
2025-10-17,113000.02,34894.0,147894.02,10,2025
2025-10-24,113198.02,36844.0,150042.02,10,2025
2025-10-31,116430.02,34148.0,150578.02,10,2025
2025-11-07,118702.02,31342.0,150044.02,10,2025
2025-11-14,119016.02,33214.0,152230.02,10,2025
2025-11-21,109870.02,38562.0,148432.02,10,2025
2025-11-28,111938.02,37834.0,149772.02,10,2025
2025-12-05,109425.68,40135.04,149560.72,10,2025
2025-12-12,115769.68,35290.0,151059.68,10,2025
2025-12-19,110123.68,41948.0,152071.68,10,2025
2025-12-26,115371.68,37724.0,153095.68,10,2025
2025-12-31,115159.68,36376.0,151535.68,10,2025
2026-01-09,113854.76,39384.0,153238.76,10,2026
2026-01-16,117816.76,34904.0,152720.76,10,2026
2026-01-23,120376.76,35548.0,155924.76,10,2026
2026-01-30,118104.76,36686.0,154790.76,10,2026
2026-02-06,123830.76,32184.0,156014.76,10,2026
2026-02-13,130652.76,27454.0,158106.76,10,2026
2026-02-27,128078.76,31448.0,159526.76,10,2026
2026-03-06,121930.76,35786.0,157716.76,9,2026
2026-03-13,122818.76,33916.0,156734.76,9,2026
2026-03-20,114066.98,39770.48,153837.46,10,2026
2026-03-27,114944.98,41492.52,156437.5,10,2026
2026-04-03,118420.98,37343.56,155764.54,10,2026
2026-04-10,118180.98,38458.6,156639.58,10,2026
2026-04-17,111851.58,42540.0,154391.58,10,2026
2026-04-24,109899.58,42320.0,152219.58,10,2026
2026-04-30,111717.58,39436.0,151153.58,10,2026
1 date cash stock_value total_value positions year
2 2023-05-05 29353.86 32311.96 61665.82 10 2023
3 2023-05-12 24353.86 37200.04 61553.9 10 2023
4 2023-05-19 19999.86 40128.84 60128.7 10 2023
5 2023-05-26 17736.34 41098.4 58834.74 10 2023
6 2023-06-02 22233.92 40253.44 62487.36 10 2023
7 2023-06-09 23083.9 39432.4 62516.3 10 2023
8 2023-06-16 27171.9 36662.04 63833.94 10 2023
9 2023-06-21 27177.36 35306.82 62484.18 10 2023
10 2023-06-30 20515.46 40323.66 60839.12 10 2023
11 2023-07-07 27155.76 33151.0 60306.76 10 2023
12 2023-07-14 29091.76 31705.8 60797.56 10 2023
13 2023-07-21 29114.82 32509.1 61623.92 9 2023
14 2023-07-28 30397.84 29960.78 60358.62 9 2023
15 2023-08-04 31326.78 29340.22 60667.0 10 2023
16 2023-08-11 28602.46 31713.8 60316.26 10 2023
17 2023-08-18 33136.7 28257.34 61394.04 10 2023
18 2023-08-25 26136.7 34300.62 60437.32 10 2023
19 2023-09-01 33946.78 29311.82 63258.6 10 2023
20 2023-09-08 29682.92 33243.98 62926.9 10 2023
21 2023-09-15 33929.1 29926.24 63855.34 10 2023
22 2023-09-22 35117.34 29932.44 65049.78 10 2023
23 2023-09-28 39873.34 27709.4 67582.74 10 2023
24 2023-10-13 44083.08 23683.52 67766.6 10 2023
25 2023-10-20 33289.08 32655.7 65944.78 10 2023
26 2023-10-27 36088.52 31940.92 68029.44 10 2023
27 2023-11-03 40429.7 29377.8 69807.5 10 2023
28 2023-11-10 46479.74 25900.26 72380.0 10 2023
29 2023-11-17 49341.34 24523.56 73864.9 10 2023
30 2023-11-24 49700.74 27047.76 76748.5 10 2023
31 2023-12-01 33892.34 39456.38 73348.72 10 2023
32 2023-12-08 50831.34 30194.3 81025.64 10 2023
33 2023-12-15 52387.94 31884.0 84271.94 10 2023
34 2023-12-22 52909.94 32946.0 85855.94 10 2023
35 2023-12-29 56387.94 33956.0 90343.94 10 2023
36 2024-01-05 56405.94 34602.0 91007.94 10 2024
37 2024-01-12 42405.94 42438.0 84843.94 10 2024
38 2024-01-19 44405.94 46458.0 90863.94 10 2024
39 2024-01-26 43543.94 44782.0 88325.94 10 2024
40 2024-02-02 27305.94 52690.0 79995.94 10 2024
41 2024-02-08 31419.94 51926.0 83345.94 10 2024
42 2024-02-23 41861.28 44551.96 86413.24 10 2024
43 2024-03-01 49575.3 40919.28 90494.58 10 2024
44 2024-03-08 53677.94 35737.8 89415.74 10 2024
45 2024-03-15 60047.92 31026.78 91074.7 10 2024
46 2024-03-22 65873.84 26076.62 91950.46 10 2024
47 2024-03-29 60401.36 30781.6 91182.96 9 2024
48 2024-04-03 66965.58 26641.44 93607.02 9 2024
49 2024-04-12 56903.58 32507.18 89410.76 10 2024
50 2024-04-19 33499.58 52126.22 85625.8 10 2024
51 2024-04-26 42761.58 46693.66 89455.24 10 2024
52 2024-04-30 51342.72 39697.38 91040.1 10 2024
53 2024-05-10 56205.06 34632.66 90837.72 10 2024
54 2024-05-17 60869.06 31647.16 92516.22 10 2024
55 2024-05-24 59605.06 32880.44 92485.5 10 2024
56 2024-05-31 65813.5 29130.0 94943.5 9 2024
57 2024-06-07 47960.4 42130.56 90090.96 10 2024
58 2024-06-14 56152.4 36035.36 92187.76 10 2024
59 2024-06-21 62239.76 31984.0 94223.76 10 2024
60 2024-06-28 52527.76 40070.0 92597.76 10 2024
61 2024-07-05 52351.76 40466.0 92817.76 10 2024
62 2024-07-12 51451.72 42662.64 94114.36 10 2024
63 2024-07-19 55290.88 38665.44 93956.32 10 2024
64 2024-07-26 61456.88 33980.7 95437.58 10 2024
65 2024-08-02 61925.74 34468.0 96393.74 10 2024
66 2024-08-09 63005.74 33154.0 96159.74 10 2024
67 2024-08-16 56715.74 39058.0 95773.74 10 2024
68 2024-08-23 55364.2 37672.44 93036.64 10 2024
69 2024-08-30 52183.52 42771.06 94954.58 10 2024
70 2024-09-06 60506.34 35878.58 96384.92 9 2024
71 2024-09-13 62642.34 30741.5 93383.84 8 2024
72 2024-09-20 58359.32 34882.94 93242.26 10 2024
73 2024-09-27 69235.32 29121.08 98356.4 10 2024
74 2024-09-30 80349.32 21369.96 101719.28 10 2024
75 2024-10-11 56083.28 43112.0 99195.28 10 2024
76 2024-10-18 71101.28 35670.0 106771.28 10 2024
77 2024-10-25 87055.28 25324.0 112379.28 10 2024
78 2024-11-01 90986.66 26929.52 117916.18 10 2024
79 2024-11-08 100636.82 23667.66 124304.48 10 2024
80 2024-11-15 96557.2 28499.72 125056.92 10 2024
81 2024-11-22 102164.48 26932.02 129096.5 10 2024
82 2024-11-29 101950.1 29600.28 131550.38 10 2024
83 2024-12-06 103392.78 28411.32 131804.1 10 2024
84 2024-12-13 96844.12 33198.0 130042.12 10 2024
85 2024-12-20 82444.12 44764.0 127208.12 10 2024
86 2024-12-27 89318.12 35982.0 125300.12 8 2024
87 2025-01-03 87408.12 37578.0 124986.12 10 2025
88 2025-01-10 85580.12 38096.0 123676.12 10 2025
89 2025-01-17 92964.12 32718.0 125682.12 9 2025
90 2025-01-24 97680.12 30942.0 128622.12 8 2025
91 2025-01-27 95062.12 29920.0 124982.12 9 2025
92 2025-02-07 99682.12 27098.0 126780.12 9 2025
93 2025-02-14 106503.36 21418.78 127922.14 9 2025
94 2025-02-21 105749.72 23782.4 129532.12 9 2025
95 2025-02-28 102728.72 26542.96 129271.68 9 2025
96 2025-03-07 105425.36 27040.4 132465.76 10 2025
97 2025-03-14 115013.82 19782.36 134796.18 9 2025
98 2025-03-21 109423.82 25211.12 134634.94 9 2025
99 2025-03-28 103822.1 31484.3 135306.4 10 2025
100 2025-04-03 99800.62 34518.32 134318.94 10 2025
101 2025-04-11 99600.62 37063.76 136664.38 10 2025
102 2025-04-18 109341.54 30561.36 139902.9 10 2025
103 2025-04-25 106835.54 32281.18 139116.72 10 2025
104 2025-04-30 102745.54 35759.3 138504.84 10 2025
105 2025-05-09 102637.84 37585.88 140223.72 10 2025
106 2025-05-16 101963.84 38900.0 140863.84 10 2025
107 2025-05-23 109423.84 32758.0 142181.84 9 2025
108 2025-05-30 115165.84 27606.0 142771.84 8 2025
109 2025-06-06 118567.88 25523.98 144091.86 10 2025
110 2025-06-13 121827.46 23271.14 145098.6 9 2025
111 2025-06-20 109651.46 33717.02 143368.48 9 2025
112 2025-06-27 108767.46 36246.7 145014.16 9 2025
113 2025-07-04 117302.28 29383.94 146686.22 10 2025
114 2025-07-11 116312.28 31418.0 147730.28 10 2025
115 2025-07-18 113160.2 33946.38 147106.58 10 2025
116 2025-07-25 115180.6 32153.94 147334.54 10 2025
117 2025-08-01 113316.6 33357.8 146674.4 10 2025
118 2025-08-08 119086.6 28390.0 147476.6 10 2025
119 2025-08-15 119662.6 28798.0 148460.6 9 2025
120 2025-08-22 126652.6 23932.0 150584.6 10 2025
121 2025-08-29 116840.6 33900.0 150740.6 10 2025
122 2025-09-05 114126.02 36400.0 150526.02 10 2025
123 2025-09-12 116580.02 34638.0 151218.02 10 2025
124 2025-09-19 115522.02 34548.0 150070.02 10 2025
125 2025-09-26 115364.02 36744.0 152108.02 9 2025
126 2025-09-30 116726.02 32560.0 149286.02 10 2025
127 2025-10-10 116570.02 31992.0 148562.02 10 2025
128 2025-10-17 113000.02 34894.0 147894.02 10 2025
129 2025-10-24 113198.02 36844.0 150042.02 10 2025
130 2025-10-31 116430.02 34148.0 150578.02 10 2025
131 2025-11-07 118702.02 31342.0 150044.02 10 2025
132 2025-11-14 119016.02 33214.0 152230.02 10 2025
133 2025-11-21 109870.02 38562.0 148432.02 10 2025
134 2025-11-28 111938.02 37834.0 149772.02 10 2025
135 2025-12-05 109425.68 40135.04 149560.72 10 2025
136 2025-12-12 115769.68 35290.0 151059.68 10 2025
137 2025-12-19 110123.68 41948.0 152071.68 10 2025
138 2025-12-26 115371.68 37724.0 153095.68 10 2025
139 2025-12-31 115159.68 36376.0 151535.68 10 2025
140 2026-01-09 113854.76 39384.0 153238.76 10 2026
141 2026-01-16 117816.76 34904.0 152720.76 10 2026
142 2026-01-23 120376.76 35548.0 155924.76 10 2026
143 2026-01-30 118104.76 36686.0 154790.76 10 2026
144 2026-02-06 123830.76 32184.0 156014.76 10 2026
145 2026-02-13 130652.76 27454.0 158106.76 10 2026
146 2026-02-27 128078.76 31448.0 159526.76 10 2026
147 2026-03-06 121930.76 35786.0 157716.76 9 2026
148 2026-03-13 122818.76 33916.0 156734.76 9 2026
149 2026-03-20 114066.98 39770.48 153837.46 10 2026
150 2026-03-27 114944.98 41492.52 156437.5 10 2026
151 2026-04-03 118420.98 37343.56 155764.54 10 2026
152 2026-04-10 118180.98 38458.6 156639.58 10 2026
153 2026-04-17 111851.58 42540.0 154391.58 10 2026
154 2026-04-24 109899.58 42320.0 152219.58 10 2026
155 2026-04-30 111717.58 39436.0 151153.58 10 2026
BIN
View File
Binary file not shown.
-53
View File
@@ -1,53 +0,0 @@
feature,importance,importance_pct
dist_to_grid_upper,1319,4.38
dist_to_grid_lower,1282,4.26
price_cv,1231,4.09
amount_mean_20d,1198,3.98
range_compression_20d,1047,3.48
rebound_from_low_60d,1019,3.38
drawdown_60d,981,3.26
ma20_deviation_pct,915,3.04
grid_touch_count_60d,907,3.01
volume_ratio,903,3.0
price_entropy,877,2.91
avg_daily_amp,857,2.85
atr_pct,781,2.59
wick_ratio_20d,780,2.59
cross_freq_x_bb,743,2.47
trend_slope_60d,743,2.47
amount_trend_20d,741,2.46
range_position_60d,735,2.44
obv_slope,731,2.43
volatility_20d,707,2.35
volume_cv_20d,697,2.32
amplitude_cv,673,2.24
intraday_trend_strength,669,2.22
amp_cv_x_entropy,661,2.2
ma60_deviation_pct,655,2.18
ma20_ma60_gap_pct,650,2.16
grid_room_balance,630,2.09
bb_width,629,2.09
amount_cv_20d,598,1.99
amp_x_grid_vol,588,1.95
trend_slope_20d,568,1.89
amp_x_grid,499,1.66
trend_abs_slope_20d,448,1.49
close_reversal_count_20d,395,1.31
near_upper_boundary_risk,366,1.22
grid_touch_count_20d,335,1.11
near_grid_line_ratio_20d,293,0.97
low_volume_days_20d,274,0.91
turnover_proxy_20d,250,0.83
rolling_grid_ratio_20d,248,0.82
cross_freq_20d,206,0.68
mv_vol_interact,190,0.63
down_days_20d,180,0.6
high_amp_days,172,0.57
up_days_20d,165,0.55
small_cap_premium,164,0.54
ln_float_mv,162,0.54
near_lower_boundary_risk,114,0.38
trend_consistency_20d,103,0.34
usable_grid_count_lower,13,0.04
usable_grid_count_upper,12,0.04
grid_cross_density_20d,0,0.0
1 feature importance importance_pct
2 dist_to_grid_upper 1319 4.38
3 dist_to_grid_lower 1282 4.26
4 price_cv 1231 4.09
5 amount_mean_20d 1198 3.98
6 range_compression_20d 1047 3.48
7 rebound_from_low_60d 1019 3.38
8 drawdown_60d 981 3.26
9 ma20_deviation_pct 915 3.04
10 grid_touch_count_60d 907 3.01
11 volume_ratio 903 3.0
12 price_entropy 877 2.91
13 avg_daily_amp 857 2.85
14 atr_pct 781 2.59
15 wick_ratio_20d 780 2.59
16 cross_freq_x_bb 743 2.47
17 trend_slope_60d 743 2.47
18 amount_trend_20d 741 2.46
19 range_position_60d 735 2.44
20 obv_slope 731 2.43
21 volatility_20d 707 2.35
22 volume_cv_20d 697 2.32
23 amplitude_cv 673 2.24
24 intraday_trend_strength 669 2.22
25 amp_cv_x_entropy 661 2.2
26 ma60_deviation_pct 655 2.18
27 ma20_ma60_gap_pct 650 2.16
28 grid_room_balance 630 2.09
29 bb_width 629 2.09
30 amount_cv_20d 598 1.99
31 amp_x_grid_vol 588 1.95
32 trend_slope_20d 568 1.89
33 amp_x_grid 499 1.66
34 trend_abs_slope_20d 448 1.49
35 close_reversal_count_20d 395 1.31
36 near_upper_boundary_risk 366 1.22
37 grid_touch_count_20d 335 1.11
38 near_grid_line_ratio_20d 293 0.97
39 low_volume_days_20d 274 0.91
40 turnover_proxy_20d 250 0.83
41 rolling_grid_ratio_20d 248 0.82
42 cross_freq_20d 206 0.68
43 mv_vol_interact 190 0.63
44 down_days_20d 180 0.6
45 high_amp_days 172 0.57
46 up_days_20d 165 0.55
47 small_cap_premium 164 0.54
48 ln_float_mv 162 0.54
49 near_lower_boundary_risk 114 0.38
50 trend_consistency_20d 103 0.34
51 usable_grid_count_lower 13 0.04
52 usable_grid_count_upper 12 0.04
53 grid_cross_density_20d 0 0.0
-57
View File
@@ -1,57 +0,0 @@
{
"version": "v6.7-rank-lambdarank-mvp",
"feature_version": "v3.4",
"objective": "lambdarank",
"metric": "ndcg@5,10",
"ndcg_at_5": 0.5108501630463596,
"ndcg_at_10": 0.577072484777789,
"spearman": 0.5896180660523218,
"spearman_baseline_regression": 0.573016131374212,
"spearman_ratio_vs_baseline": 1.0289728923307881,
"best_iter": 29,
"train_seconds": 1.4682528972625732,
"n_train": 254234,
"n_val": 73785,
"top10_features": [
{
"name": "dist_to_grid_lower",
"gain": 26718.906676471233
},
{
"name": "amp_x_grid",
"gain": 5439.311601281166
},
{
"name": "cross_freq_x_bb",
"gain": 4232.088328957558
},
{
"name": "amp_x_grid_vol",
"gain": 3015.4479908943176
},
{
"name": "dist_to_grid_upper",
"gain": 2406.65438079834
},
{
"name": "ln_float_mv",
"gain": 1248.8392915129662
},
{
"name": "avg_daily_amp",
"gain": 794.0413353443146
},
{
"name": "amount_mean_20d",
"gain": 670.1584417819977
},
{
"name": "vol_decay_x_dist_lower",
"gain": 659.5440436601639
},
{
"name": "vol_decay_x_grid_balance",
"gain": 653.8379725217819
}
]
}
-158
View File
@@ -1,158 +0,0 @@
# v6.7r3 代码使用说明
`code/` 目录包含 v6.7r3 策略的**自包含**实现,可在不依赖项目其他模块的情况下独立运行。
## 文件清单
| 文件 | 大小 | 作用 |
|---|---|---|
| `strategy.py` | ~22 KB | 完整策略实现(模型加载/特征/网格/沉寂/周度淘汰) |
## 文件结构
```
strategy.py
├── 配置常量 (网格/价格/沉寂/周度)
├── 1. 持仓 + 网格交易
│ ├── Position dataclass
│ ├── compute_initial_position() 初始建仓
│ ├── compute_single_position() 单格建仓
│ └── simulate_grid_day() 单日网格 (LIFO)
├── 2. 5 特征沉寂检测
│ └── is_slumbering() 5 特征 ≥ 3 触发
├── 3. 模型加载 + 三件套预测
│ ├── ModelBundle class
│ │ ├── load 3 .pkl
│ │ └── predict(X56) → {rank_score, top_prob, stack_prob}
├── 4. 特征计算
│ ├── compute_52_base_features() 52 维 v3.4 基础
│ └── compute_4_v67_new_features() 4 维 v6.7 新增
├── 5. 评分池
│ └── score_pool() 单日全市场评分
├── 6. 主回测入口(精简版)
│ └── quick_backtest() 生产级完整版见 tools/backtest_v67r2.py
└── 7. 入口示例
└── __main__ 加载模型 + 加载行情 + 跑回测
```
## 快速开始
```bash
# 1. 安装依赖
pip install pandas numpy lightgbm scipy
# 2. 准备数据
# 方式 A: 用项目内的 dump_market_data_to_parquet.py 拉 Postgres
python tools/dump_market_data_to_parquet.py
# 方式 B: 直接用现有 parquet (release/v6.7r3 之前已生成 5025 个)
# 3. 运行
cd release/v6.7r3/code
python strategy.py
```
## 核心 API 速查
### 1. 加载模型
```python
from strategy import ModelBundle
models = ModelBundle("../models")
# 等价: models = ModelBundle("release/v6.7r3/models")
print(models.rank_feats[:5]) # ['rolling_grid_ratio_20d', ...]
```
### 2. 三件套预测
```python
import numpy as np
import pandas as pd
# 加载单只股 120 日窗口
df_window = pd.read_parquet("data/market_data/share/000001.parquet")
df_window["date"] = pd.to_datetime(df_window["date"])
df_window = df_window.tail(120).reset_index(drop=True)
# 算 56 维特征 (这里用简化版, 生产建议用项目 core.features)
from strategy import compute_52_base_features, compute_4_v67_new_features
fd = compute_52_base_features(df_window)
fd = compute_4_v67_new_features(df_window, fd)
X56 = np.array([fd.get(k, 0.0) for k in models.rank_feats], dtype=np.float64).reshape(1, -1)
# 三件套预测
pred = models.predict(X56)
print(f"rank_score: {pred['rank_score'][0]:.3f}")
print(f"top_prob: {pred['top_prob'][0]:.3f}")
print(f"stack_prob: {pred['stack_prob'][0]:.3f}") # 月末选股用
```
### 3. 沉寂检测
```python
from strategy import is_slumbering
df = pd.read_parquet("data/market_data/share/000001.parquet")
df["date"] = pd.to_datetime(df["date"])
slumbering = is_slumbering(df, min_triggers=3)
# True = 5 特征中至少 3 个触发 → 资金离场
```
### 4. 网格交易(单日)
```python
from strategy import Position, simulate_grid_day
pos = Position(code="000001", base=10, queue=[10.0, 9.5], entry_date="2023-05-01")
new_base, new_queue, trades = simulate_grid_day(
pos.base, pos.queue,
open_p=9.4, high_p=10.6, low_p=9.3, close_p=10.5,
)
# 触发 buy at 9.0 (low 9.3 <= 9.0? no, 9.3 > 9.0 不触发)
# 触发 sell at 10.0 (high 10.6 >= 10.0, 卖出 1 格)
# 最终: base=11, queue=[9.5] (1 格清仓)
```
### 5. 评分池(每日全市场)
```python
from strategy import score_pool, ModelBundle
import pandas as pd
# 假设 kline_cache 是 dict[code, DataFrame]
models = ModelBundle("../models")
pool = score_pool(pd.Timestamp("2024-03-15"), kline_cache, models, models.rank_feats)
# pool 列: code6, latest_close, rank_score, top_prob, stack_prob
# 按 stack_prob 降序, 取 top 10
top10 = pool.head(10)
```
## 关键参数(可调)
| 参数 | 默认 | 说明 | 调优方向 |
|---|---|---|---|
| `WEEKLY_ELIM_N` | 2 | 周度淘汰: 连续 N 周不在 top 50 | 1 太频, 3+ 太慢 |
| `SLUMBER_TRIGGERS` | 3 | 沉寂检测: >= 3/5 特征触发 | 2 太宽, 4 太严 |
| `SLUMBER_DAYS` | 10 | 连续触发多少天清仓 | 5 太短, 20 太长 |
| `TOP_N` | 10 | 最大持仓数 | 5-15 视资金量 |
| `SHARES_PER_GRID` | 200 | 单格股数 (2 手) | 100 (1 手) 也可 |
| `REFILL_PRICE` | 9.0-9.8 | 补仓价格区间 | 紧贴 9-10 网格上限 |
## 依赖项目其他模块?
为保持 `code/` 目录**自包含**:
- ✅ 不依赖 `core/features.py` (内置 `compute_52_base_features` 简化版)
- ✅ 不依赖 `training/dataset_builder.py` (内置 `compute_4_v67_new_features`)
- ❌ 需要数据: parquet 行情文件
**生产部署建议**:用 `core/features.calculate_features` 替换 `compute_52_base_features`,
它有完整版 v3.4 52 维特征实现,精度更高。
## 完整版 vs 精简版
| 维度 | `code/strategy.py` (精简) | `tools/backtest_v67r2.py` (生产) |
|---|---|---|
| 特征计算 | 简化版 (10 维示例) | 完整版 (52 维 v3.4 + 4 维 v6.7) |
| 数据加载 | dict of DataFrame | parquet 目录 + score cache |
| 日志 | 无 | 详细进度打印 |
| 输出 | dict 指标 | CSV/JSON/PNG 完整产物 |
| 速度 | 慢 (无缓存) | 快 (有 score cache) |
| 用途 | 教学/集成 | 完整回测 |
**生产环境**:用 `tools/backtest_v67r2.py` 跑回测,确保完整功能。
**集成到实盘系统**:用 `code/strategy.py` 中的 `ModelBundle``quick_backtest` 作为模板。
@@ -1,574 +0,0 @@
"""
v6.7r3 策略核心实现 —— 自包含版
================================
包含:
1. 模型加载
2. 56 维特征计算
3. 三件套预测 (rank → top → stacking)
4. 网格交易模拟器
5. 5 特征沉寂检测
6. 周度评分淘汰
7. 月末调仓 + 清仓补入
依赖:
pip install pandas numpy lightgbm scipy
(内嵌了核心算法, 不依赖项目其他模块, 可独立运行)
"""
from __future__ import annotations
import math
import pickle
import time
from collections import defaultdict
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import numpy as np
import pandas as pd
# ============================================================
# 0. 配置
# ============================================================
# 网格
INITIAL_CASH = 60_000.0
TOP_N = 10
TOP_MODEL_N = 50
SHARES_PER_GRID = 200
MIN_BUY_PRICE = 7.0
MAX_BUY_PRICE = 10.0
REFILL_MIN_PRICE = 9.0
REFILL_MAX_PRICE = 9.8
GRID_LOWER, GRID_UPPER = 1, 11
# 沉寂检测
SLUMBER_TRIGGERS = 3 # >= 3/5 特征触发
SLUMBER_DAYS = 10 # 连续 10 日触发
SLUMBER_LOOKBACK_60 = 60
SLUMBER_LOOKBACK_20 = 20
# 周度淘汰
WEEKLY_ELIM_N = 2 # 连续 2 周不在 top 50 → 卖
# ============================================================
# 1. 持仓 + 网格交易
# ============================================================
@dataclass
class Position:
code: str
base: int # 当前基准价 (整数)
queue: list = field(default_factory=list) # 持仓队列: 每格成本价
entry_date: str = ""
def compute_initial_position(close: float) -> Tuple[int, list]:
"""初始建仓: base=ceil(close), queue=[10, 9, ..., base] 且 >= close"""
base = math.ceil(close)
base = max(GRID_LOWER, min(base, GRID_UPPER))
queue = [g for g in range(GRID_UPPER, base - 1, -1) if g >= close]
return base, queue
def compute_single_position(close: float) -> Tuple[int, list]:
"""单格建仓"""
base = math.ceil(close)
base = max(GRID_LOWER, min(base, GRID_UPPER))
return base, [base]
def simulate_grid_day(base, queue, open_p, high_p, low_p, close_p, can_buy=True):
"""单日网格交易 (LIFO)"""
trades = []
new_base, new_queue = base, list(queue)
# 1) 买
buy_price = new_base - 1
if low_p <= buy_price and new_base > GRID_LOWER and can_buy:
new_base = buy_price
new_queue.append(buy_price)
trades.append({"direction": "buy", "price": buy_price, "shares": SHARES_PER_GRID, "pnl": 0.0})
# 2) 卖 (循环)
while True:
sell_price = new_base + 1
if high_p >= sell_price and new_queue:
buy_cost = new_queue.pop()
pnl = (sell_price - buy_cost) * SHARES_PER_GRID
trades.append({"direction": "sell", "price": sell_price, "shares": SHARES_PER_GRID, "pnl": pnl})
new_base = sell_price
else:
break
return new_base, new_queue, trades
# ============================================================
# 2. 5 特征沉寂检测
# ============================================================
def is_slumbering(df: pd.DataFrame, lookback_60=60, lookback_20=20,
min_triggers=SLUMBER_TRIGGERS) -> bool:
"""检测一只股是否陷入'沉寂' (资金离场后长期低位震荡).
5 特征, >= min_triggers 触发.
"""
if df is None or len(df) < lookback_60:
return False
sub = df.tail(lookback_60)
close = sub["close"].values
high = sub["high"].values
low = sub["low"].values
vol = sub["volume"].values
# 1. 波动率塌陷
log_ret = np.log(close[1:] / close[:-1])
if len(log_ret) < lookback_20:
return False
vol_20d = float(np.std(log_ret[-lookback_20:], ddof=1))
vol_60d = float(np.std(log_ret, ddof=1))
vol_collapse = (vol_60d > 0) and (vol_20d / vol_60d < 0.6)
# 2. 振幅萎缩
amp_20d = float(np.mean((high[-lookback_20:] - low[-lookback_20:]) / close[-lookback_20:]) * 100)
amp_shrink = amp_20d < 2.5
# 3. 成交量枯竭
avg_vol_20 = float(np.mean(vol[-lookback_20:]))
avg_vol_60 = float(np.mean(vol))
vol_dry = (avg_vol_60 > 0) and (avg_vol_20 / avg_vol_60 < 0.5)
# 4. 价格弱势
price_max_60 = float(np.max(close))
price_weak = price_max_60 > 0 and (close[-1] / price_max_60) < 0.85
# 5. 反弹失败
recent_high_30 = float(np.max(high[-30:]))
past_high_60 = float(np.max(high))
rebound_fail = past_high_60 > 0 and (recent_high_30 / past_high_60) < 0.95
triggers = [vol_collapse, amp_shrink, vol_dry, price_weak, rebound_fail]
return sum(triggers) >= min_triggers
# ============================================================
# 3. 模型加载 + 三件套预测
# ============================================================
class ModelBundle:
"""v6.7r3 三件套模型封装"""
def __init__(self, model_dir: str | Path):
model_dir = Path(model_dir)
with open(model_dir / "rank_lambdarank.pkl", "rb") as f:
rank_b = pickle.load(f)
with open(model_dir / "top_v67r2.pkl", "rb") as f:
top_b = pickle.load(f)
with open(model_dir / "stacking_v67r2.pkl", "rb") as f:
stack_b = pickle.load(f)
self.rank_model = rank_b["model"]
self.rank_feats = rank_b["feat_names"] # 56 维
self.top_model = top_b["model"]
self.top_feats = top_b["feat_names"] # 53 维
self.stack_model = stack_b["model"]
self.stack_feats = stack_b["feat_names"] # 55 维
@staticmethod
def _safe_predict_proba(model, X):
if hasattr(model, "predict_proba"):
return model.predict_proba(X)[:, 1]
return model.predict(X, raw_score=False)
def predict(self, X56: np.ndarray) -> dict:
"""输入 56 维特征矩阵 (n, 56), 返回三件套预测 dict.
返回: rank_score (n,), top_prob (n,), stack_prob (n,)
"""
rank_pred = self.rank_model.predict(X56)
# 52 维基础特征在 X56 中的索引
base_52_idx = [self.rank_feats.index(f) for f in self.top_feats if f != "rank_predicted_rounds"]
X53 = np.column_stack([X56[:, base_52_idx], rank_pred])
top_prob = self._safe_predict_proba(self.top_model, X53)
X55 = np.column_stack([X53, top_prob])
stack_prob = self._safe_predict_proba(self.stack_model, X55)
return {
"rank_score": rank_pred,
"top_prob": top_prob,
"stack_prob": stack_prob,
}
# ============================================================
# 4. 特征计算 (从项目 core/features.py 抽取, 关键函数)
# ============================================================
def _log_returns(close: np.ndarray) -> np.ndarray:
return np.log(close[1:] / close[:-1])
def _ema(arr: np.ndarray, period: int) -> np.ndarray:
"""指数移动平均"""
alpha = 2.0 / (period + 1)
out = np.zeros_like(arr)
out[0] = arr[0]
for i in range(1, len(arr)):
out[i] = alpha * arr[i] + (1 - alpha) * out[i-1]
return out
def compute_52_base_features(df: pd.DataFrame, total_share: Optional[float] = None) -> dict:
"""计算 v3.4 的 52 维基础特征 (简化版, 不完全等同于原版).
注: 完整版在 core/features.py, 这里用 pandas/numpy 简化.
"""
n = len(df)
if n < 60:
return None
close = df["close"].values
high = df["high"].values
low = df["low"].values
open_ = df["open"].values
vol = df["volume"].values
feats = {}
# 1. rolling_grid_ratio_20d
feats["rolling_grid_ratio_20d"] = float(np.mean((close[-20:] >= 1) & (close[-20:] <= 11)) * 100)
# 2. cross_freq_20d
diff = close[1:] - close[:-1]
sign_change = np.sum(np.abs(np.diff(np.sign(diff[-19:]))) > 0)
feats["cross_freq_20d"] = float(sign_change / 19 * 100) if 19 > 0 else 0.0
# 3. avg_daily_amp
feats["avg_daily_amp"] = float(np.mean((high - low) / open_) * 100) if n > 0 else 0.0
# 4. high_amp_days
feats["high_amp_days"] = float(np.mean((high - low) / open_ > 0.02) * 100) if n > 0 else 0.0
# 5. atr_pct
tr = np.maximum(high - low, np.maximum(np.abs(high - np.roll(close, 1)),
np.abs(low - np.roll(close, 1))))
feats["atr_pct"] = float(np.mean(tr[-14:]) / close[-1] * 100) if close[-1] > 0 else 0.0
# 6. volatility_20d (年化)
log_ret = _log_returns(close)
feats["volatility_20d"] = float(np.std(log_ret[-20:], ddof=1) * np.sqrt(252) * 100) if len(log_ret) >= 20 else 0.0
# 7. price_cv
feats["price_cv"] = float(np.std(close, ddof=1) / np.mean(close) * 100) if n > 1 and np.mean(close) > 0 else 0.0
# 8. bb_width (布林带宽)
ma20 = np.mean(close[-20:])
sd20 = np.std(close[-20:], ddof=1)
feats["bb_width"] = float((4 * sd20) / ma20 * 100) if ma20 > 0 else 0.0
# 9. volume_ratio
avg_vol_20 = float(np.mean(vol[-20:])) if n >= 20 else float(np.mean(vol))
avg_vol_60 = float(np.mean(vol[-60:])) if n >= 60 else float(np.mean(vol))
feats["volume_ratio"] = avg_vol_20 / avg_vol_60 if avg_vol_60 > 0 else 0.0
# 10. obv_slope
direction = np.sign(np.diff(close))
direction = np.concatenate([[0], direction])
obv = np.cumsum(direction * vol)
if len(obv) >= 20:
x = np.arange(20)
y = obv[-20:]
feats["obv_slope"] = float((np.polyfit(x, y, 1)[0]) / (np.mean(np.abs(y)) + 1e-10))
else:
feats["obv_slope"] = 0.0
# ... (其他 42 维特征省略, 完整版在 core/features.py)
# 这里只展示 10 个核心特征的计算模式
# 实际部署时建议直接调用项目 core.features.calculate_features
return feats
def compute_4_v67_new_features(df: pd.DataFrame, fd: dict) -> dict:
"""计算 v6.7 新增的 4 维特征"""
n = len(df)
if n < 60:
return fd
close = df["close"].values
high = df["high"].values
low = df["low"].values
# 53. vol_decay_5d
log_ret = np.log(close[1:] / close[:-1])
if len(log_ret) >= 20:
vol_5d = float(np.std(log_ret[-5:], ddof=1))
vol_20d = float(np.std(log_ret[-20:], ddof=1))
fd["vol_decay_5d"] = vol_5d / vol_20d if vol_20d > 0 else 0.0
else:
fd["vol_decay_5d"] = 0.0
# 54. grid_touch_relative_10d
if n >= 60:
range_10d = float(np.max(high[-10:]) - np.min(low[-10:]))
range_60d = float(np.max(high[-60:]) - np.min(low[-60:]))
close_now = float(close[-1])
close_60d_mean = float(np.mean(close[-60:]))
if close_now > 0 and close_60d_mean > 0 and range_60d > 0:
fd["grid_touch_relative_10d"] = (range_10d / close_now) / (range_60d / close_60d_mean)
else:
fd["grid_touch_relative_10d"] = 0.0
else:
fd["grid_touch_relative_10d"] = 0.0
# 55. vol_decay_x_grid_balance
fd["vol_decay_x_grid_balance"] = fd.get("vol_decay_5d", 0.0) * fd.get("grid_room_balance", 0.0)
# 56. vol_decay_x_dist_lower
fd["vol_decay_x_dist_lower"] = fd.get("vol_decay_5d", 0.0) * fd.get("dist_to_grid_lower", 0.0)
return fd
# ============================================================
# 5. 评分池(单日)
# ============================================================
def score_pool(date: pd.Timestamp,
kline_cache: Dict[str, pd.DataFrame],
models: ModelBundle,
feat_names: list) -> pd.DataFrame:
"""对所有有 120 日历史的股算 v6.7 三件套预测.
返回 DataFrame: code6, latest_close, rank_score, top_prob, stack_prob
"""
PRICE_MIN, PRICE_MAX = 1.0, 11.0
rows = []
codes = []
closes = []
for code, df in kline_cache.items():
sub = df[df["date"] <= date]
if len(sub) < 120:
continue
latest = float(sub["close"].iloc[-1])
if not (PRICE_MIN <= latest <= PRICE_MAX):
continue
# 算 56 维特征
obs = sub.tail(120).reset_index(drop=True)
fd = compute_52_base_features(obs)
if fd is None:
continue
fd = compute_4_v67_new_features(obs, fd)
try:
X = np.array([fd.get(k, 0.0) for k in feat_names], dtype=np.float64).reshape(1, -1)
pred = models.predict(X)
except Exception:
continue
rows.append(pred)
codes.append(code)
closes.append(latest)
if not rows:
return pd.DataFrame(columns=["code6", "latest_close", "rank_score", "top_prob", "stack_prob"])
import numpy as np
return pd.DataFrame({
"code6": codes,
"latest_close": closes,
"rank_score": [r["rank_score"][0] for r in rows],
"top_prob": [r["top_prob"][0] for r in rows],
"stack_prob": [r["stack_prob"][0] for r in rows],
}).sort_values("stack_prob", ascending=False).reset_index(drop=True)
# ============================================================
# 6. 主回测入口(精简版, 仅展示核心逻辑)
# ============================================================
def quick_backtest(kline_cache: Dict[str, pd.DataFrame],
models: ModelBundle,
start_date: str = "2023-05-01",
end_date: str = "2026-04-30",
weekly_elim_n: int = WEEKLY_ELIM_N,
slumber_days: int = SLUMBER_DAYS) -> dict:
"""精简版 3 年回测 (生产级完整版见 tools/backtest_v67r2.py).
核心流程:
1. 初始建仓
2. 每日: 网格交易 + 沉寂检测
3. 周五: 周度评分淘汰 + 补仓
4. 月末: 清仓补入 (触及 11 元)
Returns:
dict: 总收益率, 年化夏普, 最大回撤, 周胜率, 终值
"""
feat_names = models.rank_feats
# 交易日索引
sample = next(iter(kline_cache.values()))
all_dates = pd.DatetimeIndex(sorted(sample["date"].unique()))
mask = (all_dates >= start_date) & (all_dates <= end_date)
backtest_dates = all_dates[mask]
# 初始评分 (INIT_SELECT_DATE)
init_date = pd.Timestamp("2023-04-28")
init_pool = score_pool(init_date, kline_cache, models, feat_names)
init_pool = init_pool[init_pool["latest_close"].apply(lambda p: MIN_BUY_PRICE <= p <= MAX_BUY_PRICE)]
init_pool = init_pool.sort_values("stack_prob", ascending=False).head(TOP_N)
# 初始建仓
positions: Dict[str, Position] = {}
cash = INITIAL_CASH
for _, row in init_pool.iterrows():
code = row["code6"]
actual_close = float(row["latest_close"])
base, grid_queue = compute_initial_position(actual_close)
if not grid_queue:
continue
positions[code] = Position(code, base, [actual_close] * len(grid_queue), "2023-04-28")
for _ in grid_queue:
cash -= actual_close * SHARES_PER_GRID
# 周度淘汰历史
top50_history = defaultdict(list)
slumber_streak: Dict[str, int] = {}
total_asset_history = []
weekly_pnl = []
for i, cur_date in enumerate(backtest_dates):
cur_str = cur_date.strftime("%Y-%m-%d")
is_week_end = (i == len(backtest_dates) - 1) or (backtest_dates[i + 1].week != cur_date.week)
# === 网格日间交易 ===
for code, pos in list(positions.items()):
if not pos.queue:
continue
df = kline_cache[code]
sub = df[df["date"] == cur_date]
if sub.empty:
continue
row = sub.iloc[0]
new_base, new_queue, day_trades = simulate_grid_day(
pos.base, pos.queue,
float(row["open"]), float(row["high"]), float(row["low"]), float(row["close"]),
)
if day_trades:
pos.base, pos.queue = new_base, new_queue
for t in day_trades:
if t["direction"] == "buy":
cash -= t["price"] * t["shares"]
else:
cash += t["price"] * t["shares"]
# === 沉寂检测 ===
for code, pos in list(positions.items()):
if not pos.queue:
continue
df = kline_cache[code]
sub = df[df["date"] <= cur_date]
if len(sub) < 60:
continue
slumber = is_slumbering(sub)
slumber_streak[code] = slumber_streak.get(code, 0) + 1 if slumber else 0
if slumber_streak[code] >= slumber_days and pos.queue:
# 全仓清仓
px = float(sub["close"].iloc[-1])
cash += px * len(pos.queue) * SHARES_PER_GRID
positions[code] = Position(code, 0, [], cur_str)
slumber_streak[code] = 0
# === 资产快照 ===
mv = sum((float(kline_cache[c][kline_cache[c]["date"] <= cur_date]["close"].iloc[-1])
* len(p.queue) * SHARES_PER_GRID)
for c, p in positions.items() if p.queue)
total_asset = cash + mv
total_asset_history.append(total_asset)
# === 周度淘汰 + 补仓 ===
if is_week_end and i > 0 and weekly_elim_n > 0:
pool = score_pool(cur_date, kline_cache, models, feat_names)
top50 = set(pool.head(TOP_MODEL_N)["code6"].tolist())
for code in list(positions.keys()):
top50_history[code].append(code in top50)
# 连续 N 周不在 top 50 → 卖出
inactive = []
for code, p in positions.items():
if not p.queue:
continue
hist = top50_history.get(code, [])
if len(hist) >= weekly_elim_n and all(x is False for x in hist[-weekly_elim_n:]):
inactive.append(code)
for code in inactive[:1]: # 每月最多淘汰 1 只 (与 v6.3 一致)
pos = positions[code]
sub = kline_cache[code][kline_cache[code]["date"] <= cur_date]
px = float(sub["close"].iloc[-1])
cash += px * len(pos.queue) * SHARES_PER_GRID
positions[code] = Position(code, 0, [], cur_str)
# 补仓
positions = {c: p for c, p in positions.items() if p.queue}
refill_needed = max(0, TOP_N - len(positions))
if refill_needed > 0:
ref_pool = pool[pool["latest_close"].apply(lambda p: REFILL_MIN_PRICE < p < REFILL_MAX_PRICE)]
ref_pool = ref_pool[~ref_pool["code6"].isin(positions.keys())]
ref_pool = ref_pool.head(refill_needed)
for _, row in ref_pool.iterrows():
code = row["code6"]
actual_close = float(row["latest_close"])
base, grid_queue = compute_single_position(actual_close)
if not grid_queue:
continue
cost = actual_close * SHARES_PER_GRID * len(grid_queue)
if cash < cost:
continue
positions[code] = Position(code, base, [actual_close] * len(grid_queue), cur_str)
cash -= cost
# 计算指标
final_value = total_asset_history[-1] if total_asset_history else INITIAL_CASH
total_return = final_value / INITIAL_CASH - 1
rets = np.diff(total_asset_history) / total_asset_history[:-1]
sharpe = float(rets.mean() / rets.std() * np.sqrt(52)) if len(rets) > 1 and rets.std() > 0 else 0.0
cum_max = np.maximum.accumulate(total_asset_history)
dd = (np.array(total_asset_history) - cum_max) / cum_max
max_dd = float(dd.min())
win_rate = float((rets > 0).mean()) if len(rets) > 0 else 0.0
return {
"total_return_pct": round(total_return * 100, 2),
"annual_sharpe": round(sharpe, 4),
"max_drawdown_pct": round(max_dd * 100, 2),
"weekly_win_rate_pct": round(win_rate * 100, 2),
"final_value": round(final_value, 2),
}
# ============================================================
# 7. 入口示例
# ============================================================
if __name__ == "__main__":
# 1. 加载模型
models = ModelBundle("models") # 默认从当前目录的 models/ 加载
print(f"✓ 加载模型: rank {len(models.rank_feats)} 维, "
f"top {len(models.top_feats)} 维, stack {len(models.stack_feats)}")
# 2. 加载行情 (示例: 从 parquet 目录)
# 实际部署时, 从 market_data.kline_stock (Postgres) 或本地 parquet 加载
from pathlib import Path
parquet_dir = Path("data/market_data/share")
kline_cache = {}
for p in parquet_dir.glob("*.parquet"):
df = pd.read_parquet(p)
df["date"] = pd.to_datetime(df["date"])
kline_cache[p.stem] = df
print(f"✓ 加载行情: {len(kline_cache)} 只股")
# 3. 跑精简版回测
metrics = quick_backtest(kline_cache, models)
print("\n=== v6.7r3 三年回测结果 (精简版) ===")
for k, v in metrics.items():
print(f" {k}: {v}")
-23
View File
@@ -1,23 +0,0 @@
date,code,exit_price,realized_pnl,slumber_streak_days
2023-07-21,920870,7.53,-374.0,10
2023-07-25,920414,9.44,-1.9999999999999574,10
2024-03-27,920641,7.56,-734.0000000000001,10
2024-04-03,920001,8.93,-164.00000000000006,10
2024-05-31,603825,8.36,-130.00000000000006,10
2024-09-06,300462,8.98,-16.000000000000014,10
2024-09-11,920641,6.67,-1404.0,10
2024-12-23,920090,6.48,-1365.9999999999995,10
2024-12-25,920021,5.71,-1352.0,10
2025-01-13,920792,8.58,-107.99999999999983,10
2025-01-22,920371,6.9,-1199.9999999999998,10
2025-01-24,920339,7.86,-571.9999999999997,10
2025-01-27,920792,8.99,-85.99999999999994,10
2025-02-05,920810,8.18,-217.99999999999997,10
2025-03-13,002789,7.35,-942.0,10
2025-05-21,300052,10.3,258.00000000000017,10
2025-05-26,920639,9.54,185.9999999999996,10
2025-05-26,920553,10.17,94.00000000000013,10
2025-06-12,300052,9.94,77.99999999999976,10
2025-08-12,300798,9.11,-53.90000000000015,10
2025-09-26,000679,7.75,-501.99999999999994,10
2026-03-03,300086,8.81,-114.00000000000006,10
1 date code exit_price realized_pnl slumber_streak_days
2 2023-07-21 920870 7.53 -374.0 10
3 2023-07-25 920414 9.44 -1.9999999999999574 10
4 2024-03-27 920641 7.56 -734.0000000000001 10
5 2024-04-03 920001 8.93 -164.00000000000006 10
6 2024-05-31 603825 8.36 -130.00000000000006 10
7 2024-09-06 300462 8.98 -16.000000000000014 10
8 2024-09-11 920641 6.67 -1404.0 10
9 2024-12-23 920090 6.48 -1365.9999999999995 10
10 2024-12-25 920021 5.71 -1352.0 10
11 2025-01-13 920792 8.58 -107.99999999999983 10
12 2025-01-22 920371 6.9 -1199.9999999999998 10
13 2025-01-24 920339 7.86 -571.9999999999997 10
14 2025-01-27 920792 8.99 -85.99999999999994 10
15 2025-02-05 920810 8.18 -217.99999999999997 10
16 2025-03-13 002789 7.35 -942.0 10
17 2025-05-21 300052 10.3 258.00000000000017 10
18 2025-05-26 920639 9.54 185.9999999999996 10
19 2025-05-26 920553 10.17 94.00000000000013 10
20 2025-06-12 300052 9.94 77.99999999999976 10
21 2025-08-12 300798 9.11 -53.90000000000015 10
22 2025-09-26 000679 7.75 -501.99999999999994 10
23 2026-03-03 300086 8.81 -114.00000000000006 10
Binary file not shown.
-55
View File
@@ -1,55 +0,0 @@
feature,importance,importance_pct
rank_predicted_rounds,1252,20.87
top_elite_prob,1163,19.38
ma20_deviation_pct,321,5.35
atr_pct,296,4.93
dist_to_grid_lower,225,3.75
price_cv,190,3.17
amount_mean_20d,183,3.05
drawdown_60d,158,2.63
obv_slope,145,2.42
dist_to_grid_upper,142,2.37
ma20_ma60_gap_pct,141,2.35
range_position_60d,124,2.07
trend_slope_60d,123,2.05
rebound_from_low_60d,114,1.9
intraday_trend_strength,98,1.63
volume_ratio,98,1.63
volume_cv_20d,86,1.43
ma60_deviation_pct,81,1.35
grid_room_balance,69,1.15
range_compression_20d,69,1.15
volatility_20d,64,1.07
amount_trend_20d,56,0.93
near_upper_boundary_risk,54,0.9
up_days_20d,50,0.83
avg_daily_amp,48,0.8
amplitude_cv,48,0.8
amount_cv_20d,44,0.73
small_cap_premium,42,0.7
near_lower_boundary_risk,42,0.7
high_amp_days,40,0.67
cross_freq_x_bb,37,0.62
amp_x_grid,37,0.62
bb_width,36,0.6
amp_x_grid_vol,36,0.6
ln_float_mv,33,0.55
price_entropy,28,0.47
turnover_proxy_20d,26,0.43
trend_slope_20d,25,0.42
mv_vol_interact,25,0.42
wick_ratio_20d,22,0.37
grid_touch_count_60d,21,0.35
amp_cv_x_entropy,21,0.35
grid_touch_count_20d,20,0.33
rolling_grid_ratio_20d,14,0.23
near_grid_line_ratio_20d,12,0.2
usable_grid_count_upper,9,0.15
down_days_20d,9,0.15
trend_abs_slope_20d,7,0.12
cross_freq_20d,6,0.1
usable_grid_count_lower,5,0.08
low_volume_days_20d,3,0.05
close_reversal_count_20d,1,0.02
trend_consistency_20d,1,0.02
grid_cross_density_20d,0,0.0
1 feature importance importance_pct
2 rank_predicted_rounds 1252 20.87
3 top_elite_prob 1163 19.38
4 ma20_deviation_pct 321 5.35
5 atr_pct 296 4.93
6 dist_to_grid_lower 225 3.75
7 price_cv 190 3.17
8 amount_mean_20d 183 3.05
9 drawdown_60d 158 2.63
10 obv_slope 145 2.42
11 dist_to_grid_upper 142 2.37
12 ma20_ma60_gap_pct 141 2.35
13 range_position_60d 124 2.07
14 trend_slope_60d 123 2.05
15 rebound_from_low_60d 114 1.9
16 intraday_trend_strength 98 1.63
17 volume_ratio 98 1.63
18 volume_cv_20d 86 1.43
19 ma60_deviation_pct 81 1.35
20 grid_room_balance 69 1.15
21 range_compression_20d 69 1.15
22 volatility_20d 64 1.07
23 amount_trend_20d 56 0.93
24 near_upper_boundary_risk 54 0.9
25 up_days_20d 50 0.83
26 avg_daily_amp 48 0.8
27 amplitude_cv 48 0.8
28 amount_cv_20d 44 0.73
29 small_cap_premium 42 0.7
30 near_lower_boundary_risk 42 0.7
31 high_amp_days 40 0.67
32 cross_freq_x_bb 37 0.62
33 amp_x_grid 37 0.62
34 bb_width 36 0.6
35 amp_x_grid_vol 36 0.6
36 ln_float_mv 33 0.55
37 price_entropy 28 0.47
38 turnover_proxy_20d 26 0.43
39 trend_slope_20d 25 0.42
40 mv_vol_interact 25 0.42
41 wick_ratio_20d 22 0.37
42 grid_touch_count_60d 21 0.35
43 amp_cv_x_entropy 21 0.35
44 grid_touch_count_20d 20 0.33
45 rolling_grid_ratio_20d 14 0.23
46 near_grid_line_ratio_20d 12 0.2
47 usable_grid_count_upper 9 0.15
48 down_days_20d 9 0.15
49 trend_abs_slope_20d 7 0.12
50 cross_freq_20d 6 0.1
51 usable_grid_count_lower 5 0.08
52 low_volume_days_20d 3 0.05
53 close_reversal_count_20d 1 0.02
54 trend_consistency_20d 1 0.02
55 grid_cross_density_20d 0 0.0
-36
View File
@@ -1,36 +0,0 @@
{
"rank": {
"v6.6": {
"spearman_on_val": 0.5741312551267312
},
"v6.7r2": {
"spearman_on_val": 0.5896180660523218
}
},
"top": {
"v6.6": {
"pr_auc_val": 0.6881080916474673
},
"v6.7r2": {
"pr_auc_val": 0.6953262363660502
},
"delta": 0.007218144718582842
},
"stacking": {
"v6.6": {
"pr_auc_val": 0.6474384440874665,
"optimal_threshold": 0.32116277663299964,
"f1_at_thr": 0.6333791329260092
},
"v6.7r2": {
"pr_auc_val": 0.6640333379409579,
"optimal_threshold": 0.3311218467281351,
"f1_at_thr": 0.6402777365656805
},
"delta_pr_auc": 0.016594893853491333
},
"n_train": 254234,
"n_val": 73785,
"elite_rate_train": 0.2665890478850193,
"elite_rate_val": 0.19013349596801518
}
BIN
View File
Binary file not shown.
-54
View File
@@ -1,54 +0,0 @@
feature,importance,importance_pct
rank_predicted_rounds,2126,6.86
amount_mean_20d,1476,4.76
price_cv,1376,4.44
range_compression_20d,1165,3.76
atr_pct,1105,3.56
drawdown_60d,1090,3.52
volume_ratio,1030,3.32
amount_trend_20d,998,3.22
price_entropy,888,2.86
rebound_from_low_60d,872,2.81
ma20_deviation_pct,868,2.8
obv_slope,842,2.72
trend_slope_60d,827,2.67
wick_ratio_20d,800,2.58
ma20_ma60_gap_pct,768,2.48
amplitude_cv,767,2.47
dist_to_grid_lower,757,2.44
range_position_60d,747,2.41
amp_cv_x_entropy,714,2.3
bb_width,699,2.25
ma60_deviation_pct,691,2.23
volatility_20d,680,2.19
volume_cv_20d,657,2.12
intraday_trend_strength,650,2.1
avg_daily_amp,643,2.07
dist_to_grid_upper,629,2.03
trend_slope_20d,626,2.02
amp_x_grid_vol,551,1.78
amount_cv_20d,550,1.77
grid_room_balance,537,1.73
trend_abs_slope_20d,504,1.63
grid_touch_count_60d,480,1.55
cross_freq_x_bb,399,1.29
amp_x_grid,383,1.24
near_grid_line_ratio_20d,277,0.89
close_reversal_count_20d,267,0.86
high_amp_days,250,0.81
turnover_proxy_20d,243,0.78
ln_float_mv,235,0.76
low_volume_days_20d,228,0.74
mv_vol_interact,223,0.72
small_cap_premium,218,0.7
grid_touch_count_20d,218,0.7
up_days_20d,218,0.7
down_days_20d,209,0.67
near_upper_boundary_risk,165,0.53
trend_consistency_20d,105,0.34
near_lower_boundary_risk,87,0.28
cross_freq_20d,86,0.28
rolling_grid_ratio_20d,64,0.21
usable_grid_count_lower,6,0.02
usable_grid_count_upper,6,0.02
grid_cross_density_20d,0,0.0
1 feature importance importance_pct
2 rank_predicted_rounds 2126 6.86
3 amount_mean_20d 1476 4.76
4 price_cv 1376 4.44
5 range_compression_20d 1165 3.76
6 atr_pct 1105 3.56
7 drawdown_60d 1090 3.52
8 volume_ratio 1030 3.32
9 amount_trend_20d 998 3.22
10 price_entropy 888 2.86
11 rebound_from_low_60d 872 2.81
12 ma20_deviation_pct 868 2.8
13 obv_slope 842 2.72
14 trend_slope_60d 827 2.67
15 wick_ratio_20d 800 2.58
16 ma20_ma60_gap_pct 768 2.48
17 amplitude_cv 767 2.47
18 dist_to_grid_lower 757 2.44
19 range_position_60d 747 2.41
20 amp_cv_x_entropy 714 2.3
21 bb_width 699 2.25
22 ma60_deviation_pct 691 2.23
23 volatility_20d 680 2.19
24 volume_cv_20d 657 2.12
25 intraday_trend_strength 650 2.1
26 avg_daily_amp 643 2.07
27 dist_to_grid_upper 629 2.03
28 trend_slope_20d 626 2.02
29 amp_x_grid_vol 551 1.78
30 amount_cv_20d 550 1.77
31 grid_room_balance 537 1.73
32 trend_abs_slope_20d 504 1.63
33 grid_touch_count_60d 480 1.55
34 cross_freq_x_bb 399 1.29
35 amp_x_grid 383 1.24
36 near_grid_line_ratio_20d 277 0.89
37 close_reversal_count_20d 267 0.86
38 high_amp_days 250 0.81
39 turnover_proxy_20d 243 0.78
40 ln_float_mv 235 0.76
41 low_volume_days_20d 228 0.74
42 mv_vol_interact 223 0.72
43 small_cap_premium 218 0.7
44 grid_touch_count_20d 218 0.7
45 up_days_20d 218 0.7
46 down_days_20d 209 0.67
47 near_upper_boundary_risk 165 0.53
48 trend_consistency_20d 105 0.34
49 near_lower_boundary_risk 87 0.28
50 cross_freq_20d 86 0.28
51 rolling_grid_ratio_20d 64 0.21
52 usable_grid_count_lower 6 0.02
53 usable_grid_count_upper 6 0.02
54 grid_cross_density_20d 0 0.0
-61
View File
@@ -1,61 +0,0 @@
{
"version": "v6.7r3",
"feature_version": "v3.4",
"architecture": "stacking_calibrated",
"data_source": "mysql://100.121.118.116:3306/grid_seeker_model_base",
"training_date": "2026-06-24T11:00:00.000000",
"n_stocks_total": 5378,
"n_stocks_after_filter": 1533,
"n_training_samples": 254234,
"window_days": 120,
"future_days": 60,
"step_days": 20,
"y_rounds_mean": 0.1998384357465777,
"y_rounds_median": 0.0,
"y_rounds_zero_rate": 0.7108340511263267,
"elite_rate": 26.66,
"rank": {
"cv_mae": 0.2053,
"cv_r2": 0.2258,
"spearman": 0.5896,
"best_params": {
"num_leaves": 63,
"min_child_samples": 30,
"max_depth": 7
},
"n_features": 56
},
"top": {
"cv_pr_auc": 0.6953,
"best_params": {
"num_leaves": 63,
"min_child_samples": 30,
"max_depth": -1
},
"n_features": 53
},
"stacking": {
"cv_pr_auc": 0.6640,
"best_params": {
"num_leaves": 31,
"min_child_samples": 20,
"max_depth": -1
},
"n_features": 55,
"optimal_threshold": 0.33
},
"backtest": {
"start": "2023-05-04",
"end": "2026-04-30",
"total_return_pct": 151.92,
"annual_return_pct": 36.46,
"annual_sharpe": 1.7404,
"max_drawdown_pct": -12.10,
"weekly_win_rate_pct": 59.48,
"final_value": 151154,
"rebalancing_frequency": "weekly",
"elimination_window": "2_weeks"
},
"previous_version": "v6.6",
"previous_version_backup": "models_backup_20260624_110913"
}
-127
View File
@@ -1,127 +0,0 @@
with open('core/ui/flet/app_v2.py', 'r', encoding='utf-8') as f:
content = f.read()
# Patch 1: Add lock alongside _score_refreshing init
old_init = " self._score_refreshing = False # 刷新锁"
new_init = " self._score_refreshing = False\n self._score_lock = threading.Lock()"
content = content.replace(old_init, new_init, 1)
print('Patch 1 (init):', 'OK' if old_init not in content else 'NOT FOUND')
# Patch 2: _prev_day - use lock instead of flag guard
old_prev = ''' def _prev_day(_):
if self._score_refreshing:
return
self._score_cur_date -= timedelta(days=1)
self._score_date_label.value = str(self._score_cur_date)
self._score_refreshing = True
self._score_nav_btns[0].disabled = True
self._score_nav_btns[1].disabled = True
self._score_nav_btns[2].disabled = True
self._refresh_scoring()'''
new_prev = ''' def _prev_day(_):
if not self._score_lock.acquire(blocking=False):
return
self._score_cur_date -= timedelta(days=1)
self._score_date_label.value = str(self._score_cur_date)
self._score_date_label.update()
self._score_refreshing = True
for btn in self._score_nav_btns:
btn.disabled = True
for btn in self._score_nav_btns:
btn.update()
self._refresh_scoring()'''
content = content.replace(old_prev, new_prev, 1)
print('Patch 2 (_prev_day):', 'OK' if old_prev not in content else 'NOT FOUND')
# Patch 3: _next_day - use lock
old_next = ''' def _next_day(_):
if self._score_refreshing:
return
if self._score_cur_date >= self._score_max_date:
return
self._score_cur_date += timedelta(days=1)
self._score_date_label.value = str(self._score_cur_date)
self._score_refreshing = True
self._score_nav_btns[0].disabled = True
self._score_nav_btns[1].disabled = True
self._score_nav_btns[2].disabled = True
self._refresh_scoring()'''
new_next = ''' def _next_day(_):
if not self._score_lock.acquire(blocking=False):
return
if self._score_cur_date >= self._score_max_date:
self._score_lock.release()
return
self._score_cur_date += timedelta(days=1)
self._score_date_label.value = str(self._score_cur_date)
self._score_date_label.update()
self._score_refreshing = True
for btn in self._score_nav_btns:
btn.disabled = True
for btn in self._score_nav_btns:
btn.update()
self._refresh_scoring()'''
content = content.replace(old_next, new_next, 1)
print('Patch 3 (_next_day):', 'OK' if old_next not in content else 'NOT FOUND')
# Patch 4: _today - use lock
old_today = ''' def _today(_):
if self._score_refreshing:
return
if self._score_cur_date >= self._score_max_date:
return
self._score_cur_date = self._score_max_date
self._score_date_label.value = str(self._score_cur_date)
self._score_refreshing = True
self._score_nav_btns[0].disabled = True
self._score_nav_btns[1].disabled = True
self._score_nav_btns[2].disabled = True
self._refresh_scoring()'''
new_today = ''' def _today(_):
if not self._score_lock.acquire(blocking=False):
return
if self._score_cur_date >= self._score_max_date:
self._score_lock.release()
return
self._score_cur_date = self._score_max_date
self._score_date_label.value = str(self._score_cur_date)
self._score_date_label.update()
self._score_refreshing = True
for btn in self._score_nav_btns:
btn.disabled = True
for btn in self._score_nav_btns:
btn.update()
self._refresh_scoring()'''
content = content.replace(old_today, new_today, 1)
print('Patch 4 (_today):', 'OK' if old_today not in content else 'NOT FOUND')
# Patch 5: _revert_nav_btns - release lock at end
old_revert = ''' def _revert_nav_btns(self):
"""重新启用导航按钮并解除刷新锁;已达上限日期时禁用'明天''今天'按钮"""
self._score_refreshing = False
at_max = self._score_cur_date >= self._score_max_date
for btn in self._score_nav_btns:
btn.disabled = False
if at_max:
self._score_nav_btns[1].disabled = True # next
self._score_nav_btns[2].disabled = True # today
for btn in self._score_nav_btns:
btn.update()'''
new_revert = ''' def _revert_nav_btns(self):
"""重新启用导航按钮并解除刷新锁;已达上限日期时禁用'明天''今天'按钮"""
self._score_refreshing = False
at_max = self._score_cur_date >= self._score_max_date
for btn in self._score_nav_btns:
btn.disabled = False
if at_max:
self._score_nav_btns[1].disabled = True # next
self._score_nav_btns[2].disabled = True # today
for btn in self._score_nav_btns:
btn.update()
self._score_lock.release()'''
content = content.replace(old_revert, new_revert, 1)
print('Patch 5 (_revert_nav_btns):', 'OK' if old_revert not in content else 'NOT FOUND')
with open('core/ui/flet/app_v2.py', 'w', encoding='utf-8') as f:
f.write(content)
print('Done writing')
+10 -50
View File
@@ -1,52 +1,12 @@
# coding:utf-8
"""
启动入口 — Flet UI
"""
import sys
import os
import subprocess
import ssl
import traceback
import tkinter as tk
from core.main_entry import MainEntry
# 修复 Windows 上 flet_desktop 子进程弹出控制台窗口的问题
_original_popen = subprocess.Popen
class Popen(_original_popen):
def __init__(self, *args, **kwargs):
if sys.platform == "win32" and "creationflags" not in kwargs:
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
super().__init__(*args, **kwargs)
subprocess.Popen = Popen
# PyInstaller 打包后,设置 FLET_VIEW_PATH 指向打包内的 Flet 客户端
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS
flet_client_path = os.path.join(base_path, '.flet', 'client', 'flet-desktop-full-0.85.3')
flet_exe = os.path.join(flet_client_path, 'flet', 'flet.exe')
log_file = os.path.join(os.path.dirname(sys.executable), 'startup_log.txt')
with open(log_file, 'w') as f:
f.write(f'base_path: {base_path}\n')
f.write(f'flet_client_path: {flet_client_path}\n')
f.write(f'flet_exe: {flet_exe}\n')
f.write(f'exists: {os.path.exists(flet_exe)}\n')
if os.path.exists(flet_exe):
f.write('Setting FLET_VIEW_PATH\n')
os.environ['FLET_VIEW_PATH'] = flet_client_path
f.write(f'FLET_VIEW_PATH: {os.environ.get("FLET_VIEW_PATH")}\n')
if hasattr(ssl, '_create_unverified_context'):
ssl._create_default_https_context = ssl._create_unverified_context
def excepthook(type, value, tb):
log_file = os.path.join(os.path.dirname(sys.executable), 'error_log.txt')
with open(log_file, 'w') as f:
f.write(''.join(traceback.format_exception(type, value, tb)))
sys.__excepthook__(type, value, tb)
sys.excepthook = excepthook
if __name__ == '__main__':
from core.ui.flet.app_v2 import run
run()
# 这是应用的启动入口程序,负责初始化并启动主窗口。
# 它创建一个Tkinter根窗口,实例化主窗口类MainBoardWindow
# 并调用其run方法启动主事件循环。
if __name__ == "__main__":
import tkinter as tk
root = tk.Tk()
app = MainEntry(root)
app.run()
+4 -4
View File
@@ -4,7 +4,7 @@ a = Analysis(
['starter.py'],
pathex=[],
binaries=[],
datas=[('xtquant/xtdata.ini', 'xtquant'), ('flet_desktop/app', 'flet_desktop/app')], # xtdata 依赖的配置文件
datas=[('config.ini', '.'), ('xtquant/xtdata.ini', 'xtquant')], # 明确包含配置文件和xtdata.ini
hiddenimports=['brotli', 'brotli.encoding'],
hookspath=[],
hooksconfig={},
@@ -24,8 +24,8 @@ exe = EXE(
name='神之一手',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
strip=True, # 去除调试符号
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
@@ -34,5 +34,5 @@ exe = EXE(
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='logo.ico',
icon='logo.png' # 添加图标文件
)