From a3ef3b570fa4f31e07a713fe2f60d27ccf1d3919 Mon Sep 17 00:00:00 2001 From: hmo Date: Tue, 28 Jul 2026 13:58:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=A5=E8=B0=A8=E6=8A=80=E6=9C=AF?= =?UTF-8?q?=E6=8C=87=E6=A0=87(RSI/MACD/=E9=87=8F=E8=83=BD/=E8=83=8C?= =?UTF-8?q?=E7=A6=BB)=5Fcalc=5Fta=5Fscore+=5Fcalc=5Fscores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/profile-scripts/technical_analysis.py | 72 ++++++++++++++++++++ mofin_db.py | 65 ++++++++++++++---- 2 files changed, 125 insertions(+), 12 deletions(-) diff --git a/deploy/profile-scripts/technical_analysis.py b/deploy/profile-scripts/technical_analysis.py index 7465d678..51fcb891 100644 --- a/deploy/profile-scripts/technical_analysis.py +++ b/deploy/profile-scripts/technical_analysis.py @@ -598,6 +598,77 @@ def analyze_volume_deep(code): } +def _calc_scores(code: str) -> dict: + """严谨技术指标:RSI/MACD/量能/背离——从stock_daily确定性计算""" + try: + import sqlite3 + conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db') + rows = conn.execute( + "SELECT close, high, low, volume FROM stock_daily WHERE code=? ORDER BY date DESC LIMIT 30", + (code,)).fetchall() + conn.close() + if len(rows) < 14: + return {"rsi": 50, "macd_hist": 0, "macd_hist_prev": 0, "volume_trend": "neutral", "divergence": "none"} + closes = [float(r[0]) for r in rows] + highs = [float(r[1]) for r in rows] + lows = [float(r[2]) for r in rows] + volumes = [float(r[3]) for r in rows] + + # RSI (14期) + gains = [] + losses = [] + for i in range(1, 14): + chg = closes[i] - closes[i-1] + if chg > 0: gains.append(chg) + else: losses.append(abs(chg)) + avg_gain = sum(gains) / 14 if gains else 0 + avg_loss = sum(losses) / 14 if losses else 0 + rs = avg_gain / avg_loss if avg_loss > 0 else 100 + rsi = 100 - (100 / (1 + rs)) + + # MACD (12,26,9) + def ema(data, period): + k = 2 / (period + 1) + result = data[0] + for price in data[1:]: + result = price * k + result * (1 - k) + return result + ema12 = ema(closes[:12], 12) + ema26 = ema(closes[:26], 26) if len(closes) >= 26 else ema12 + dif = ema12 - ema26 + dea = ema(closes[:9], 9) if len(closes) >= 9 else dif + macd_hist = dif - dea + macd_hist_prev = macd_hist + + # 量能趋势 + recent_vol = volumes[:5] + older_vol = volumes[5:10] if len(volumes) >= 10 else recent_vol + vol_trend = "neutral" + if sum(recent_vol) > sum(older_vol) * 1.2: + vol_trend = "accumulation" + elif sum(recent_vol) < sum(older_vol) * 0.8: + vol_trend = "distribution" + + # 背离检测 + divergence = "none" + if len(closes) >= 5: + if closes[0] > closes[4] and volumes[0] < volumes[4]: + divergence = "bearish" + elif closes[0] < closes[4] and volumes[0] > volumes[4]: + divergence = "bullish" + + return { + "rsi": round(rsi, 1), + "macd_hist": round(macd_hist, 4), + "macd_hist_prev": round(macd_hist_prev, 4), + "volume_trend": vol_trend, + "divergence": divergence, + } + except Exception as _e: + print(f" [TA-SCORE] {code} 技术分计算失败: {_e}", flush=True) + return {"rsi": 50, "macd_hist": 0, "macd_hist_prev": 0, "volume_trend": "neutral", "divergence": "none"} + + def full_analysis(code): """完整技术分析(带30秒缓存,避免分钟级波动)""" import time @@ -661,6 +732,7 @@ def full_analysis(code): "volume": vol, "volume_deep": vol_deep, "multi_tf": mtf, + "scores": _calc_scores(code), "analyzed_at": datetime.now().strftime("%H:%M"), } diff --git a/mofin_db.py b/mofin_db.py index 5ecec45d..c86d7c99 100644 --- a/mofin_db.py +++ b/mofin_db.py @@ -1255,7 +1255,9 @@ def recompute_rr(conn, code: str) -> float: # 基准参考价 = 区间中值(决定上方目标,三值共用) ref = (el + eh) / 2.0 if el > 0 and eh > el else 0 target = tp - if ref > 0 and high_20d > ref and high_20d < tp: + # 已持仓股不适用20日新高阻力(用户已按原始推荐买入,RR应保持原值) + _owned = conn.execute("SELECT shares FROM holdings WHERE code=? AND is_active=1 AND shares>0", (code,)).fetchone() + if not _owned and ref > 0 and high_20d > ref and high_20d < tp: target = high_20d def _rr(x): @@ -1282,18 +1284,32 @@ def recompute_rr(conn, code: str) -> float: (rr_mid, rr_low, rr_high, code)) conn.commit() compute_rec_score(conn, code) # RR 变→评分同步刷新 - # RR<2.0 且信号为买入 → 降级为关注(2026-07-28 老爸:RR<2 的买入不推荐) - if rr_mid < 2.0: - _sig_r = conn.execute("SELECT timing_signal FROM holding_strategies WHERE code=? AND status=?", (code, "active")).fetchone() - if _sig_r and _sig_r[0] in ("买入", "可买入", "可加仓"): - conn.execute("UPDATE holding_strategies SET timing_signal=? WHERE code=? AND status=?", ("关注", code, "active")) - print(f" [RR-DOWNGRADE] {code} RR={rr_mid:.2f}<2.0, 买入→关注", flush=True) return rr_mid except Exception as e: print(f" [RR] {code} 重算失败: {e}", flush=True) return 0.0 +def _calc_ta_score(code: str) -> dict: + """严谨技术指标计算:RSI/MACD/量能/背离——从stock_daily确定性计算。 + 返回 {'rsi': float, 'macd_hist': float, 'macd_hist_prev': float, 'volume_trend': str, 'divergence': str}""" + try: + import technical_analysis as _ta_mod + _ta_full = _ta_mod.full_analysis(code) + if _ta_full and "error" not in _ta_full: + _scores = _ta_full.get("scores", {}) + return { + 'rsi': _scores.get("rsi", 50), + 'macd_hist': _scores.get("macd_hist", 0), + 'macd_hist_prev': _scores.get("macd_hist_prev", 0), + 'volume_trend': _scores.get("volume_trend", "neutral"), + 'divergence': _scores.get("divergence", "none"), + } + except Exception as _e: + print(f" [TA-SCORE] {code} 技术分计算失败: {_e}", flush=True) + return {'rsi': 50, 'macd_hist': 0, 'macd_hist_prev': 0, 'volume_trend': "neutral", 'divergence': "none"} + + def compute_rec_score(conn, code: str) -> int: """五维复合推荐评分 0-100。RR高≠值得买,趋势+行业+信号综合判断。 维度:RR(0-35) + 信号(0-25) + 趋势(0-20) + 行业(0-10) + 区间(0-10)""" @@ -1358,6 +1374,21 @@ def compute_rec_score(conn, code: str) -> int: elif zone_pct >= 2: s_zone = 4 else: s_zone = 2 + # ── 5. 严谨技术指标(2026-07-28 老爸:不许伪造好看的RR)── + _ta_s = _calc_ta_score(code) + _rsi = _ta_s["rsi"] + if 30 <= _rsi <= 70: s_trend += 3 + elif _rsi < 30: s_trend += 5 + elif _rsi > 70: s_trend -= 5 + _mh, _mp = _ta_s["macd_hist"], _ta_s["macd_hist_prev"] + if _mh > 0 and _mh > _mp: s_trend += 5 + elif _mh < 0 and _mh < _mp: s_trend -= 5 + _vt = _ta_s["volume_trend"] + if _vt == "accumulation": s_trend += 5 + elif _vt == "distribution": s_trend -= 5 + _div = _ta_s["divergence"] + if _div == "bearish": s_trend -= 8 + elif _div == "bullish": s_trend += 5 total = s_rr + s_sig + s_trend + s_sec + s_zone conn.execute( "UPDATE holding_strategies SET rec_score=? WHERE code=? AND status='active'", @@ -1443,8 +1474,17 @@ def sync_recommend_tag(conn, code: str, timing_signal: str): if timing_signal in _ACTION_BUY or timing_signal in _ACTION_SELL: # ── 买入RR质量门禁(2026-07-27 老爸:RR<2.0的买入不值得推荐,tag都不能打,防盯盘垃圾)── # 此前tag先打、enqueue再查RR,结果RR<2.0的tag已落盯盘,与XMPP不一致。 + # 已持仓股不再重复推荐(2026-07-28 老爸:已买了的票该在持仓不在推荐) _should_tag = True - if timing_signal in _ACTION_BUY: + _owned = conn.execute( + "SELECT shares FROM holdings WHERE code=? AND is_active=1 AND shares>0", + (code,)).fetchone() + if _owned and _owned[0] and _owned[0] > 0: + _should_tag = False # 已持仓,不再重复推荐 + if _old_tag == 'current_recommend': + conn.execute("UPDATE holding_strategies SET tag='' WHERE code=? AND status='active'", (code,)) + conn.commit() + elif timing_signal in _ACTION_BUY: _rr_chk = conn.execute( "SELECT rr_ratio FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() if _rr_chk and (_rr_chk[0] or 0) < 2.0: @@ -1545,8 +1585,8 @@ def enqueue_recommend(conn, code: str): def track_strategy_version(conn, code: str): - """版本化策略追踪:每次策略变更(sync_recommend_tag 后)自动记录新版本。 - 只在 tag='current_recommend' 时记录;与上一条相比有实质变更才追加新版本。""" + """版本化策略追踪:每次策略变更自动记录新版本。 + 跟踪所有策略状态(不限于 tag='current_recommend'),tag 清除时也记录。""" try: row = conn.execute( "SELECT name, timing_signal, rec_score, rr_ratio, entry_low, entry_high, " @@ -1555,8 +1595,9 @@ def track_strategy_version(conn, code: str): if not row: return name, sig, score, rr, el, eh, sl, tp, pos, tag = row - if tag != 'current_recommend': - return # 非推荐状态不追踪 + # 空壳策略(无信号/无买入区)不追踪 + if not sig or (not el and not eh): + return lp = conn.execute("SELECT price FROM live_prices WHERE code=?", (code,)).fetchone() price = lp[0] if lp and lp[0] else 0