更新qmt代码
This commit is contained in:
527
main/factor/dragon_factor.py
Normal file
527
main/factor/dragon_factor.py
Normal file
@@ -0,0 +1,527 @@
|
||||
"""
|
||||
追涨策略专用因子模块
|
||||
包含:形态突破、筹码穿透、攻击资金流
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
from main.factor.all_factors import calculate_all_factors
|
||||
from main.factor.momentum_factors import ReturnFactor, VolatilityFactor
|
||||
from main.factor.money_flow_factors import AccumAccel, ChipLockin, CostSqueeze, FlowIntensityFactor, HighCostSelling, InstNetAccum, LGFlowFactor
|
||||
from main.factor.operator_framework import StockWiseFactor
|
||||
from main.factor.special_factors import VolumeRatioFactor
|
||||
from main.factor.technical_factors import CrossSectionalRankFactor, SMAFactor
|
||||
|
||||
# ==========================================
|
||||
# 第一类:形态与趋势突破因子 (Price & Trend)
|
||||
# ==========================================
|
||||
|
||||
class LimitUpGene(StockWiseFactor):
|
||||
"""
|
||||
涨停基因因子
|
||||
逻辑:寻找'准涨停'或'强势封板'的特征。
|
||||
追涨策略不仅看是否涨停,更看封板的坚决度和实体的饱满度。
|
||||
"""
|
||||
factor_id = "factor_limit_up_gene"
|
||||
required_factor_ids = ["close", "open", "high", "low", "up_limit"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name=self.factor_id,
|
||||
parameters={},
|
||||
required_factor_ids=self.required_factor_ids
|
||||
)
|
||||
|
||||
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
|
||||
close = g["close"]
|
||||
open_price = g["open"]
|
||||
high = g["high"]
|
||||
low = g["low"]
|
||||
up_limit = g["up_limit"]
|
||||
|
||||
# 1. 封板接近度:收盘价距离涨停价有多近 (1.0 表示封死涨停)
|
||||
# 注意:这里加 1e-6 是为了防止 up_limit 为 NaN (虽然理论上不该有)
|
||||
limit_proximity = close / (up_limit + 1e-6)
|
||||
|
||||
# 2. 实体饱满度:(收盘-开盘) / (最高-最低)。越接近1,说明光头光脚,多头越强。
|
||||
range_len = high - low
|
||||
body_len = close - open_price
|
||||
body_strength = body_len / (range_len + 1e-6)
|
||||
|
||||
# 3. 极值修正:如果是涨停,body_strength 可能会失效(一字板),给予最高分
|
||||
# 逻辑:如果封板 (limit_proximity > 0.99) 且 是一字板 (range_len 极小),给强分
|
||||
|
||||
# 综合打分:封板接近度 * 实体强度
|
||||
score = limit_proximity * body_strength
|
||||
|
||||
# 稳态化
|
||||
return score.alias(self.factor_id)
|
||||
|
||||
|
||||
class TrendBreakout(StockWiseFactor):
|
||||
"""
|
||||
历史新高突破因子
|
||||
逻辑:股价越接近历史新高(his_high),上方的套牢盘越少,拉升阻力越小。
|
||||
"""
|
||||
factor_id = "factor_trend_breakout"
|
||||
required_factor_ids = ["close", "his_high", "his_low"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name=self.factor_id,
|
||||
parameters={},
|
||||
required_factor_ids=self.required_factor_ids
|
||||
)
|
||||
|
||||
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
|
||||
close = g["close"]
|
||||
his_high = g["his_high"]
|
||||
|
||||
# 距离历史新高的比率 (接近1表示即将突破或已突破)
|
||||
# 注意:数据清洗时需确保 his_high 有效
|
||||
dist_to_high = close / (his_high + 1e-6)
|
||||
|
||||
# 动量加速:当前价格相对于5日前的涨幅
|
||||
# 注意:这里假设 group_df 是按时间排序的单只股票数据
|
||||
mom_5 = close / close.shift(5).fill_null(strategy="forward") - 1.0
|
||||
|
||||
# 核心逻辑:只有在接近新高时的动量才有效
|
||||
breakout_score = dist_to_high * (1 + mom_5)
|
||||
|
||||
return breakout_score.log1p().alias(self.factor_id)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 第二类:筹码穿透与真空因子 (Chip Structure)
|
||||
# ==========================================
|
||||
|
||||
class ChipPenetration(StockWiseFactor):
|
||||
"""
|
||||
筹码穿透率因子 (Blue Sky Factor)
|
||||
逻辑:收盘价强力穿透95%筹码成本线,意味着上方进入'真空区',
|
||||
此时所有持筹者都获利,抛压最小。
|
||||
"""
|
||||
factor_id = "factor_chip_penetration"
|
||||
required_factor_ids = ["close", "cost_50pct", "cost_95pct", "vol"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name=self.factor_id,
|
||||
parameters={},
|
||||
required_factor_ids=self.required_factor_ids
|
||||
)
|
||||
|
||||
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
|
||||
close = g["close"]
|
||||
cost95 = g["cost_95pct"]
|
||||
cost50 = g["cost_50pct"]
|
||||
vol = g["vol"]
|
||||
|
||||
# 1. 穿透度:当前价格相对于95%成本线的位置
|
||||
# > 0 表示突破,数值越大突破越强
|
||||
penetration = (close - cost95) / (cost95 + 1e-6)
|
||||
|
||||
# 2. 放量确认:突破必须伴随放量
|
||||
vol_5d = vol.rolling_mean(window_size=5, min_periods=1)
|
||||
vol_ratio = vol / (vol_5d + 1e-6)
|
||||
|
||||
# 逻辑:只有放量的突破才是真突破
|
||||
# 使用 sigmoid 类似的逻辑平滑 volume 影响
|
||||
valid_breakout = penetration * pl.when(vol_ratio > 1.0).then(vol_ratio.log1p()).otherwise(0.5)
|
||||
|
||||
return valid_breakout.alias(self.factor_id)
|
||||
|
||||
|
||||
class WinnerExpansion(StockWiseFactor):
|
||||
"""
|
||||
获利盘扩张速率因子
|
||||
逻辑:追涨最核心的动力来源是'赚钱效应'的快速扩散。
|
||||
如果我们无法直接获得 winner_rate,可以通过 cost 分布估算。
|
||||
"""
|
||||
factor_id = "factor_winner_expansion"
|
||||
required_factor_ids = ["close", "cost_5pct", "cost_95pct"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name=self.factor_id,
|
||||
parameters={},
|
||||
required_factor_ids=self.required_factor_ids
|
||||
)
|
||||
|
||||
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
|
||||
close = g["close"]
|
||||
cost5 = g["cost_5pct"]
|
||||
cost95 = g["cost_95pct"]
|
||||
|
||||
# 简易估算获利盘比例 (0~1)
|
||||
# 假设筹码在 cost5 到 cost95 之间均匀分布
|
||||
chip_range = cost95 - cost5
|
||||
winner_proxy = (close - cost5) / (chip_range + 1e-6)
|
||||
# 截断到 0-1
|
||||
winner_proxy = winner_proxy.clip(0.0, 1.0)
|
||||
|
||||
# 计算获利盘的变化率 (一阶差分)
|
||||
expansion_rate = winner_proxy.diff()
|
||||
|
||||
# 逻辑:我们要找的是获利盘突然急剧增加的时刻 (爆拉脱离成本区)
|
||||
return expansion_rate.fill_null(0.0).alias(self.factor_id)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 第三类:攻击型资金流因子 (Attack Flow)
|
||||
# ==========================================
|
||||
|
||||
class AttackFlow(StockWiseFactor):
|
||||
"""
|
||||
主力攻击流因子
|
||||
逻辑:区别于普通的净流入,我们只关注'上涨过程中的'主力买入。
|
||||
如果股价下跌而主力流入,可能是左侧抄底(不适合追涨);
|
||||
如果股价上涨且主力大幅流入,这是右侧点火(适合追涨)。
|
||||
"""
|
||||
factor_id = "factor_attack_flow"
|
||||
required_factor_ids = ["close", "open", "buy_lg_vol", "buy_elg_vol", "sell_lg_vol", "sell_elg_vol", "circ_mv"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name=self.factor_id,
|
||||
parameters={},
|
||||
required_factor_ids=self.required_factor_ids
|
||||
)
|
||||
|
||||
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
|
||||
close = g["close"]
|
||||
open_price = g["open"]
|
||||
buy_elg = g["buy_elg_vol"]
|
||||
buy_lg = g["buy_lg_vol"]
|
||||
sell_elg = g["sell_elg_vol"]
|
||||
sell_lg = g["sell_lg_vol"]
|
||||
circ_mv = g["circ_mv"]
|
||||
|
||||
# 1. 计算大单净额
|
||||
main_net = (buy_elg + buy_lg) - (sell_elg + sell_lg)
|
||||
|
||||
# 2. 归一化 (换手率视角)
|
||||
circ_shares = (circ_mv * 10000) / (close + 1e-6) # 假设 circ_mv 单位万元
|
||||
net_rate = main_net / (circ_shares + 1e-6)
|
||||
|
||||
# 3. 价格强度权重
|
||||
# 如果是阳线 (Close > Open),权重为正且放大;阴线权重为 0 或 负
|
||||
price_strength = (close - open_price) / (open_price + 1e-6)
|
||||
|
||||
# 核心逻辑:资金流 * 价格涨幅
|
||||
# 只有当 资金大幅净流入 AND 价格大涨 时,该因子才会有极高值
|
||||
attack_score = net_rate * price_strength
|
||||
|
||||
# 只保留正向攻击 (负向代表出货或洗盘,暂不计入追涨分)
|
||||
attack_score = pl.when(attack_score > 0).then(attack_score).otherwise(0.0)
|
||||
|
||||
return attack_score.log1p().alias(self.factor_id)
|
||||
|
||||
|
||||
class DivergenceAlert(StockWiseFactor):
|
||||
"""
|
||||
量价/资金背离因子 (负面因子)
|
||||
逻辑:价格在涨,但主力在跑。这是追涨的大忌。
|
||||
用于过滤掉诱多陷阱。
|
||||
"""
|
||||
factor_id = "factor_divergence_alert"
|
||||
required_factor_ids = ["close", "open", "buy_lg_vol", "buy_elg_vol", "sell_lg_vol", "sell_elg_vol", "vol"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name=self.factor_id,
|
||||
parameters={},
|
||||
required_factor_ids=self.required_factor_ids
|
||||
)
|
||||
|
||||
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
|
||||
close = g["close"]
|
||||
open_price = g["open"]
|
||||
vol = g["vol"]
|
||||
|
||||
# 1. 价格涨跌幅
|
||||
pct_change = (close - open_price) / open_price
|
||||
|
||||
# 2. 主力资金净占比
|
||||
main_net = (g["buy_elg_vol"] + g["buy_lg_vol"]) - (g["sell_elg_vol"] + g["sell_lg_vol"])
|
||||
flow_ratio = main_net / (vol + 1e-6)
|
||||
|
||||
# 3. 背离识别
|
||||
# 情况A (诱多):价格大涨 (pct > 2%) 但 主力净流出 (flow < -0.1)
|
||||
trap_signal = (pct_change > 0.02) & (flow_ratio < -0.1)
|
||||
|
||||
# 转换为因子值:背离越严重,值越负
|
||||
# 正常情况给0,背离情况给负分
|
||||
factor = pl.when(trap_signal).then(flow_ratio * 10).otherwise(0.0)
|
||||
|
||||
return factor.alias(self.factor_id)
|
||||
|
||||
class PriceGammaFactor(StockWiseFactor):
|
||||
"""
|
||||
价格加速度因子 (Gamma)
|
||||
逻辑:识别加速上涨的股票。
|
||||
比如:前天涨1%,昨天涨3%,今天涨8% -> 加速度极高 -> 适合追涨。
|
||||
"""
|
||||
factor_id = "factor_price_gamma"
|
||||
required_factor_ids = ["close"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name=self.factor_id, parameters={}, required_factor_ids=self.required_factor_ids)
|
||||
|
||||
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
|
||||
close = g["close"]
|
||||
# 1. 计算每日收益率
|
||||
ret = close.pct_change()
|
||||
|
||||
# 2. 计算收益率的变化率 (即加速度)
|
||||
# 追涨策略看重短期的爆发,比如最近3天
|
||||
# 使用线性回归斜率或者简单的差分来近似
|
||||
|
||||
# 简单版:(Ret_T - Ret_T-2)
|
||||
accel = ret - ret.shift(2)
|
||||
|
||||
# 进阶版:只关注正向加速 (负向加速不重要,那是下跌或回调)
|
||||
# 如果收益率是负的,直接给低分
|
||||
score = pl.when(ret > 0).then(accel).otherwise(-1.0)
|
||||
|
||||
return score.alias(self.factor_id)
|
||||
|
||||
class TrendEfficiencyFactor(StockWiseFactor):
|
||||
"""
|
||||
趋势效率因子 (ER - Efficiency Ratio)
|
||||
逻辑:位移 / 路程。
|
||||
数值越接近 1.0,说明走势越像一根直线(单边拉升),追涨胜率越高。
|
||||
"""
|
||||
factor_id = "factor_trend_efficiency"
|
||||
required_factor_ids = ["close"]
|
||||
|
||||
def __init__(self, window=10):
|
||||
super().__init__(name=self.factor_id, parameters={"window": window}, required_factor_ids=self.required_factor_ids)
|
||||
|
||||
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
|
||||
close = g["close"]
|
||||
window = self.parameters["window"]
|
||||
|
||||
# 1. 总位移 (Change): |Price_T - Price_T-n|
|
||||
change = (close - close.shift(window)).abs()
|
||||
|
||||
# 2. 总路程 (Path): sum(|Price_t - Price_t-1|)
|
||||
# 也就是每一天波动的绝对值之和
|
||||
path = (close - close.shift(1)).abs().rolling_sum(window)
|
||||
|
||||
# 3. 效率 = 位移 / 路程
|
||||
efficiency = change / (path + 1e-6)
|
||||
|
||||
return efficiency.alias(self.factor_id)
|
||||
|
||||
class MoneyUrgencyFactor(StockWiseFactor):
|
||||
"""
|
||||
资金饥渴度因子
|
||||
逻辑:量比 * 大单主动买入占比
|
||||
"""
|
||||
factor_id = "factor_money_urgency"
|
||||
required_factor_ids = ["vol", "buy_lg_vol", "buy_elg_vol", "circ_mv"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name=self.factor_id, parameters={}, required_factor_ids=self.required_factor_ids)
|
||||
|
||||
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
|
||||
vol = g["vol"]
|
||||
big_buy = g["buy_lg_vol"] + g["buy_elg_vol"]
|
||||
|
||||
# 1. 量比 (Volume Ratio): 今日量 / 过去5日均量
|
||||
vol_ma5 = vol.rolling_mean(5).shift(1) # 注意避开未来函数,分母用T-1及之前
|
||||
vol_ratio = vol / (vol_ma5 + 1e-6)
|
||||
|
||||
# 2. 攻击性买入占比 (Aggressive Buy Ratio)
|
||||
attack_ratio = big_buy / (vol + 1e-6)
|
||||
|
||||
# 3. 共振:只有放量且主力大买,才是追涨信号
|
||||
# 如果缩量大买(可能是控盘),如果放量大卖(出货)
|
||||
# 这里用乘法放大共振效应
|
||||
urgency = vol_ratio * attack_ratio
|
||||
|
||||
return urgency.alias(self.factor_id)
|
||||
|
||||
from typing import List, Dict, Any
|
||||
import polars as pl
|
||||
|
||||
# 假设你之前的因子定义都在这些模块中
|
||||
# from main.factor.chasing_factors import (
|
||||
# LimitUpGene, TrendBreakout, # 形态类
|
||||
# ChipPenetration, WinnerExpansion, # 筹码类
|
||||
# AttackFlow, DivergenceAlert # 资金类
|
||||
# )
|
||||
# from main.factor.fund_flow import LGFlowFactor, CostSqueeze, ... # 原有因子
|
||||
# from main.factor.common import CrossSectionalRankFactor, SMAFactor ...
|
||||
|
||||
# def run_chasing_strategy_pipeline(df: pl.DataFrame) -> pl.DataFrame:
|
||||
# """
|
||||
# 执行【追涨/打板策略】的因子计算流程
|
||||
|
||||
# 该函数会组合:
|
||||
# 1. 基础动量与波动率因子 (Base)
|
||||
# 2. 资金流与筹码原有因子 (Legacy)
|
||||
# 3. 新增的追涨专用因子 (New Chasing Factors)
|
||||
# 4. 截面Rank因子 (用于选股排序)
|
||||
|
||||
# Returns:
|
||||
# df_result: 包含所有因子列的 DataFrame
|
||||
# """
|
||||
|
||||
# # =======================================================
|
||||
# # 1. 配置股票截面因子 (Stock Wise)
|
||||
# # =======================================================
|
||||
# stock_configs = [
|
||||
# # --- A. 新增:追涨核心因子 (Priority High) ---
|
||||
# {"class": LimitUpGene, "params": {}}, # 涨停基因 (形态)
|
||||
# {"class": TrendBreakout, "params": {}}, # 趋势突破 (形态)
|
||||
# {"class": ChipPenetration, "params": {}}, # 筹码穿透 (筹码)
|
||||
# {"class": WinnerExpansion, "params": {}}, # 获利盘扩张 (筹码)
|
||||
# {"class": AttackFlow, "params": {}}, # 攻击资金流 (资金)
|
||||
# {"class": DivergenceAlert, "params": {}}, # 顶背离警示 (风控)
|
||||
|
||||
# # --- B. 保留:原有高价值因子 (用于辅助验证) ---
|
||||
# # 资金流强度
|
||||
# {"class": FlowIntensityFactor, "params": {}},
|
||||
# {"class": LGFlowFactor, "params": {}},
|
||||
# {"class": InstNetAccum, "params": {}}, # 机构净累积
|
||||
# {"class": AccumAccel, "params": {}}, # 累积加速
|
||||
|
||||
# # 筹码结构
|
||||
# {"class": CostSqueeze, "params": {}}, # 成本挤压 (用于低位启动判断)
|
||||
# {"class": ChipLockin, "params": {}}, # 筹码锁定
|
||||
# {"class": HighCostSelling, "params": {}}, # 高位抛压 (用于风控)
|
||||
|
||||
# # --- C. 基础:技术指标 (用于过滤) ---
|
||||
# {"class": SMAFactor, "params": {"window": 5}}, # 5日线防守
|
||||
# {"class": SMAFactor, "params": {"window": 20}}, # 趋势判断
|
||||
# {"class": VolatilityFactor, "params": {"period": 10}}, # 波动率(剔除织布机)
|
||||
# {"class": ReturnFactor, "params": {"period": 5}}, # 5日涨幅
|
||||
# {"class": ReturnFactor, "params": {"period": 20}}, # 月涨幅
|
||||
# {"class": VolumeRatioFactor, "params": {}}, # 量比
|
||||
# ]
|
||||
|
||||
# # =======================================================
|
||||
# # 2. 配置日期截面因子 (Date Wise / Cross Sectional)
|
||||
# # =======================================================
|
||||
# # 追涨策略的核心在于:买入全市场最强的票。
|
||||
# # 因此,我们需要对核心因子进行截面排序 (Rank)。
|
||||
|
||||
# date_configs = [
|
||||
# # --- 基础排序 ---
|
||||
# {"class": CrossSectionalRankFactor, "params": {"column": "circ_mv", "name": "rank_mv"}}, # 市值排序(剔除微盘)
|
||||
# {"class": CrossSectionalRankFactor, "params": {"column": "return_5", "name": "rank_ret5"}}, # 短期强度排序
|
||||
|
||||
# # --- 策略核心排序 (重要!) ---
|
||||
# # 1. 攻击力排序:全市场谁的主力攻击性最强?
|
||||
# {
|
||||
# "class": CrossSectionalRankFactor,
|
||||
# "params": {"column": "factor_attack_flow", "name": "rank_attack_flow"}
|
||||
# },
|
||||
# # 2. 突破度排序:全市场谁的上方真空度最高?
|
||||
# {
|
||||
# "class": CrossSectionalRankFactor,
|
||||
# "params": {"column": "factor_chip_penetration", "name": "rank_chip_penetration"}
|
||||
# },
|
||||
# # 3. 涨停基因排序:全市场谁的板最硬?
|
||||
# {
|
||||
# "class": CrossSectionalRankFactor,
|
||||
# "params": {"column": "factor_limit_up_gene", "name": "rank_limit_gene"}
|
||||
# },
|
||||
# # 4. 资金流强度排序
|
||||
# {
|
||||
# "class": CrossSectionalRankFactor,
|
||||
# "params": {"column": "flow_intensity", "name": "rank_flow_intensity"}
|
||||
# }
|
||||
# ]
|
||||
|
||||
# # =======================================================
|
||||
# # 3. 调用统一计算接口
|
||||
# # =======================================================
|
||||
# print(f"开始计算追涨策略因子... 包含 {len(stock_configs)} 个股票因子配置")
|
||||
|
||||
# # 调用你提供的 calculate_all_factors
|
||||
# # 注意:这里会覆盖函数内部的默认 list,只计算我们指定的
|
||||
# result_df, factor_ids = calculate_all_factors(
|
||||
# df=df,
|
||||
# stock_factor_configs=stock_configs,
|
||||
# date_factor_configs=date_configs
|
||||
# )
|
||||
|
||||
# print(f"计算完成。生成因子列: {factor_ids}")
|
||||
# return result_df, factor_ids
|
||||
|
||||
def run_chasing_strategy_pipeline(df: pl.DataFrame) -> pl.DataFrame:
|
||||
|
||||
stock_configs = [
|
||||
{"class": LimitUpGene, "params": {}}, # 涨停基因 (形态)
|
||||
{"class": TrendBreakout, "params": {}}, # 趋势突破 (形态)
|
||||
{"class": ChipPenetration, "params": {}}, # 筹码穿透 (筹码)
|
||||
{"class": WinnerExpansion, "params": {}}, # 获利盘扩张 (筹码)
|
||||
{"class": AttackFlow, "params": {}}, # 攻击资金流 (资金)
|
||||
{"class": DivergenceAlert, "params": {}}, # 顶背离警示 (风控)
|
||||
|
||||
# --- B. 保留:原有高价值因子 (用于辅助验证) ---
|
||||
# 资金流强度
|
||||
{"class": FlowIntensityFactor, "params": {}},
|
||||
{"class": LGFlowFactor, "params": {}},
|
||||
{"class": InstNetAccum, "params": {}}, # 机构净累积
|
||||
{"class": AccumAccel, "params": {}}, # 累积加速
|
||||
|
||||
# 筹码结构
|
||||
{"class": CostSqueeze, "params": {}}, # 成本挤压 (用于低位启动判断)
|
||||
{"class": ChipLockin, "params": {}}, # 筹码锁定
|
||||
{"class": HighCostSelling, "params": {}}, # 高位抛压 (用于风控)
|
||||
|
||||
# --- C. 基础:技术指标 (用于过滤) ---
|
||||
{"class": SMAFactor, "params": {"window": 5}}, # 5日线防守
|
||||
{"class": SMAFactor, "params": {"window": 20}}, # 趋势判断
|
||||
{"class": VolatilityFactor, "params": {"period": 10}}, # 波动率(剔除织布机)
|
||||
{"class": ReturnFactor, "params": {"period": 5}}, # 5日涨幅
|
||||
{"class": ReturnFactor, "params": {"period": 20}}, # 月涨幅
|
||||
{"class": VolumeRatioFactor, "params": {}}, # 量比
|
||||
# 1. 爆发力 (Gamma)
|
||||
{"class": PriceGammaFactor, "params": {}},
|
||||
|
||||
# 2. 纯粹度 (Efficiency)
|
||||
{"class": TrendEfficiencyFactor, "params": {"window": 10}},
|
||||
|
||||
# 3. 饥渴度 (Urgency)
|
||||
{"class": MoneyUrgencyFactor, "params": {}},
|
||||
|
||||
# # 4. 辅助:位置修正 (防止追在山顶)
|
||||
# # 使用相对位置,剔除已经翻倍的票
|
||||
# {"class": LowPositionStart, "params": {}},
|
||||
]
|
||||
|
||||
# 日期截面配置 (关键步骤)
|
||||
date_configs = [
|
||||
# 对三个核心因子进行排序
|
||||
{"class": CrossSectionalRankFactor, "params": {"column": "factor_price_gamma", "name": "rank_gamma"}},
|
||||
{"class": CrossSectionalRankFactor, "params": {"column": "factor_trend_efficiency_10", "name": "rank_eff"}},
|
||||
{"class": CrossSectionalRankFactor, "params": {"column": "factor_money_urgency", "name": "rank_urgency"}},
|
||||
]
|
||||
|
||||
# ... 执行计算 ...
|
||||
result_df, calc_feature = calculate_all_factors(df, stock_configs, date_configs)
|
||||
|
||||
# ==========================================
|
||||
# 核心差异点:如何利用因子选股?
|
||||
# ==========================================
|
||||
|
||||
# 不使用简单的加权求和,而是使用“漏斗筛选”或“极值乘积”
|
||||
# 模拟“规则型策略”的严格性
|
||||
|
||||
# result_df = result_df.with_columns([
|
||||
# (
|
||||
# # 逻辑:
|
||||
# # 1. 加速度要在市场前 10% (rank_gamma > 0.9)
|
||||
# # 2. 走势要非常丝滑 (rank_eff > 0.8)
|
||||
# # 3. 资金要非常急迫 (rank_urgency > 0.8)
|
||||
# # 4. 乘法效应:强者恒强
|
||||
# pl.col("rank_gamma_true_factor_price_gamma") * pl.col("rank_eff_true_factor_trend_efficiency_10") * pl.col("rank_urgency")
|
||||
# ).alias("aggressive_score")
|
||||
# ])
|
||||
|
||||
return result_df, calc_feature
|
||||
9635
main/train/Classify/Classify-Dragon.ipynb
Normal file
9635
main/train/Classify/Classify-Dragon.ipynb
Normal file
File diff suppressed because one or more lines are too long
109
qmt/api_server.py
Normal file
109
qmt/api_server.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# coding:utf-8
|
||||
import os
|
||||
import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from qmt_engine import QMTEngine, QMTStatus
|
||||
|
||||
|
||||
# ================= Pydantic模型 =================
|
||||
class StatusResponse(BaseModel):
|
||||
"""状态响应模型"""
|
||||
running: bool
|
||||
qmt_connected: bool
|
||||
start_time: str
|
||||
last_loop_update: str
|
||||
account_id: str
|
||||
|
||||
|
||||
class PositionsResponse(BaseModel):
|
||||
"""持仓响应模型"""
|
||||
real_positions: List[Dict[str, Any]]
|
||||
virtual_positions: Dict[str, Dict[str, str]]
|
||||
|
||||
|
||||
class LogsResponse(BaseModel):
|
||||
"""日志响应模型"""
|
||||
logs: List[str]
|
||||
|
||||
|
||||
# ================= FastAPI应用 =================
|
||||
class QMTAPIServer:
|
||||
"""QMT API服务器"""
|
||||
|
||||
def __init__(self, qmt_engine: QMTEngine):
|
||||
self.app = FastAPI(title="QMT Monitor")
|
||||
self.qmt_engine = qmt_engine
|
||||
self._setup_middleware()
|
||||
self._setup_routes()
|
||||
|
||||
def _setup_middleware(self):
|
||||
"""设置中间件"""
|
||||
self.app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
def _setup_routes(self):
|
||||
"""设置路由"""
|
||||
|
||||
@self.app.get("/", summary="仪表盘页面")
|
||||
async def read_root():
|
||||
"""返回仪表盘HTML页面"""
|
||||
if os.path.exists("dashboard.html"):
|
||||
return FileResponse("dashboard.html")
|
||||
return {"error": "Dashboard not found"}
|
||||
|
||||
@self.app.get("/api/status", response_model=StatusResponse, summary="获取系统状态")
|
||||
def get_status():
|
||||
"""获取QMT连接状态和系统信息"""
|
||||
status = self.qmt_engine.get_status()
|
||||
return StatusResponse(
|
||||
running=status.is_running,
|
||||
qmt_connected=status.is_connected,
|
||||
start_time=status.start_time,
|
||||
last_loop_update=status.last_heartbeat,
|
||||
account_id=status.account_id
|
||||
)
|
||||
|
||||
@self.app.get("/api/positions", response_model=PositionsResponse, summary="获取持仓信息")
|
||||
def get_positions():
|
||||
"""获取实盘和虚拟持仓信息"""
|
||||
positions = self.qmt_engine.get_positions()
|
||||
return PositionsResponse(
|
||||
real_positions=positions["real_positions"],
|
||||
virtual_positions=positions["virtual_positions"]
|
||||
)
|
||||
|
||||
@self.app.get("/api/logs", response_model=LogsResponse, summary="获取日志")
|
||||
def get_logs(lines: int = Query(50, ge=1, le=1000, description="返回日志行数")):
|
||||
"""获取最近的交易日志"""
|
||||
logs = self.qmt_engine.get_logs(lines)
|
||||
return LogsResponse(logs=logs)
|
||||
|
||||
@self.app.get("/api/health", summary="健康检查")
|
||||
def health_check():
|
||||
"""健康检查接口"""
|
||||
status = self.qmt_engine.get_status()
|
||||
if status.is_running and status.is_connected:
|
||||
return {"status": "healthy", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
else:
|
||||
return {"status": "unhealthy", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
|
||||
def get_app(self) -> FastAPI:
|
||||
"""获取FastAPI应用实例"""
|
||||
return self.app
|
||||
|
||||
|
||||
# ================= 辅助函数 =================
|
||||
def create_api_server(qmt_engine: QMTEngine) -> FastAPI:
|
||||
"""创建API服务器"""
|
||||
server = QMTAPIServer(qmt_engine)
|
||||
return server.get_app()
|
||||
53
qmt/main.py
Normal file
53
qmt/main.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# coding:utf-8
|
||||
import threading
|
||||
import sys
|
||||
import uvicorn
|
||||
|
||||
from .qmt_engine import QMTEngine
|
||||
from .api_server import create_api_server
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数 - 启动QMT交易引擎和API服务器"""
|
||||
print(">>> 系统正在启动...")
|
||||
|
||||
# 创建QMT引擎实例
|
||||
engine = QMTEngine()
|
||||
|
||||
try:
|
||||
# 初始化引擎
|
||||
engine.initialize('config.json')
|
||||
print("✅ QMT引擎初始化成功")
|
||||
except Exception as e:
|
||||
print(f"❌ QMT引擎初始化失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 启动交易线程
|
||||
trading_thread = threading.Thread(target=engine.run_trading_loop, daemon=True)
|
||||
trading_thread.start()
|
||||
print("✅ 交易线程启动成功")
|
||||
|
||||
# 创建API服务器
|
||||
app = create_api_server(engine)
|
||||
print("✅ API服务器创建成功")
|
||||
|
||||
# 启动Web服务
|
||||
print(">>> Web服务启动: http://localhost:8001")
|
||||
try:
|
||||
uvicorn.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=8001,
|
||||
log_level="warning",
|
||||
access_log=False
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\n>>> 正在关闭系统...")
|
||||
engine.stop()
|
||||
print(">>> 系统已关闭")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 使用 -u 参数运行是最佳实践: python -u main.py
|
||||
# 但这里也在代码里强制 flush 了
|
||||
main()
|
||||
426
qmt/qmt_engine.py
Normal file
426
qmt/qmt_engine.py
Normal file
@@ -0,0 +1,426 @@
|
||||
# coding:utf-8
|
||||
import time
|
||||
import datetime
|
||||
import traceback
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
import redis
|
||||
from xtquant import xtdata
|
||||
from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
|
||||
from xtquant.xttype import StockAccount
|
||||
from xtquant import xtconstant
|
||||
|
||||
# ================= 0. Windows 控制台防卡死补丁 =================
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
# 禁用快速编辑模式 (0x0040),防止鼠标点击终端导致程序挂起
|
||||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 128)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class QMTStatus:
|
||||
"""系统状态封装类"""
|
||||
is_connected: bool
|
||||
start_time: str
|
||||
last_heartbeat: str
|
||||
account_id: str
|
||||
is_running: bool
|
||||
|
||||
|
||||
# ================= 1. 虚拟持仓与对账辅助类 =================
|
||||
|
||||
class PositionManager:
|
||||
"""Redis 持仓管理器:负责维护每个子策略的虚拟仓位"""
|
||||
|
||||
def __init__(self, r_client):
|
||||
self.r = r_client
|
||||
|
||||
def _get_key(self, strategy_name):
|
||||
return f"POS:{strategy_name}"
|
||||
|
||||
def mark_holding(self, strategy_name, code):
|
||||
"""下单时先在 Redis 占位(0股),占用一个槽位"""
|
||||
self.r.hsetnx(self._get_key(strategy_name), code, 0)
|
||||
|
||||
def rollback_holding(self, strategy_name, code):
|
||||
"""报单失败时回滚,释放 Redis 占位"""
|
||||
key = self._get_key(strategy_name)
|
||||
val = self.r.hget(key, code)
|
||||
if val is not None and int(val) == 0:
|
||||
self.r.hdel(key, code)
|
||||
|
||||
def update_actual_volume(self, strategy_name, code, delta_vol):
|
||||
"""成交回调时更新 Redis 实际股数"""
|
||||
key = self._get_key(strategy_name)
|
||||
new_vol = self.r.hincrby(key, code, int(delta_vol))
|
||||
if new_vol <= 0:
|
||||
self.r.hdel(key, code)
|
||||
new_vol = 0
|
||||
return new_vol
|
||||
|
||||
def get_position(self, strategy_name, code):
|
||||
"""获取某个策略下某只股票的虚拟持仓"""
|
||||
vol = self.r.hget(self._get_key(strategy_name), code)
|
||||
return int(vol) if vol else 0
|
||||
|
||||
def get_holding_count(self, strategy_name):
|
||||
"""获取当前策略已占用的槽位总数"""
|
||||
return self.r.hlen(self._get_key(strategy_name))
|
||||
|
||||
def get_all_virtual_positions(self, strategy_name):
|
||||
return self.r.hgetall(self._get_key(strategy_name))
|
||||
|
||||
def force_delete(self, strategy_name, code):
|
||||
self.r.hdel(self._get_key(strategy_name), code)
|
||||
|
||||
def clean_stale_placeholders(self, strategy_name, xt_trader, acc):
|
||||
"""清理长时间未成交且实盘无持仓的占位符"""
|
||||
try:
|
||||
key = self._get_key(strategy_name)
|
||||
all_pos = self.r.hgetall(key)
|
||||
if not all_pos: return
|
||||
|
||||
active_orders = xt_trader.query_stock_orders(acc, cancelable_only=True)
|
||||
active_codes = [o.stock_code for o in active_orders] if active_orders else []
|
||||
real_positions = xt_trader.query_stock_positions(acc)
|
||||
real_holdings = [p.stock_code for p in real_positions if p.volume > 0] if real_positions else []
|
||||
|
||||
for code, vol_str in all_pos.items():
|
||||
if int(vol_str) == 0:
|
||||
if (code not in real_holdings) and (code not in active_codes):
|
||||
self.r.hdel(key, code)
|
||||
except Exception as e:
|
||||
logging.getLogger("QMT_Engine").error(f"清理占位异常: {e}")
|
||||
|
||||
|
||||
class DailySettlement:
|
||||
"""收盘对账逻辑"""
|
||||
|
||||
def __init__(self, xt_trader, acc, pos_mgr, strategies_config):
|
||||
self.trader = xt_trader
|
||||
self.acc = acc
|
||||
self.pos_mgr = pos_mgr
|
||||
self.strategies_config = strategies_config
|
||||
self.has_settled = False
|
||||
|
||||
def run_settlement(self):
|
||||
"""收盘后强制同步 Redis 和实盘持仓"""
|
||||
real_positions = self.trader.query_stock_positions(self.acc)
|
||||
real_pos_map = {p.stock_code: p.volume for p in real_positions if p.volume > 0} if real_positions else {}
|
||||
|
||||
for strategy in self.strategies_config.keys():
|
||||
virtual = self.pos_mgr.get_all_virtual_positions(strategy)
|
||||
for code, v_str in virtual.items():
|
||||
v = int(v_str)
|
||||
if code not in real_pos_map:
|
||||
self.pos_mgr.force_delete(strategy, code)
|
||||
elif v == 0 and code in real_pos_map:
|
||||
self.pos_mgr.update_actual_volume(strategy, code, real_pos_map[code])
|
||||
self.has_settled = True
|
||||
|
||||
|
||||
# ================= 2. QMT 核心引擎 =================
|
||||
|
||||
class MyXtQuantTraderCallback(XtQuantTraderCallback):
|
||||
"""交易回调事件监听"""
|
||||
|
||||
def __init__(self, pos_mgr):
|
||||
self.pos_mgr = pos_mgr
|
||||
self.is_connected = False
|
||||
self.logger = logging.getLogger("QMT_Engine")
|
||||
|
||||
def on_disconnected(self):
|
||||
self.logger.warning(">> 回调通知: 交易端连接断开")
|
||||
self.is_connected = False
|
||||
|
||||
def on_stock_trade(self, trade):
|
||||
try:
|
||||
# QMTEngine 是单例,可直接通过类访问
|
||||
cache_info = QMTEngine().order_cache.get(trade.order_id)
|
||||
if not cache_info: return
|
||||
strategy, _, action = cache_info
|
||||
self.logger.info(f">>> [成交] {strategy} | {trade.stock_code} | {trade.traded_volume}股")
|
||||
if action == 'BUY':
|
||||
self.pos_mgr.update_actual_volume(strategy, trade.stock_code, trade.traded_volume)
|
||||
elif action == 'SELL':
|
||||
self.pos_mgr.update_actual_volume(strategy, trade.stock_code, -trade.traded_volume)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
def on_order_error(self, err):
|
||||
try:
|
||||
self.logger.error(f"下单失败回调: {err.error_msg} ID:{err.order_id}")
|
||||
cache = QMTEngine().order_cache.get(err.order_id)
|
||||
if cache and cache[2] == 'BUY':
|
||||
self.pos_mgr.rollback_holding(cache[0], cache[1])
|
||||
if err.order_id in QMTEngine().order_cache:
|
||||
del QMTEngine().order_cache[err.order_id]
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class QMTEngine:
|
||||
"""QMT 交易引擎单例"""
|
||||
_instance = None
|
||||
_lock = threading.Lock()
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
with cls._lock:
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if hasattr(self, '_initialized'): return
|
||||
self.logger = None
|
||||
self.config = {}
|
||||
self.xt_trader = None
|
||||
self.acc = None
|
||||
self.pos_manager = None
|
||||
self.callback = None
|
||||
self.is_running = True
|
||||
self.start_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.last_heartbeat = "Initializing..."
|
||||
self.order_cache = {} # OrderID -> (Strategy, Code, Action)
|
||||
self.settler = None
|
||||
self._initialized = True
|
||||
|
||||
def initialize(self, config_file='config.json'):
|
||||
self._setup_logger()
|
||||
self.config = self._load_config(config_file)
|
||||
# 初始化 Redis
|
||||
try:
|
||||
self.redis_client = redis.Redis(**self.config['redis'], decode_responses=True)
|
||||
self.redis_client.ping()
|
||||
self.pos_manager = PositionManager(self.redis_client)
|
||||
self.logger.info("Redis 建立连接成功")
|
||||
except Exception as e:
|
||||
self.logger.critical(f"Redis 连接失败: {e}")
|
||||
raise
|
||||
self._reconnect_qmt()
|
||||
|
||||
def _setup_logger(self):
|
||||
log_dir = "logs"
|
||||
if not os.path.exists(log_dir): os.makedirs(log_dir)
|
||||
log_file = os.path.join(log_dir, f"{datetime.date.today().strftime('%Y-%m-%d')}.log")
|
||||
self.logger = logging.getLogger("QMT_Engine")
|
||||
self.logger.setLevel(logging.INFO)
|
||||
if self.logger.handlers:
|
||||
for h in self.logger.handlers[:]: h.close(); self.logger.removeHandler(h)
|
||||
fmt = logging.Formatter('[%(asctime)s] [%(levelname)s] [%(threadName)s] %(message)s', '%Y-%m-%d %H:%M:%S')
|
||||
fh = logging.FileHandler(log_file, mode='a', encoding='utf-8')
|
||||
fh.setFormatter(fmt)
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(fmt)
|
||||
self.logger.addHandler(fh);
|
||||
self.logger.addHandler(sh)
|
||||
|
||||
def _load_config(self, config_file):
|
||||
base = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(
|
||||
os.path.abspath(__file__))
|
||||
path = os.path.join(base, config_file)
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def _get_global_total_slots(self):
|
||||
"""本地引擎计算:所有策略总共分配了多少个槽位"""
|
||||
return sum(info.get('total_slots', 0) for info in self.config.get('strategies', {}).values())
|
||||
|
||||
def _get_execution_setting(self, strategy_name, key, default=None):
|
||||
"""扩展性配置读取:读取 execution 字典中的参数"""
|
||||
strat_cfg = self.config.get('strategies', {}).get(strategy_name, {})
|
||||
exec_cfg = strat_cfg.get('execution', {})
|
||||
return exec_cfg.get(key, default)
|
||||
|
||||
def _reconnect_qmt(self):
|
||||
q = self.config['qmt']
|
||||
if self.xt_trader:
|
||||
try:
|
||||
self.xt_trader.stop()
|
||||
except:
|
||||
pass
|
||||
self.xt_trader = XtQuantTrader(q['path'], int(time.time()))
|
||||
self.acc = StockAccount(q['account_id'], q['account_type'])
|
||||
self.callback = MyXtQuantTraderCallback(self.pos_manager)
|
||||
self.xt_trader.register_callback(self.callback)
|
||||
self.xt_trader.start()
|
||||
if self.xt_trader.connect() == 0:
|
||||
self.xt_trader.subscribe(self.acc)
|
||||
self.callback.is_connected = True
|
||||
self.settler = DailySettlement(self.xt_trader, self.acc, self.pos_manager, self.config['strategies'])
|
||||
for s in self.config['strategies'].keys():
|
||||
self.pos_manager.clean_stale_placeholders(s, self.xt_trader, self.acc)
|
||||
self.logger.info("✅ QMT 终端连接成功")
|
||||
return True
|
||||
return False
|
||||
|
||||
def process_strategy_queue(self, strategy_name):
|
||||
"""处理 Redis 中的策略信号"""
|
||||
queue_key = f"{strategy_name}_real"
|
||||
msg_json = self.redis_client.lpop(queue_key)
|
||||
if not msg_json: return
|
||||
try:
|
||||
self.redis_client.rpush(f"{queue_key}:history", msg_json)
|
||||
data = json.loads(msg_json)
|
||||
if data.get('is_backtest'): return
|
||||
today = datetime.date.today().strftime('%Y-%m-%d')
|
||||
if data.get('timestamp', '').split(' ')[0] != today: return
|
||||
|
||||
action = data.get('action')
|
||||
stock = data.get('stock_code')
|
||||
price = float(data.get('price', 0))
|
||||
msg_slots = int(data.get('total_slots', 0))
|
||||
|
||||
if action == 'BUY':
|
||||
self._process_buy(strategy_name, stock, price, msg_slots)
|
||||
elif action == 'SELL':
|
||||
self._process_sell(strategy_name, stock, price)
|
||||
except Exception as e:
|
||||
self.logger.error(f"消息解析异常: {e}")
|
||||
|
||||
def _process_buy(self, strategy_name, stock_code, price, msg_slots):
|
||||
"""核心开仓逻辑"""
|
||||
# 1. 验证配置
|
||||
strat_cfg = self.config.get('strategies', {}).get(strategy_name)
|
||||
if not strat_cfg: return
|
||||
local_slots = strat_cfg.get('total_slots', 0)
|
||||
|
||||
# 2. 安全校验:信号槽位与本地实盘配置必须严格一致
|
||||
if msg_slots != local_slots:
|
||||
self.logger.error(f"⚠️ [{strategy_name}] 槽位不匹配!拒绝下单。信号预期:{msg_slots} | 本地配置:{local_slots}")
|
||||
return
|
||||
|
||||
# 3. 检查子策略占用
|
||||
if self.pos_manager.get_holding_count(strategy_name) >= local_slots:
|
||||
self.logger.warning(f"[{strategy_name}] 槽位已满,拦截买入 {stock_code}")
|
||||
return
|
||||
|
||||
# 4. 资金计算(由本地引擎统筹全局)
|
||||
try:
|
||||
asset = self.xt_trader.query_stock_asset(self.acc)
|
||||
global_total = self._get_global_total_slots()
|
||||
if not asset or global_total <= 0: return
|
||||
|
||||
# 单笔预算 = (总资产现金 + 持仓市值) / 全局总槽位
|
||||
total_equity = asset.cash + asset.market_value
|
||||
target_amt = total_equity / global_total
|
||||
# 实际可用金额不超过现金的 98%(预留滑点/手续费)
|
||||
actual_amt = min(target_amt, asset.cash * 0.98)
|
||||
|
||||
if actual_amt < 2000:
|
||||
self.logger.warning(f"[{strategy_name}] 可用金额不足2000,取消买入 {stock_code}")
|
||||
return
|
||||
|
||||
# --- 价格偏移处理 ---
|
||||
offset = self._get_execution_setting(strategy_name, 'buy_price_offset', 0.0)
|
||||
final_price = round(price + offset, 3)
|
||||
|
||||
vol = int(actual_amt / (final_price if final_price > 0 else 1.0) / 100) * 100
|
||||
if vol < 100: return
|
||||
|
||||
oid = self.xt_trader.order_stock(self.acc, stock_code, xtconstant.STOCK_BUY, vol, xtconstant.FIX_PRICE,
|
||||
final_price, strategy_name, 'PyBuy')
|
||||
if oid != -1:
|
||||
self.logger.info(
|
||||
f"√√√ [{strategy_name}] 开仓下单: {stock_code} | 价格:{final_price}(加价:{offset}) | 数量:{vol}")
|
||||
self.order_cache[oid] = (strategy_name, stock_code, 'BUY')
|
||||
self.pos_manager.mark_holding(strategy_name, stock_code)
|
||||
else:
|
||||
self.logger.error(f"XXX [{strategy_name}] 开仓发单拒绝")
|
||||
except Exception as e:
|
||||
self.logger.error(f"买入异常: {e}", exc_info=True)
|
||||
|
||||
def _process_sell(self, strategy_name, stock_code, price):
|
||||
"""核心平仓逻辑"""
|
||||
v_vol = self.pos_manager.get_position(strategy_name, stock_code)
|
||||
if v_vol <= 0: return
|
||||
|
||||
real_pos = self.xt_trader.query_stock_positions(self.acc)
|
||||
rp = next((p for p in real_pos if p.stock_code == stock_code), None) if real_pos else None
|
||||
can_use = rp.can_use_volume if rp else 0
|
||||
|
||||
final_vol = min(v_vol, can_use)
|
||||
if final_vol <= 0:
|
||||
self.logger.warning(f"[{strategy_name}] {stock_code} 无可用平仓额度 (Redis:{v_vol}, 实盘:{can_use})")
|
||||
return
|
||||
|
||||
# --- 价格偏移处理 ---
|
||||
offset = self._get_execution_setting(strategy_name, 'sell_price_offset', 0.0)
|
||||
final_price = round(price + offset, 3)
|
||||
|
||||
oid = self.xt_trader.order_stock(self.acc, stock_code, xtconstant.STOCK_SELL, final_vol, xtconstant.FIX_PRICE,
|
||||
final_price, strategy_name, 'PySell')
|
||||
if oid != -1:
|
||||
self.logger.info(
|
||||
f"√√√ [{strategy_name}] 平仓下单: {stock_code} | 价格:{final_price}(偏移:{offset}) | 数量:{final_vol}")
|
||||
self.order_cache[oid] = (strategy_name, stock_code, 'SELL')
|
||||
|
||||
def run_trading_loop(self):
|
||||
"""交易主线程循环"""
|
||||
self.logger.info(">>> 交易主循环子线程已启动 <<<")
|
||||
last_check = 0
|
||||
while self.is_running:
|
||||
try:
|
||||
self.last_heartbeat = datetime.datetime.now().strftime('%H:%M:%S')
|
||||
# 健康检查
|
||||
if time.time() - last_check > 15:
|
||||
last_check = time.time()
|
||||
try:
|
||||
if not (self.xt_trader and self.acc and self.xt_trader.query_stock_asset(self.acc)):
|
||||
self._reconnect_qmt()
|
||||
except:
|
||||
self._reconnect_qmt()
|
||||
|
||||
# 交易时间判断
|
||||
curr = datetime.datetime.now().strftime('%H%M%S')
|
||||
is_trading = ('091500' <= curr <= '113000') or ('130000' <= curr <= '150000')
|
||||
|
||||
if is_trading and self.callback and self.callback.is_connected:
|
||||
if self.settler: self.settler.reset_flag()
|
||||
for s in self.config.get('strategies', {}).keys():
|
||||
self.process_strategy_queue(s)
|
||||
elif '150500' <= curr <= '151500' and self.settler and not self.settler.has_settled:
|
||||
self.settler.run_settlement()
|
||||
|
||||
time.sleep(1 if is_trading else 5)
|
||||
except Exception as e:
|
||||
self.logger.error(f"主循环异常: {e}")
|
||||
time.sleep(10)
|
||||
|
||||
# ================= 外部接口 =================
|
||||
|
||||
def get_status(self) -> QMTStatus:
|
||||
conn = self.callback.is_connected if self.callback else False
|
||||
return QMTStatus(conn, self.start_time, self.last_heartbeat,
|
||||
self.acc.account_id if self.acc else "Unknown", self.is_running)
|
||||
|
||||
def get_positions(self) -> Dict[str, Any]:
|
||||
real = []
|
||||
if self.callback and self.callback.is_connected:
|
||||
pos = self.xt_trader.query_stock_positions(self.acc)
|
||||
if pos:
|
||||
real = [{"code": p.stock_code, "volume": p.volume, "can_use": p.can_use_volume, "value": p.market_value}
|
||||
for p in pos if p.volume > 0]
|
||||
virtual = {s: self.pos_manager.get_all_virtual_positions(s) for s in self.config.get('strategies', {}).keys()}
|
||||
return {"real_positions": real, "virtual_positions": virtual}
|
||||
|
||||
def get_logs(self, lines=50):
|
||||
log_path = os.path.join("logs", f"{datetime.date.today().strftime('%Y-%m-%d')}.log")
|
||||
if not os.path.exists(log_path): return ["今日暂无日志"]
|
||||
with open(log_path, 'r', encoding='utf-8') as f:
|
||||
return [l.strip() for l in f.readlines()[-lines:]]
|
||||
|
||||
def stop(self):
|
||||
self.is_running = False
|
||||
self.logger.info("收到引擎停止指令")
|
||||
@@ -1,4 +1,5 @@
|
||||
from xtquant import xttrader
|
||||
from xtquant.xtdata import download_history_data, get_market_data
|
||||
from xtquant.xttype import StockAccount
|
||||
import random
|
||||
|
||||
@@ -31,5 +32,5 @@ else:
|
||||
print('订阅失败')
|
||||
|
||||
|
||||
asset = xt_trader.query_stock_asset(account)
|
||||
print(asset.cash)
|
||||
download_history_data('000001.SZ', '1m', start_time='20251201', end_time='')
|
||||
print(get_market_data(stock_list=['000001.SZ'], period='1m', start_time='20251201', end_time=''))
|
||||
|
||||
64
qmt/run.py
Normal file
64
qmt/run.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# coding:utf-8
|
||||
"""
|
||||
QMT交易系统启动器
|
||||
用于直接运行,避免包导入问题
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 将当前目录添加到Python路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if current_dir not in sys.path:
|
||||
sys.path.insert(0, current_dir)
|
||||
|
||||
# 导入模块
|
||||
from qmt_engine import QMTEngine
|
||||
from api_server import create_api_server
|
||||
import threading
|
||||
import uvicorn
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数 - 启动QMT交易引擎和API服务器"""
|
||||
print(">>> 系统正在启动...")
|
||||
|
||||
# 创建QMT引擎实例
|
||||
engine = QMTEngine()
|
||||
|
||||
try:
|
||||
# 初始化引擎
|
||||
engine.initialize('config.json')
|
||||
print("✅ QMT引擎初始化成功")
|
||||
except Exception as e:
|
||||
print(f"❌ QMT引擎初始化失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 启动交易线程
|
||||
trading_thread = threading.Thread(target=engine.run_trading_loop, daemon=True)
|
||||
trading_thread.start()
|
||||
print("✅ 交易线程启动成功")
|
||||
|
||||
# 创建API服务器
|
||||
app = create_api_server(engine)
|
||||
print("✅ API服务器创建成功")
|
||||
|
||||
# 启动Web服务
|
||||
print(">>> Web服务启动: http://localhost:8001")
|
||||
try:
|
||||
uvicorn.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=8001,
|
||||
log_level="warning",
|
||||
access_log=False
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\n>>> 正在关闭系统...")
|
||||
engine.stop()
|
||||
print(">>> 系统已关闭")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 使用 -u 参数运行是最佳实践: python -u run.py
|
||||
# 但这里也在代码里强制 flush 了
|
||||
main()
|
||||
@@ -1,75 +1,57 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: ================= <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> =================
|
||||
:: <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ¼ (<28><>ȷ<EFBFBD><C8B7>·<EFBFBD><C2B7><EFBFBD><EFBFBD>ȷ)
|
||||
set "WORK_DIR=C:\Data\Project\NewStock\qmt"
|
||||
:: Python<EFBFBD>ű<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
set "SCRIPT_NAME=qmt_trader.py"
|
||||
:: <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD><EFBFBD><EFBFBD>
|
||||
set MAX_RETRIES=5
|
||||
:: <20><><EFBFBD>Լ<EFBFBD><D4BC><EFBFBD><EFBFBD><EFBFBD>
|
||||
:: ================= 配置选项 =================
|
||||
:: 1. 自动获取当前脚本所在目录作为工作目录
|
||||
set "WORK_DIR=%~dp0"
|
||||
:: 2. 设置启动文件(对应你拆分后的入口文件)
|
||||
set "SCRIPT_NAME=run.py"
|
||||
:: 3. 失败重启配置
|
||||
set MAX_RETRIES=10
|
||||
set RETRY_COUNT=0
|
||||
:: <20><><EFBFBD>Եȴ<D4B5>ʱ<EFBFBD><CAB1>(<28><>)
|
||||
set RETRY_WAIT=10
|
||||
:: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־Ŀ¼
|
||||
set "LOG_DIR=%WORK_DIR%\logs\launcher"
|
||||
set RETRY_WAIT=15
|
||||
:: 4. 日志配置
|
||||
set "LOG_DIR=%WORK_DIR%logs\launcher"
|
||||
:: ===========================================
|
||||
|
||||
:: 1. <20>л<EFBFBD><D0BB><EFBFBD><EFBFBD><EFBFBD>Ŀ¼
|
||||
:: 切换到工作目录
|
||||
cd /d "%WORK_DIR%"
|
||||
title QMT ʵ<EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD>ϵͳ [Port:8001]
|
||||
title QMT 自动化交易系统 [监控中]
|
||||
|
||||
:: 2. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־Ŀ¼
|
||||
:: 创建日志目录
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
:: <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>־<EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD> (<28><EFBFBD><F2B5A5B5><EFBFBD><EFBFBD>ڴ<EFBFBD><DAB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>䳣<EFBFBD><E4B3A3>Windows<77><73>ʽ)
|
||||
:: 获取当前日期用于日志命名
|
||||
set "TODAY=%date:~0,4%-%date:~5,2%-%date:~8,2%"
|
||||
set "LOG_FILE=%LOG_DIR%\%TODAY%.log"
|
||||
set "LOG_FILE=%LOG_DIR%\launcher_%TODAY%.log"
|
||||
|
||||
cls
|
||||
echo ==================================================
|
||||
echo QMT ʵ<EFBFBD>̽<EFBFBD><EFBFBD><EFBFBD>ϵͳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
echo ʱ<EFBFBD><EFBFBD>: %time%
|
||||
echo <EFBFBD><EFBFBD>־: %LOG_FILE%
|
||||
echo <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: http://localhost:8001
|
||||
echo QMT 交易系统守护进程启动
|
||||
echo 工作目录: %WORK_DIR%
|
||||
echo 启动文件: %SCRIPT_NAME%
|
||||
echo 监控地址: http://localhost:8001
|
||||
echo 日志文件: %LOG_FILE%
|
||||
echo ==================================================
|
||||
|
||||
:LOOP
|
||||
echo.
|
||||
echo [%time%] <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӽ<EFBFBD><D3BD><EFBFBD>...
|
||||
echo [%time%] <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӽ<EFBFBD><D3BD><EFBFBD>... >> "%LOG_FILE%"
|
||||
|
||||
:: 3. <20><><EFBFBD><EFBFBD> Python <20>ű<EFBFBD>
|
||||
:: ʹ<><CAB9> uv run <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2>&1 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҳд<D2B2><D0B4><EFBFBD><EFBFBD>־
|
||||
uv run %SCRIPT_NAME% >> "%LOG_FILE%" 2>&1
|
||||
|
||||
:: 4. <20><><EFBFBD><EFBFBD><EFBFBD>˳<EFBFBD>
|
||||
set EXIT_CODE=%errorlevel%
|
||||
echo [%time%] <20><><EFBFBD><EFBFBD><EFBFBD>쳣<EFBFBD>˳<EFBFBD><CBB3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: %EXIT_CODE% >> "%LOG_FILE%"
|
||||
echo <20><><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˳<EFBFBD> (Code: %EXIT_CODE%)
|
||||
|
||||
:: 5. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
:: 检查重试次数
|
||||
if %RETRY_COUNT% GEQ %MAX_RETRIES% (
|
||||
echo [%time%] <EFBFBD>ﵽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵͳֹͣ<EFBFBD><EFBFBD> >> "%LOG_FILE%"
|
||||
:: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʾ
|
||||
msg * "QMT <20><><EFBFBD><EFBFBD>ϵͳ<CFB5>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><DEB7>Զ<EFBFBD><D4B6>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD><D6BE>"
|
||||
echo [%time%] !!! 达到最大重试次数,系统停止重启 !!! >> "%LOG_FILE%"
|
||||
color 0C
|
||||
echo 错误: 系统多次崩溃,请检查日志后手动重启。
|
||||
msg * "QMT 系统异常崩溃且无法自动恢复,请检查日志!"
|
||||
goto FAIL
|
||||
)
|
||||
|
||||
set /a RETRY_COUNT+=1
|
||||
echo [%time%] <EFBFBD>ȴ<EFBFBD> %RETRY_WAIT% <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>е<EFBFBD> %RETRY_COUNT% <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>... >> "%LOG_FILE%"
|
||||
echo <20><><EFBFBD>ڵȴ<DAB5><C8B4><EFBFBD><EFBFBD><EFBFBD> (%RETRY_COUNT%/%MAX_RETRIES%)...
|
||||
timeout /t %RETRY_WAIT% >nul
|
||||
echo [%time%] 正在启动交易引擎 (第 %RETRY_COUNT% 次重启)...
|
||||
echo [%time%] >>> 启动子进程: uv run %SCRIPT_NAME% >> "%LOG_FILE%"
|
||||
|
||||
goto LOOP
|
||||
:: 启动程序 (使用 -u 参数确保输出实时刷新到日志)
|
||||
uv run python -u %SCRIPT_NAME% >> "%LOG_FILE%" 2>&1
|
||||
|
||||
:FAIL
|
||||
title QMT ʵ<><CAB5><EFBFBD>ػ<EFBFBD>ϵͳ [<5B>ѱ<EFBFBD><D1B1><EFBFBD>]
|
||||
color 4F
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo ϵͳ<CFB5><CDB3>ֹͣ<CDA3><D6B9><EFBFBD><EFBFBD>
|
||||
echo <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD>ļ<EFBFBD>: %LOG_FILE%
|
||||
echo ==========================================
|
||||
pause
|
||||
exit /b 1
|
||||
:: 程序退出逻辑
|
||||
set EXIT_CODE=%errorlevel%
|
||||
if %EXIT_CODE% EQU 0 (
|
||||
echo [%time%] 程序正常关闭。 >> "%LOG_FILE%"
|
||||
echo 程序运行结束。
|
||||
Reference in New Issue
Block a user