12维分析(大盘技术面×个股技术面): 弱市深超跌edge前提=大盘ADX∈[25,30] 全市场10年验证: ADX25-30 avg+4.89% vs ADX>=40 -0.10% vs ADX<20 +1.63% 叠加bias60<-20+RSI<30: avg+5.04%/去2018+5.67%/正年10/11 落地: 非甜区默认跳过(--force可强制); 文档§13记录完整推导
474 lines
19 KiB
Python
474 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
"""mr_scanner.py — v_mr 均值回复策略实盘扫描器
|
||
|
||
回测侧 v_mr(strategy_lab.run_mr_backtest / mr_engine_v2.py)已 10y 全市场验证
|
||
(21690笔 / WR 53.6% / avg +3.34%),但实盘此前没有该策略的扫描机制 ——
|
||
本脚本把 v_mr 的入场筛选原样搬到实盘,按日扫描全市场深超跌小盘票。
|
||
|
||
入场条件(v_mr 精选版,2026-08-02 六步方法论调优,见 docs/v_mr_strategy.md §11):
|
||
基线 6 条件(与回测 mr_cfg v2 定稿一致):
|
||
1. bias60 < -10% : 收盘价在 MA60 下方超 10%(单边,无下限)
|
||
2. RSI14 < 42 : 超卖
|
||
3. ret60 < -15% : 60 日跌幅超 15%(单边,无下限)
|
||
4. mom20 < 5% : 20 日低动量(还没启动反弹)
|
||
5. amount20 < 1500万 : 小盘(20 日均成交额,百万元)
|
||
6. rsi_delta >= 2 : RSI 5 日回升 ≥2(止跌回升确认)
|
||
精选正面因子(回测 21690 笔分层验证):
|
||
7. mom20 ∈ [-20, -14] : 加速下跌尾声(20日跌14-20%)→ avg +5.73%/wr 63.5%
|
||
8. rsi_delta >= 10 : RSI 强回升(止跌确认强)→ avg +5.85%/wr 63.3%
|
||
9. dist_ma20 ∈ [-10, -7] : 离 MA20 有 7-10% 空间 → avg +5.71%/wr 63.0%
|
||
10. atr_pct >= 4.5 : 波动大弹性足 → avg +5.96%/wr 61.0%
|
||
精选负面因子(剔除):
|
||
11. mkt_adx >= 35 : 大盘强趋势下行时抄底危险(亏损组 40.15 vs 盈利 29.43)
|
||
12. OBV delta < 0 : 资金流出(亏损组 -977k vs 盈利 +25k,最强负面)
|
||
13. bias60 < -22 : 过深超跌易续跌(亏损组 -19.75 vs 盈利 -17.67)
|
||
14. prev_ret60 < -32 : 60日跌超32%易续跌(亏损组 -29.57 vs 盈利 -26.26)
|
||
出场建议(精选版):tp=+25% / sl=-10% / max_hold=30 交易日
|
||
(网格扫描 tp×sl×hold 最优:avg 12.73%→15.51%, wr 88.9%→90.3%)
|
||
|
||
市场门控(2026-08-02 新增):
|
||
- 只读 market_regime 表,regime 为 trend_down 或 choppy 时启用扫描
|
||
(v_mr 主战场:下跌趋势 + 震荡市;趋势市让位 v_next4)
|
||
- trend_up 时跳过(v_next4 追涨主战场,不扫超跌)
|
||
|
||
数据源:腾讯前复权日K(qfq),datalen=120(覆盖 MA60 + 60日回看 + RSI 收敛)
|
||
指标算法:与 backtest_framework.py 完全一致(内联,零偏差)
|
||
输出:candidates 表(sector='v_mr'),与 accumulation_scanner 同 UPSERT 模式
|
||
|
||
用法:
|
||
python3 mr_scanner.py # 完整扫描(regime 门控)
|
||
python3 mr_scanner.py --force # 忽略 regime 门控强制扫描
|
||
python3 mr_scanner.py --top N # 输出前 N 只(默认 10)
|
||
"""
|
||
import sys, json, urllib.request, re, time, sqlite3
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
|
||
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
|
||
UA = "Mozilla/5.0"
|
||
|
||
# ── v_mr 参数(精选版,2026-08-02 六步方法论调优,见 docs/v_mr_strategy.md §11)──
|
||
MR_CFG = {
|
||
"bias_max": -10, # MA60 下方超 10%(单边无下限)
|
||
"rsi_max": 42, # RSI 超卖
|
||
"ret_max": -15, # 60 日跌超 15%(单边无下限)
|
||
"mom20_max": 5, # 20 日低动量(基线门槛)
|
||
"amount_max": 15, # 百万元 = 1500 万日成交额
|
||
"rsi_delta_min": 2, # RSI 5 日回升 ≥2(基线门槛)
|
||
"mkt_mode": "any", # 实盘由 regime 门控替代(trend_down/choppy 才扫)
|
||
}
|
||
# 精选正面因子(六步方法论第3步:显著收益共同因子)
|
||
SEL_CFG = {
|
||
"mom20_min": -20, "mom20_max": -14, # 加速下跌尾声
|
||
"rsi_delta_min": 10, # RSI 强回升
|
||
"dist_ma20_min": -10, "dist_ma20_max": -7, # 离 MA20 7-10% 空间
|
||
"atr_pct_min": 4.5, # 波动大弹性足
|
||
}
|
||
# 精选负面因子(六步方法论第5步:亏损票共同特征,剔除)
|
||
NEG_CFG = {
|
||
"mkt_adx_max": 35, # 大盘强趋势下行不抄底
|
||
"obv_delta_min": 0, # 资金不流出
|
||
"bias60_min": -22, # 不过深超跌
|
||
"prev_ret60_min": -32, # 60日跌不过 32%
|
||
}
|
||
EXIT_CFG = {"tp_pct": 0.25, "sl_pct": 0.10, "max_hold_days": 30}
|
||
|
||
TOP_N = 10
|
||
|
||
|
||
# ── 技术指标(与 backtest_framework.py 完全同算法)──
|
||
|
||
def calc_ma(series, n):
|
||
result = []
|
||
for i in range(len(series)):
|
||
if i < n - 1:
|
||
result.append(None)
|
||
else:
|
||
result.append(sum(series[i - n + 1:i + 1]) / n)
|
||
return result
|
||
|
||
|
||
def calc_rsi(series, n=14):
|
||
deltas = [series[i] - series[i - 1] for i in range(1, len(series))]
|
||
gains = [d if d > 0 else 0 for d in deltas]
|
||
losses = [-d if d < 0 else 0 for d in deltas]
|
||
result = [None] * (n + 1)
|
||
avg_gain = sum(gains[:n]) / n
|
||
avg_loss = sum(losses[:n]) / n
|
||
if avg_loss == 0:
|
||
result.append(100)
|
||
else:
|
||
rs = avg_gain / avg_loss
|
||
result.append(100 - 100 / (1 + rs))
|
||
for i in range(n, len(gains)):
|
||
avg_gain = (avg_gain * (n - 1) + gains[i]) / n
|
||
avg_loss = (avg_loss * (n - 1) + losses[i]) / n
|
||
if avg_loss == 0:
|
||
result.append(100)
|
||
else:
|
||
rs = avg_gain / avg_loss
|
||
result.append(100 - 100 / (1 + rs))
|
||
while len(result) < len(series):
|
||
result.insert(0, None)
|
||
return result[:len(series)]
|
||
|
||
|
||
def calc_atr(klines, n=14):
|
||
"""ATR(Average True Range),与回测 calc_factors 的 atr_pct 同口径"""
|
||
if len(klines) < n + 1:
|
||
return None
|
||
trs = []
|
||
for i in range(1, len(klines)):
|
||
h, l, pc = klines[i]["high"], klines[i]["low"], klines[i - 1]["close"]
|
||
tr = max(h - l, abs(h - pc), abs(l - pc))
|
||
trs.append(tr)
|
||
atr = sum(trs[-n:]) / n
|
||
return atr
|
||
|
||
|
||
def calc_obv(klines):
|
||
"""OBV 能量潮(近20日变化量),资金流向指标"""
|
||
if len(klines) < 21:
|
||
return 0
|
||
obv = 0
|
||
for i in range(1, len(klines)):
|
||
if klines[i]["close"] > klines[i - 1]["close"]:
|
||
obv += klines[i]["volume"] * 100 # 手→股
|
||
elif klines[i]["close"] < klines[i - 1]["close"]:
|
||
obv -= klines[i]["volume"] * 100
|
||
# 近20日 OBV 变化
|
||
obv_now = 0
|
||
for i in range(max(1, len(klines) - 20), len(klines)):
|
||
if klines[i]["close"] > klines[i - 1]["close"]:
|
||
obv_now += klines[i]["volume"] * 100
|
||
elif klines[i]["close"] < klines[i - 1]["close"]:
|
||
obv_now -= klines[i]["volume"] * 100
|
||
return obv_now
|
||
|
||
|
||
# ── 数据获取 ──
|
||
|
||
def fetch_tx_klines(code, datalen=120):
|
||
"""腾讯前复权日K(qfq),与 stock_daily 数据零偏差,返回 [{date,open,close,high,low,volume}]"""
|
||
raw = str(code).strip()
|
||
if raw.startswith(("6", "9")):
|
||
prefix = "sh"
|
||
elif raw.startswith(("0", "3")):
|
||
prefix = "sz"
|
||
else:
|
||
return None
|
||
url = f"http://ifzq.gtimg.cn/appstock/app/fqkline/get?param={prefix}{raw},day,,,{datalen},qfq"
|
||
try:
|
||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||
with opener.open(req, timeout=8) as r:
|
||
text = r.read().decode("utf-8", errors="replace").strip()
|
||
data = json.loads(text)
|
||
node = data.get("data", {}).get(f"{prefix}{raw}", {})
|
||
bars = node.get("qfqday") or node.get("day") or []
|
||
if not bars or len(bars) < 70:
|
||
return None
|
||
result = []
|
||
for b in bars:
|
||
if len(b) < 6:
|
||
continue
|
||
result.append({
|
||
"date": b[0][:10],
|
||
"open": float(b[1]),
|
||
"close": float(b[2]),
|
||
"high": float(b[3]),
|
||
"low": float(b[4]),
|
||
"volume": float(b[5]), # 手
|
||
})
|
||
return result
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
# 兼容别名(供外部引用)
|
||
fetch_sina_klines = fetch_tx_klines
|
||
|
||
|
||
def get_stock_pool():
|
||
"""待扫描股票池:stock_daily 的 distinct code(与回测 run_mr_backtest 完全同口径)
|
||
|
||
回测股票池 = SELECT DISTINCT sd.code FROM stock_daily(4266只,含300/688,
|
||
不含301新创业板——数据源未收录)。实盘扫描用同一口径,保证 v_mr
|
||
信号覆盖的股票都是回测验证过的。
|
||
"""
|
||
conn = sqlite3.connect(str(DB_PATH), timeout=5)
|
||
try:
|
||
existing = set()
|
||
for r in conn.execute("SELECT code FROM holding_strategies WHERE status='active'"):
|
||
existing.add(str(r[0]))
|
||
for r in conn.execute("SELECT code FROM holdings WHERE is_active=1"):
|
||
existing.add(str(r[0]))
|
||
# 与回测完全一致:stock_daily 有K线的股票(回测 universe='a' 排除5位港股)
|
||
all_stocks = [str(r[0]) for r in
|
||
conn.execute("SELECT DISTINCT code FROM stock_daily").fetchall()]
|
||
finally:
|
||
conn.close()
|
||
# 只留 A 股(6位数字),排除港股(5位0开头)—— 与回测 is_hk_code 逻辑一致
|
||
a_stocks = [c for c in all_stocks if len(c) == 6 and c.isdigit()]
|
||
return a_stocks, existing
|
||
|
||
|
||
def load_regime():
|
||
"""读取 market_regime 最新状态"""
|
||
try:
|
||
conn = sqlite3.connect(str(DB_PATH), timeout=5)
|
||
row = conn.execute(
|
||
"SELECT date, above_ma20, adx, regime FROM market_regime "
|
||
"ORDER BY date DESC LIMIT 1").fetchone()
|
||
conn.close()
|
||
if row:
|
||
return {"date": row[0], "above_ma20": bool(row[1]), "adx": row[2], "regime": row[3]}
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
# ── v_mr 筛选(精选版:基线6条件 + 正面4因子 + 剔除4负面)──
|
||
|
||
def check_vmr(klines, mkt_adx=None):
|
||
"""对单只股票做 v_mr 精选入场筛选。命中返回信号 dict,否则 None。
|
||
klines 为升序日K。mkt_adx 为大盘 ADX(来自 market_regime,用于负面因子)。"""
|
||
if not klines or len(klines) < 70:
|
||
return None
|
||
closes = [k["close"] for k in klines]
|
||
highs = [k["high"] for k in klines]
|
||
lows = [k["low"] for k in klines]
|
||
i = len(klines) - 1 # 最新一日
|
||
close = closes[i]
|
||
if close <= 0:
|
||
return None
|
||
|
||
ma60 = calc_ma(closes, 60)
|
||
ma20 = calc_ma(closes, 20)
|
||
rsi_all = calc_rsi(closes)
|
||
rsi = rsi_all[i] if i < len(rsi_all) else None
|
||
m60 = ma60[i]
|
||
if not m60 or m60 <= 0 or rsi is None:
|
||
return None
|
||
|
||
# 1. MA60 下方超跌(单边)
|
||
bias60 = (close - m60) / m60 * 100
|
||
if bias60 > MR_CFG["bias_max"]: # 只拦上沿(-10%),无下限
|
||
return None
|
||
|
||
# 2. RSI 超卖
|
||
if rsi > MR_CFG["rsi_max"]:
|
||
return None
|
||
|
||
# 3. 60 日跌幅(单边)
|
||
prev60 = closes[i - 60] if i >= 60 else 0
|
||
prev_ret60 = (close - prev60) / prev60 * 100 if prev60 > 0 else 0
|
||
if prev_ret60 > MR_CFG["ret_max"]: # 只拦上沿(-15%),无下限
|
||
return None
|
||
|
||
# 4. 20 日低动量
|
||
prev20 = closes[i - 20] if i >= 20 else 0
|
||
mom20 = (close - prev20) / prev20 * 100 if prev20 > 0 else 0
|
||
if mom20 > MR_CFG["mom20_max"]:
|
||
return None
|
||
|
||
# 5. 小盘(20 日均成交额,百万元)
|
||
# 腾讯日K volume 单位=手(×100股),成交额=手×100×均价
|
||
amt20 = []
|
||
for k in klines[max(0, i - 19):i + 1]:
|
||
avg_px = (k["high"] + k["low"] + k["close"]) / 3
|
||
amt20.append(k["volume"] * 100 * avg_px) # 元
|
||
amt_valid = [a for a in amt20 if a and a > 0]
|
||
amount_ma20 = sum(amt_valid) / len(amt_valid) if amt_valid else 0
|
||
amount_ma20_m = amount_ma20 / 1e6 # 元 → 百万元
|
||
if MR_CFG["amount_max"] is not None and amount_ma20_m > MR_CFG["amount_max"]:
|
||
return None
|
||
|
||
# 6. RSI 5 日回升(止跌确认,基线门槛 ≥2)
|
||
rsi0 = rsi_all[i - 5] if i >= 5 else None
|
||
rsi_delta = (rsi - rsi0) if rsi0 is not None else 0
|
||
if rsi_delta < MR_CFG["rsi_delta_min"]:
|
||
return None
|
||
|
||
# ── 精选正面因子(六步方法论第3步)──
|
||
# 7. mom20 ∈ [-20, -14]:加速下跌尾声
|
||
if not (SEL_CFG["mom20_min"] <= mom20 < SEL_CFG["mom20_max"]):
|
||
return None
|
||
# 8. rsi_delta >= 10:RSI 强回升
|
||
if rsi_delta < SEL_CFG["rsi_delta_min"]:
|
||
return None
|
||
# 9. dist_ma20 ∈ [-10, -7]:离 MA20 有 7-10% 空间
|
||
m20 = ma20[i]
|
||
dist_ma20 = (close - m20) / m20 * 100 if m20 and m20 > 0 else 0
|
||
if not (SEL_CFG["dist_ma20_min"] <= dist_ma20 < SEL_CFG["dist_ma20_max"]):
|
||
return None
|
||
# 10. atr_pct >= 4.5:波动大弹性足
|
||
atr = calc_atr(klines)
|
||
atr_pct = atr / close * 100 if atr and close > 0 else 0
|
||
if atr_pct < SEL_CFG["atr_pct_min"]:
|
||
return None
|
||
|
||
# ── 精选负面因子(六步方法论第5步,剔除)──
|
||
# 11. mkt_adx >= 35:大盘强趋势下行不抄底
|
||
if mkt_adx is not None and mkt_adx >= NEG_CFG["mkt_adx_max"]:
|
||
return None
|
||
# 12. OBV delta < 0:资金流出剔除
|
||
obv_delta = calc_obv(klines)
|
||
if obv_delta < NEG_CFG["obv_delta_min"]:
|
||
return None
|
||
# 13. bias60 < -22:过深超跌易续跌
|
||
if bias60 < NEG_CFG["bias60_min"]:
|
||
return None
|
||
# 14. prev_ret60 < -32:60日跌超32%易续跌
|
||
if prev_ret60 < NEG_CFG["prev_ret60_min"]:
|
||
return None
|
||
|
||
# 命中 → 出场建议(精选版 exit_cfg)
|
||
tp_pct = EXIT_CFG["tp_pct"]
|
||
sl_pct = EXIT_CFG["sl_pct"]
|
||
target = round(close * (1 + tp_pct), 2)
|
||
stop = round(close * (1 - sl_pct), 2)
|
||
|
||
return {
|
||
"price": close,
|
||
"bias60": round(bias60, 2),
|
||
"rsi": round(rsi, 2),
|
||
"prev_ret60": round(prev_ret60, 2),
|
||
"mom20": round(mom20, 2),
|
||
"amount_ma20": round(amount_ma20_m, 2),
|
||
"rsi_delta": round(rsi_delta, 2),
|
||
"dist_ma20": round(dist_ma20, 2),
|
||
"atr_pct": round(atr_pct, 2),
|
||
"obv_delta": round(obv_delta, 0),
|
||
"mkt_adx": mkt_adx,
|
||
"target": target,
|
||
"stop_loss": stop,
|
||
"date": klines[i]["date"],
|
||
}
|
||
|
||
|
||
def main():
|
||
force = "--force" in sys.argv
|
||
top_n = TOP_N
|
||
if "--top" in sys.argv:
|
||
try:
|
||
top_n = int(sys.argv[sys.argv.index("--top") + 1])
|
||
except (ValueError, IndexError):
|
||
pass
|
||
|
||
print(f"[MR] {datetime.now().strftime('%H:%M')} v_mr 实盘扫描开始", flush=True)
|
||
|
||
# ── regime 门控 + 大盘 ADX(负面因子用 + 甜区门控 2026-08-03 12维发现)──
|
||
regime = load_regime()
|
||
mkt_adx = None
|
||
if regime:
|
||
rg = regime["regime"]
|
||
mkt_adx = regime["adx"]
|
||
print(f" 市场阶段: {regime['date']} → {rg} (adx={regime['adx']})", flush=True)
|
||
if rg not in ("trend_down", "choppy") and not force:
|
||
print(f" ⏭ {rg} 非 v_mr 主战场(trend_down/choppy 才扫),跳过", flush=True)
|
||
return
|
||
if rg not in ("trend_down", "choppy"):
|
||
print(f" ⚠ --force 强制扫描(当前 {rg})", flush=True)
|
||
# 12维框架验证(2026-08-03): 弱市深超跌edge的前提是大盘ADX∈[25,30](甜区)
|
||
# ADX<20 avg+1.63% / 20-25 -1.24% / 25-30 +4.89% / 30-40 -0.04% / >=40 -0.10%
|
||
# 非甜区时 v_mr 信号质量显著下降,默认跳过(--force 可强制)
|
||
if mkt_adx is not None and not (25 <= mkt_adx < 30) and not force:
|
||
print(f" ⏭ 大盘ADX={mkt_adx:.1f} 非甜区[25,30)(12维验证:强跌/弱跌市超跌信号质量差),跳过", flush=True)
|
||
return
|
||
if mkt_adx is not None and not (25 <= mkt_adx < 30):
|
||
print(f" ⚠ --force 强制扫描(当前ADX={mkt_adx:.1f} 非甜区)", flush=True)
|
||
else:
|
||
print(" ⚠ market_regime 不可用,默认执行扫描(v_mr 全市场可用)", flush=True)
|
||
|
||
# ── 幂等检查:当天已扫过 v_mr 则跳过(避免 30 分钟调度重复全扫描)──
|
||
import sqlite3 as _sq
|
||
_conn = _sq.connect(str(DB_PATH), timeout=5)
|
||
try:
|
||
_today = datetime.now().strftime("%Y-%m-%d")
|
||
_n = _conn.execute(
|
||
"SELECT COUNT(*) FROM candidates WHERE sector='v_mr' AND substr(created_at,1,10)=?",
|
||
(_today,)).fetchone()[0]
|
||
except Exception:
|
||
_n = 0
|
||
_conn.close()
|
||
if _n > 0 and not force:
|
||
print(f" 已有 {_n} 条今日 v_mr 候选,跳过重复扫描(--force 可强制)", flush=True)
|
||
return
|
||
|
||
# ── 股票池 ──
|
||
all_stocks, existing = get_stock_pool()
|
||
print(f" 股票池: {len(all_stocks)}只A股, 已有策略: {len(existing)}只", flush=True)
|
||
if not all_stocks:
|
||
print(" ⚠ stocks 表为空", flush=True)
|
||
return
|
||
|
||
# 并发拉日K(ThreadPool 8 并发)
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
pool = [c for c in all_stocks if c not in existing]
|
||
found = []
|
||
done = 0
|
||
with ThreadPoolExecutor(max_workers=8) as ex:
|
||
fut_map = {ex.submit(fetch_sina_klines, c): c for c in pool}
|
||
for fut in as_completed(fut_map):
|
||
code = fut_map[fut]
|
||
done += 1
|
||
klines = fut.result()
|
||
if klines:
|
||
sig = check_vmr(klines, mkt_adx=mkt_adx)
|
||
if sig:
|
||
found.append((code, sig))
|
||
if done % 400 == 0:
|
||
print(f" 已扫描 {done}/{len(pool)}", flush=True)
|
||
|
||
print(f" 命中 v_mr 精选条件: {len(found)} 只", flush=True)
|
||
|
||
# 排序:同分按偏度,精选因子已强过滤,按 rsi_delta 降序(止跌确认最强优先)
|
||
found.sort(key=lambda x: -x[1]["rsi_delta"])
|
||
|
||
# ── 写 candidates 表(UPSERT,保留计算列)──
|
||
conn = sqlite3.connect(str(DB_PATH), timeout=5)
|
||
inserted = 0
|
||
for code, sig in found[:top_n]:
|
||
name = code
|
||
try:
|
||
r = conn.execute("SELECT name FROM stocks WHERE code=?", (code,)).fetchone()
|
||
if r and r[0]:
|
||
name = r[0]
|
||
except Exception:
|
||
pass
|
||
price = sig["price"]
|
||
entry_low = round(price * 0.97, 2)
|
||
entry_high = round(price * 1.02, 2)
|
||
sl = sig["stop_loss"]
|
||
tp = sig["target"]
|
||
reasons = (f"v_mr精选(bias60={sig['bias60']}% rsi={sig['rsi']} "
|
||
f"ret60={sig['prev_ret60']}% mom20={sig['mom20']}% "
|
||
f"dist20={sig['dist_ma20']}% atr={sig['atr_pct']}% "
|
||
f"rsi_delta={sig['rsi_delta']} obv={sig['obv_delta']:.0f})")
|
||
# 检查是否已在 candidates 且未 promoted
|
||
exists = conn.execute(
|
||
"SELECT code FROM candidates WHERE code=? AND (promoted IS NULL OR promoted=0)",
|
||
(code,)).fetchone()
|
||
if exists:
|
||
continue
|
||
conn.execute(
|
||
"INSERT INTO candidates (code, name, sector, reason, "
|
||
"entry_range, stop_loss, target, created_at) "
|
||
"VALUES (?,?,?,?,?,?,?,datetime('now','localtime')) "
|
||
"ON CONFLICT(code) DO UPDATE SET "
|
||
"name=excluded.name, sector=excluded.sector, reason=excluded.reason, "
|
||
"entry_range=excluded.entry_range, stop_loss=excluded.stop_loss, target=excluded.target",
|
||
(code, name, "v_mr", reasons,
|
||
f"{entry_low}~{entry_high}", sl, tp))
|
||
inserted += 1
|
||
print(f" 🟢 {code} {name} 价{price} bias60={sig['bias60']}% "
|
||
f"rsi={sig['rsi']} ret60={sig['prev_ret60']}% {reasons}", flush=True)
|
||
conn.commit()
|
||
conn.close()
|
||
print(f" ✅ 新增 {inserted} 只 v_mr 精选候选(前 {top_n})", flush=True)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|