feat(versioning): 策略版本化+到期评估模块
- mofin_db.py: 新增write_holding_strategy_versioned(对比关键字段→supersede/new) - strategy_effectiveness.py: 策略到期后用完整K线评估区间职责履行 - holding_strategies新字段: strategy_source/cycle_start/buy_zone_expected_days/take_profit_expected_days - strategy_effectiveness表: 记录策略评估结果(买入区/止损/止盈/时间准确性+改进建议)
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""策略到期评估模块:用完整K线数据评估每个区间的职责履行"""
|
||||
|
||||
import os, sys, sqlite3, json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
DB = "/home/hmo/MoFin/data/mofin.db"
|
||||
|
||||
def get_expired_strategies(conn):
|
||||
"""获取已到期且未评估的策略"""
|
||||
now = datetime.now().strftime("%Y-%m-%d")
|
||||
return conn.execute("""
|
||||
SELECT id, code, version, strategy_source, entry_low, entry_high,
|
||||
stop_loss, take_profit, buy_zone_expected_days, take_profit_expected_days,
|
||||
created_at, superseded_at
|
||||
FROM holding_strategies
|
||||
WHERE status IN ('active', 'superseded')
|
||||
AND superseded_at IS NOT NULL
|
||||
AND id NOT IN (SELECT strategy_id FROM strategy_effectiveness)
|
||||
ORDER BY superseded_at
|
||||
""").fetchall()
|
||||
|
||||
def get_kline(conn, code, start_date, end_date):
|
||||
"""获取K线数据"""
|
||||
return conn.execute("""
|
||||
SELECT date, open, close, high, low
|
||||
FROM stock_daily
|
||||
WHERE code=? AND date BETWEEN ? AND ?
|
||||
ORDER BY date
|
||||
""", (code, start_date, end_date)).fetchall()
|
||||
|
||||
def evaluate_zone(klines, zone_type, zone_low, zone_high, expected_days):
|
||||
"""评估单个区间的职责履行"""
|
||||
if not klines or zone_low is None or zone_high is None:
|
||||
return {"accuracy": "no_data", "detail": "无K线数据或区间未设定"}
|
||||
|
||||
first_date = klines[0][0]
|
||||
last_date = klines[-1][0]
|
||||
|
||||
# 统计每日形态
|
||||
days_in_zone = 0 # 收盘在区间内
|
||||
days_penetrated_above = 0 # 收盘突破上沿
|
||||
days_penetrated_below = 0 # 收盘突破下沿
|
||||
v_shape_days = 0 # V型(进入后离开)
|
||||
first_trigger_day = None # 首次触发日
|
||||
prev_close = None
|
||||
|
||||
for date, open_p, close, high, low in klines:
|
||||
if close is None:
|
||||
continue
|
||||
# 收盘价相对于区间
|
||||
if zone_low <= close <= zone_high:
|
||||
days_in_zone += 1
|
||||
if first_trigger_day is None:
|
||||
first_trigger_day = date
|
||||
elif close > zone_high:
|
||||
days_penetrated_above += 1
|
||||
elif close < zone_low:
|
||||
days_penetrated_below += 1
|
||||
|
||||
# V型检测:前一天在区间内,今天离开
|
||||
if prev_close is not None:
|
||||
if (zone_low <= prev_close <= zone_high) and (close < zone_low or close > zone_high):
|
||||
v_shape_days += 1
|
||||
prev_close = close
|
||||
|
||||
total_days = len(klines)
|
||||
actual_days_to_trigger = None
|
||||
if first_trigger_day:
|
||||
fd = datetime.strptime(first_trigger_day, "%Y-%m-%d")
|
||||
sd = datetime.strptime(first_date, "%Y-%m-%d")
|
||||
actual_days_to_trigger = (fd - sd).days
|
||||
|
||||
# 区间职责评估
|
||||
if zone_type == "buy_zone":
|
||||
# 买入区职责:触发后是否上涨
|
||||
if first_trigger_day:
|
||||
trigger_idx = next(i for i, k in enumerate(klines) if k[0] == first_trigger_day)
|
||||
after_klines = klines[trigger_idx:]
|
||||
if after_klines:
|
||||
trigger_close = after_klines[0][2]
|
||||
max_after = max(k[2] for k in after_klines if k[2])
|
||||
profit_pct = (max_after - trigger_close) / trigger_close * 100 if trigger_close else 0
|
||||
if profit_pct > 5:
|
||||
accuracy = "effective"
|
||||
elif profit_pct > 0:
|
||||
accuracy = "partially_effective"
|
||||
else:
|
||||
accuracy = "ineffective"
|
||||
else:
|
||||
accuracy = "no_data"
|
||||
else:
|
||||
accuracy = "not_triggered"
|
||||
|
||||
elif zone_type == "stop_loss":
|
||||
# 止损职责:触发后是否继续跌(避免更大损失)
|
||||
if first_trigger_day:
|
||||
trigger_idx = next(i for i, k in enumerate(klines) if k[0] == first_trigger_day)
|
||||
after_klines = klines[trigger_idx:]
|
||||
if len(after_klines) >= 2:
|
||||
trigger_close = after_klines[0][2]
|
||||
# 之后1-2天是否继续跌
|
||||
next_2d_low = min(k[4] for k in after_klines[1:3] if k[4])
|
||||
if next_2d_low and next_2d_low < trigger_close * 0.97:
|
||||
accuracy = "effective" # 止损后继续跌,止损正确
|
||||
elif next_2d_low and next_2d_low > trigger_close * 1.03:
|
||||
accuracy = "ineffective" # 止损后反弹,被洗盘
|
||||
else:
|
||||
accuracy = "neutral"
|
||||
else:
|
||||
accuracy = "no_data"
|
||||
else:
|
||||
accuracy = "not_triggered"
|
||||
|
||||
elif zone_type == "take_profit":
|
||||
# 止盈职责:触发后是否回落(锁定正确)
|
||||
if first_trigger_day:
|
||||
trigger_idx = next(i for i, k in enumerate(klines) if k[0] == first_trigger_day)
|
||||
after_klines = klines[trigger_idx:]
|
||||
if len(after_klines) >= 2:
|
||||
trigger_close = after_klines[0][2]
|
||||
# 之后3天最高价
|
||||
max_3d = max(k[3] for k in after_klines[1:4] if k[3])
|
||||
if max_3d and max_3d > trigger_close * 1.05:
|
||||
accuracy = "ineffective" # 止盈后继续涨,偏保守
|
||||
elif max_3d and max_3d < trigger_close:
|
||||
accuracy = "effective" # 止盈后回落,锁定正确
|
||||
else:
|
||||
accuracy = "neutral"
|
||||
else:
|
||||
accuracy = "no_data"
|
||||
else:
|
||||
accuracy = "not_triggered"
|
||||
|
||||
# 时间准确性
|
||||
if actual_days_to_trigger and expected_days:
|
||||
ratio = actual_days_to_trigger / expected_days
|
||||
if ratio <= 1:
|
||||
time_accuracy = "on_time"
|
||||
elif ratio <= 1.5:
|
||||
time_accuracy = "slightly_late"
|
||||
else:
|
||||
time_accuracy = "late"
|
||||
else:
|
||||
time_accuracy = "no_data"
|
||||
|
||||
return {
|
||||
"accuracy": accuracy,
|
||||
"time_accuracy": time_accuracy,
|
||||
"actual_days": actual_days_to_trigger,
|
||||
"expected_days": expected_days,
|
||||
"days_in_zone": days_in_zone,
|
||||
"v_shape_days": v_shape_days,
|
||||
"total_days": total_days,
|
||||
}
|
||||
|
||||
def evaluate_strategy(conn, strategy):
|
||||
"""评估单个策略"""
|
||||
sid, code, ver, source, el, eh, sl, tp, buy_exp, tp_exp, created, superseded = strategy
|
||||
|
||||
# 获取K线数据(从创建到被替代)
|
||||
klines = get_kline(conn, code, created[:10], superseded[:10])
|
||||
if not klines:
|
||||
return None
|
||||
|
||||
buy_eval = evaluate_zone(klines, "buy_zone", el, eh, buy_exp)
|
||||
tp_eval = evaluate_zone(klines, "take_profit", tp, tp*1.05 if tp else None, tp_exp) if tp else None
|
||||
sl_eval = evaluate_zone(klines, "stop_loss", sl*0.95 if sl else None, sl, None) if sl else None
|
||||
|
||||
# 综合评价
|
||||
assessment = []
|
||||
if buy_eval["accuracy"] == "ineffective":
|
||||
assessment.append("买入区预判有误(触发后下跌)")
|
||||
elif buy_eval["accuracy"] == "effective":
|
||||
assessment.append("买入区预判准确")
|
||||
if sl_eval and sl_eval["accuracy"] == "ineffective":
|
||||
assessment.append("止损被洗盘(触发后反弹)")
|
||||
elif sl_eval and sl_eval["accuracy"] == "effective":
|
||||
assessment.append("止损有效(避免更大损失)")
|
||||
if tp_eval and tp_eval["accuracy"] == "ineffective":
|
||||
assessment.append("止盈偏保守(触发后继续涨)")
|
||||
elif tp_eval and tp_eval["accuracy"] == "effective":
|
||||
assessment.append("止盈锁定正确")
|
||||
if buy_eval["time_accuracy"] == "late":
|
||||
assessment.append("入场时机偏晚")
|
||||
elif buy_eval["time_accuracy"] == "on_time":
|
||||
assessment.append("入场时机准确")
|
||||
|
||||
# 改进建议
|
||||
suggestions = []
|
||||
if buy_eval["accuracy"] == "ineffective":
|
||||
suggestions.append("买入区需要下调(当前价位偏高)")
|
||||
if buy_eval["time_accuracy"] == "late":
|
||||
suggestions.append("买入区预期天数需要延长")
|
||||
if sl_eval and sl_eval["accuracy"] == "ineffective":
|
||||
suggestions.append("止损位设置过于激进,考虑放宽")
|
||||
if tp_eval and tp_eval["accuracy"] == "ineffective":
|
||||
suggestions.append("止盈位偏保守,可适当提高")
|
||||
|
||||
return {
|
||||
"strategy_id": sid,
|
||||
"code": code,
|
||||
"strategy_source": source,
|
||||
"version": ver,
|
||||
"period_start": created[:10],
|
||||
"period_end": superseded[:10],
|
||||
"buy_zone_accuracy": buy_eval["accuracy"],
|
||||
"take_profit_accuracy": tp_eval["accuracy"] if tp_eval else "no_zone",
|
||||
"stop_loss_accuracy": sl_eval["accuracy"] if sl_eval else "no_zone",
|
||||
"time_accuracy": buy_eval["time_accuracy"],
|
||||
"overall_assessment": "; ".join(assessment) if assessment else "数据不足",
|
||||
"improvement_suggestion": "; ".join(suggestions) if suggestions else "暂无改进建议",
|
||||
}
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(DB, timeout=30)
|
||||
expired = get_expired_strategies(conn)
|
||||
print(f"待评估策略: {len(expired)} 个")
|
||||
|
||||
ok = 0
|
||||
for strategy in expired:
|
||||
result = evaluate_strategy(conn, strategy)
|
||||
if result:
|
||||
conn.execute("""
|
||||
INSERT INTO strategy_effectiveness
|
||||
(strategy_id, code, strategy_source, version, period_start, period_end,
|
||||
buy_zone_accuracy, take_profit_accuracy, stop_loss_accuracy,
|
||||
time_accuracy, overall_assessment, improvement_suggestion)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""", (result["strategy_id"], result["code"], result["strategy_source"],
|
||||
result["version"], result["period_start"], result["period_end"],
|
||||
result["buy_zone_accuracy"], result["take_profit_accuracy"],
|
||||
result["stop_loss_accuracy"], result["time_accuracy"],
|
||||
result["overall_assessment"], result["improvement_suggestion"]))
|
||||
ok += 1
|
||||
print(f" {result['code']} v{result['version']}: {result['overall_assessment'][:50]}")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"\n评估完成: {ok} 个策略已评估")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user