diff --git a/deploy/profile-scripts/mofin_db.py b/deploy/profile-scripts/mofin_db.py index ce7722bf..b3a80af2 100644 --- a/deploy/profile-scripts/mofin_db.py +++ b/deploy/profile-scripts/mofin_db.py @@ -2430,3 +2430,59 @@ def sync_strategy_def(conn, d: dict): print(f" [SYNC_DEF] {name} 新注册(needs_review=1)") conn.commit() return changed + + +# ── 版本化写入函数(包装原有 write_holding_strategy)── +def write_holding_strategy_versioned(conn, code, name, data, source_trigger="batch_12d"): + """版本化写入策略卡: + 1. 对比关键字段,有变化 → supersede旧版 + 新增active行(version+1) + 2. 无变化 → 跳过写入 + 3. 首次创建 → version=1.1 + """ + from datetime import datetime + now = datetime.now().isoformat() + + # 读当前 active 记录 + old = conn.execute( + "SELECT id, version, entry_low, entry_high, stop_loss, take_profit, timing_signal " + "FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() + + key_fields = ['entry_low', 'entry_high', 'stop_loss', 'take_profit', 'timing_signal'] + changed = False + if old: + for i, f in enumerate(key_fields): + old_v = old[i+2] + new_v = data.get(f) + if new_v is not None and str(old_v or '') != str(new_v): + changed = True + break + + if old and not changed: + print(f" [NOCHANGE] {code} 关键字段无变化, 跳过写入", flush=True) + return (True, "no_change") + + # 版本号处理 + if old and changed: + old_id = old[0] + old_ver = old[1] or '1.1' + conn.execute( + "UPDATE holding_strategies SET status='superseded', superseded_at=? WHERE id=?", + (now, old_id)) + try: + major, minor = old_ver.split('.') + new_ver = f"{major}.{int(minor)+1}" + except: + new_ver = "1.2" + print(f" [VERSION] {code} v{old_ver}->superseded, 新v{new_ver}", flush=True) + elif not old: + new_ver = '1.1' + data['cycle_start'] = now[:10] + print(f" [NEW] {code} v1.1 新建策略卡", flush=True) + else: + new_ver = old[1] if old else '1.1' + + if not data.get('strategy_source'): + data['strategy_source'] = data.get('strategy_name', '') + data['version'] = new_ver + + return write_holding_strategy(conn, code, name, data, source_trigger) diff --git a/deploy/profile-scripts/strategy_effectiveness.py b/deploy/profile-scripts/strategy_effectiveness.py new file mode 100644 index 00000000..3c605b9b --- /dev/null +++ b/deploy/profile-scripts/strategy_effectiveness.py @@ -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()