From 56373e0770193a3a5647ba0353012a685deaeb3d Mon Sep 17 00:00:00 2001 From: hmo Date: Wed, 29 Jul 2026 08:42:37 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20technical=5Fanalysis.py=E5=88=86?= =?UTF-8?q?=E5=8F=89=E6=B2=BB=E7=90=86=E2=80=94=E2=80=94=E6=A0=B9=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E5=8F=96=E6=B6=88=E8=B7=9F=E8=B8=AA=EF=BC=8C=E4=B8=A4?= =?UTF-8?q?=E4=B8=AA=E9=99=88=E6=97=A7=E5=89=AF=E6=9C=AC=E7=A1=AC=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E5=88=B0=E6=9D=83=E5=A8=81=E7=89=88(deploy/profile-sc?= =?UTF-8?q?ripts)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + technical_analysis.py | 679 ------------------------------------------ 2 files changed, 1 insertion(+), 679 deletions(-) delete mode 100644 technical_analysis.py diff --git a/.gitignore b/.gitignore index dea92e02..ffcb95bb 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ scripts/mofin_db.py scripts/mo_data.py mofin_db.py mo_data.py +technical_analysis.py diff --git a/technical_analysis.py b/technical_analysis.py deleted file mode 100644 index 7465d678..00000000 --- a/technical_analysis.py +++ /dev/null @@ -1,679 +0,0 @@ -#!/usr/bin/env python3 -"""technical_analysis.py — 技术面分析模块 v2 - -基于多日价格数据计算支撑位/压力位: -1. 缓存每日 HLC 到 price_history.json -2. 使用 5 日最高/最低计算枢轴点 -3. 结合振幅自动调整区间宽度 - -使用方式: - from technical_analysis import full_analysis - result = full_analysis("603259") # 自动识别A股/港股 -""" - -import json -import os -import urllib.request -from datetime import datetime, date - -# 腾讯API字段索引 -F = { - "name": 1, "code": 2, "price": 3, "close_yest": 4, "open": 5, - "volume": 6, "timestamp": 30, "change": 31, "change_pct": 32, - "high": 33, "low": 34, "amplitude": 43, - "turnover": 38, "pe": 39, "pb": 46, - "limit_up": 47, "limit_down": 48, - "avg_price": 51, "inner_vol": 52, "outer_vol": 53, -} - -HISTORY_PATH = "/home/hmo/web-dashboard/data/price_history.json" -HISTORY_DAYS = 60 # 使用最近 N 天的 HLC 数据 - - -def _load_history(): - """读取价格历史缓存""" - try: - return json.load(open(HISTORY_PATH)) - except (FileNotFoundError, json.JSONDecodeError): - return {} - - -def _save_history(h): - json.dump(h, open(HISTORY_PATH, "w"), ensure_ascii=False, indent=2) - - -def _market_prefix(code): - """根据代码确定腾讯API前缀""" - if code.startswith("sh") or code.startswith("sz") or code.startswith("hk"): - code = code[2:] if code[2:].isdigit() else code - raw = str(code).split("_")[0] - if len(raw) == 5 and raw.isdigit(): - return "hk" - if raw.startswith("6") or raw.startswith("5"): - return "sh" - return "sz" - - -def get_quote(code): - """获取行情数据。先拿DB的价格和涨跌幅,再调腾讯API拿HLC全量数据""" - import time - _cache = get_quote.__dict__.get("_cache", {}) - now = time.time() - cached = _cache.get(code) - if cached and (now - cached["ts"]) < 60: - return cached["data"] - - # 先从DB拿基础价格(快速,不阻塞) - db_price = None - db_chg = None - try: - from mofin_db import get_price_from_db - p, chg = get_price_from_db(code) - if p: - db_price, db_chg = p, chg - except: - pass - - # 腾讯API获取全量HLC数据 - raw = str(code).split("_")[0] - prefix = _market_prefix(code) - url = f"http://qt.gtimg.cn/q={prefix}{raw}" - try: - r = urllib.request.urlopen(url, timeout=5) - fields = r.read().decode("gbk").split('"')[1].split("~") - except Exception as e: - if db_price: - return {"code": code, "price": db_price, "change_pct": db_chg or 0} - return {"code": code, "error": str(e)} - - def get(i): - try: - return float(fields[i]) if fields[i].strip() else None - except (IndexError, ValueError): - return None - - today_str = date.today().isoformat() - q = { - "code": raw, - "market": prefix, - "name": fields[F["name"]] if len(fields) > F["name"] else code, - "price": get(3), - "close_yest": get(4), - "open": get(5), - "high": get(33), - "low": get(34), - "volume": get(6), - "amount": get(37), - "change": get(31), - "change_pct": get(32), - "amplitude": get(43), - "turnover_rate": get(38), - "pe": get(39), - "pb": get(46), - "limit_up": get(47), - "limit_down": get(48), - "avg_price": get(51), - "inner_vol": get(52), - "outer_vol": get(53), - "timestamp": fields[F["timestamp"]] if len(fields) > F["timestamp"] else "", - "_date": today_str, - } - - # 写入价格历史缓存(每日一次) - h = get(33) # high - l = get(34) # low - c = get(3) # price / close - v = get(6) # volume(手) - amt = get(37) # 成交额 - if h and l and c: - history = _load_history() - if raw not in history: - history[raw] = [] - days = history[raw] - # 如果今天已有记录,更新(盘中数据更精确) - if days and len(days) > 0 and days[-1].get("date") == today_str: - days[-1]["high"] = max(days[-1]["high"], h) - days[-1]["low"] = min(days[-1]["low"], l) - days[-1]["close"] = c # 盘中用最新价,收盘后是收盘价 - if v: days[-1]["volume"] = v - if amt: days[-1]["amount"] = amt - else: - entry = {"date": today_str, "high": h, "low": l, "close": c} - if v: entry["volume"] = v - if amt: entry["amount"] = amt - days.append(entry) - # 只保留最近 HISTORY_DAYS 天 - history[raw] = days[-HISTORY_DAYS:] - _save_history(history) - - # 写入60秒缓存 - get_quote.__dict__["_cache"] = {**get_quote.__dict__.get("_cache", {}), code: {"ts": now, "data": q}} - - return q - - -def calc_support_resistance(q): - """计算技术支撑位和压力位 — 多日枢轴点算法 - - 使用多个数据源确定有效区间: - 1. 当日波幅(H-L) - 2. 最近 N 日的最高/最低(从 price_history.json 读取) - 3. 价格基数的百分比(对大市值低波动股票有效) - """ - h = q.get("high") - l = q.get("low") - c = q.get("price") - yc = q.get("close_yest") - amplitude = q.get("amplitude") # 当日振幅% - code = q.get("code", "") - - if not all([h, l, c]): - return {"error": "数据不足"} - - # 多日最高/最低(从历史缓存读取) - history = _load_history() - hist_days = history.get(code, []) - multi_high = max(d["high"] for d in hist_days) if hist_days else h - multi_low = min(d["low"] for d in hist_days) if hist_days else l - - # 有效区间 = max(当日波幅, 多日波幅, 价格×5%) - daily_range = h - l - multi_range = multi_high - multi_low - min_range = c * 0.05 # 5%价格基数 - - effective_range = max(daily_range, multi_range, min_range) - - # 如果股价接近多日高点(>80%分位),说明在上升趋势中,扩大区间 - trend_position = (c - multi_low) / (multi_high - multi_low) if multi_high > multi_low else 0.5 - if trend_position > 0.8: - # 高位运行,扩大有效区间到价格的8%确保合理空间 - effective_range = max(effective_range, c * 0.08) - elif trend_position < 0.2: - # 低位运行,同样扩大 - effective_range = max(effective_range, c * 0.08) - - # 如果振幅数据可用且振幅较小(<3%),进一步扩大区间确保有效性 - if amplitude and amplitude > 0 and amplitude < 3: - # 低波动股票用 振幅×3 作为最小范围 - amp_based = c * amplitude / 100 * 3 - effective_range = max(effective_range, amp_based) - - # 枢轴点 (Pivot Point) - pp = (h + l + c) / 3 - - # 支撑位 - s1 = 2 * pp - h # 弱支撑 - s2 = pp - effective_range # 强支撑 - - # 压力位 - r1 = 2 * pp - l # 弱压力 - r2 = pp + effective_range # 强压力 - - # 参考昨收调整 - if yc: - if yc < s1: - s1 = yc - if yc > r1: - r1 = yc - - # A股涨停/跌停价作为极端边界 - limit_up = q.get("limit_up") - limit_down = q.get("limit_down") - market = q.get("market", "hk") - if market != "hk" and limit_up and limit_down: - # 注意:当现价逼近涨停/跌停时,limit不再是有效边界 - # 用有效区间判断:如果自然计算的r2/s2在合理范围内不截断 - natural_r2 = r2 - natural_s2 = s2 - # 涨停限制只对距离现价超过2%的强压位生效 - if limit_up < r2 and (limit_up - c) / c < 0.02: - # 涨停价离现价<2%,说明可能封板,不截断 - pass # 使用自然计算的r2 - elif limit_up < r2: - r2 = limit_up - if limit_down > s2 and (c - limit_down) / c < 0.02: - pass # 接近跌停,不截断 - elif limit_down > s2: - s2 = limit_down - - return { - "strong_support": round(s2, 2), - "weak_support": round(s1, 2), - "pivot": round(pp, 2), - "weak_resist": round(r1, 2), - "strong_resist": round(r2, 2), - "today_high": h, - "today_low": l, - "multi_high": multi_high, - "multi_low": multi_low, - "effective_range": round(effective_range, 2), - } - - -def analyze_candlestick(q): - """判断K线形态""" - o = q.get("open") - c = q.get("price") - h = q.get("high") - l = q.get("low") - yc = q.get("close_yest") - - if not all([o, c, h, l]): - return {"pattern": "unknown", "sentiment": "neutral"} - - if c >= o: - body = c - o - upper = h - c - lower = o - l - is_green = True - else: - body = o - c - upper = h - o - lower = c - l - is_green = False - - total_range = h - l - if total_range == 0: - return {"pattern": "平盘", "sentiment": "neutral"} - - body_pct = body / total_range * 100 - upper_pct = upper / total_range * 100 - lower_pct = lower / total_range * 100 - - if body_pct < 5: - if upper_pct > 60: - pattern = "倒T线/射击之星" - sentiment = "bearish" - elif lower_pct > 60: - pattern = "锤子线/T字线" - sentiment = "bullish" - else: - pattern = "十字星" - sentiment = "neutral" - elif body_pct < 30: - if upper_pct > 40 and lower_pct > 40: - pattern = "长影星线" - sentiment = "neutral" - elif upper_pct > 40: - pattern = "倒T线/射击之星" - sentiment = "bearish" if is_green else "bearish" - elif lower_pct > 40: - pattern = "锤子线/T字线" - sentiment = "bullish" if is_green else "bullish" - else: - pattern = "小阳线" if is_green else "小阴线" - sentiment = "bullish" if is_green else "bearish" - else: - if upper_pct > 30: - pattern = "带上影阳线" if is_green else "带上影阴线" - sentiment = "neutral" if is_green else "bearish" - elif lower_pct > 30: - pattern = "带下影阳线" if is_green else "带下影阴线" - sentiment = "bullish" if is_green else "neutral" - else: - pattern = "光头光脚阳线" if is_green else "光头光脚阴线" - sentiment = "bullish" if is_green else "bearish" - - gap_up = "" - gap_down = "" - if yc: - if o > yc * 1.01: - gap_up = "跳空高开" - if not is_green: - sentiment = "neutral" - elif o < yc * 0.99: - gap_down = "跳空低开" - if is_green: - sentiment = "neutral" - - return { - "pattern": pattern, - "sentiment": sentiment, - "body_pct": round(body_pct, 1), - "upper_shadow_pct": round(upper_pct, 1), - "lower_shadow_pct": round(lower_pct, 1), - "is_green": is_green, - "gap": gap_up or gap_down or "无跳空", - } - - -def analyze_volume(q): - """量价分析""" - outer = q.get("outer_vol") - inner = q.get("inner_vol") - turnover = q.get("turnover_rate") - - result = {} - if outer and inner and (outer + inner) > 0: - ratio = outer / (outer + inner) - result["buy_sell_ratio"] = round(ratio, 2) - if ratio > 0.55: - result["volume_signal"] = "主动买盘占优" - elif ratio < 0.45: - result["volume_signal"] = "主动卖盘占优" - else: - result["volume_signal"] = "买卖均衡" - else: - result["volume_signal"] = "数据不足" - - if turnover: - result["turnover_rate"] = turnover - - return result - - -def analyze_volume_trend(code): - """量价趋势分析:对比历史N日平均成交量,检测量价背离模式 - - 从 price_history.json 读取历史数据,比较今日量价关系。 - """ - result = {} - try: - history = _load_history() - days = history.get(code, []) - if len(days) < 3: - result["trend"] = "数据不足" - return result - - today = days[-1] - prev = days[-2] if len(days) >= 2 else None - today_vol = today.get("volume", 0) - today_close = today.get("close", 0) - - if not today_vol or not today_close: - result["trend"] = "数据不足" - return result - - # 计算N日均量 - vols_5 = [d.get("volume", 0) for d in days[-6:-1] if d.get("volume")] - vols_20 = [d.get("volume", 0) for d in days[-21:-1] if d.get("volume")] - avg_5 = sum(vols_5) / len(vols_5) if vols_5 else 0 - avg_20 = sum(vols_20) / len(vols_20) if vols_20 else 0 - - vol_ratio_vs_5 = today_vol / avg_5 if avg_5 > 0 else 0 - vol_ratio_vs_20 = today_vol / avg_20 if avg_20 > 0 else 0 - - result["avg_volume_5d"] = round(avg_5, 0) - result["avg_volume_20d"] = round(avg_20, 0) - result["today_volume"] = int(today_vol) - result["volume_ratio_vs_5d"] = round(vol_ratio_vs_5, 2) - result["volume_ratio_vs_20d"] = round(vol_ratio_vs_20, 2) - - # 最近3日的收盘价和成交量趋势 - if len(days) >= 3: - recent_close = [d.get("close", 0) for d in days[-4:-1]] - recent_vol = [d.get("volume", 0) for d in days[-4:-1]] - if all(recent_close) and all(recent_vol): - price_up = today_close > recent_close[-1] - vol_up = today_vol > recent_vol[-1] - - # 量价模式判定 - if vol_ratio_vs_5 >= 1.8: - # 明显放量 - if price_up: - result["trend"] = "放量上攻" - result["action"] = "buy_conformation" - else: - # 价格下跌但大幅放量 = 恐慌?还是承接收筹? - # 看今日K线:如果是阳线(低开高走)= 承接 - # 简单判断:如果close > open = 有承接 - result["trend"] = "放量下跌" - result["action"] = "watch" - elif vol_ratio_vs_5 <= 0.6: - # 明显缩量 - if price_up: - result["trend"] = "缩量上涨" - result["action"] = "divergence" - else: - result["trend"] = "缩量回调" - result["action"] = "healthy_pullback" - elif vol_ratio_vs_5 >= 1.3: - # 温和放量 - if price_up: - result["trend"] = "温和放量上涨" - result["action"] = "bullish" - else: - result["trend"] = "温和放量下跌" - result["action"] = "bearish" - else: - # 正常量 - if price_up: - result["trend"] = "正常量上涨" - result["action"] = "neutral_bullish" - else: - result["trend"] = "正常量下跌" - result["action"] = "neutral_bearish" - - # 量价背离检测:价格走高但成交量逐日递减 - if len(days) >= 5: - close_5 = [d.get("close", 0) for d in days[-5:]] - vol_5 = [d.get("volume", 0) for d in days[-5:]] - if all(close_5) and all(vol_5): - close_trend = close_5[-1] - close_5[0] - vol_trend = vol_5[-1] - vol_5[0] - # 价格涨但量跌 = 顶背离 - if close_trend > 0 and vol_trend < 0 and abs(vol_trend) > sum(vol_5) * 0.3: - result["divergence"] = "顶背离(价涨量缩)" - # 价格跌但量涨 = 底背离 - elif close_trend < 0 and vol_trend > 0 and abs(vol_trend) > sum(vol_5) * 0.3: - result["divergence"] = "底背离(价跌量增)" - - except Exception as e: - result["trend_error"] = str(e) - - return result - - -def analyze_volume_deep(code): - """深度量价分析:从日K线分析量价配合/背离/建仓/出货 - - 使用 mtf_cache 表的日K线数据做历史量价分析。 - """ - import sqlite3 - from pathlib import Path - - DATA_DIR = Path(__file__).parent / "scripts" / "data" - if not (DATA_DIR / "mofin.db").exists(): - DATA_DIR = Path(__file__).parent / "data" - try: - conn = sqlite3.connect(str(DATA_DIR / "mofin.db")) - row = conn.execute("SELECT cache_json FROM mtf_cache WHERE code=?", (code,)).fetchone() - conn.close() - if not row: - return {"volume_signal": "数据不足"} - data = json.loads(row[0]) - except Exception: - return {"volume_signal": "数据不足"} - - daily = data.get("daily", []) - if len(daily) < 5: - return {"volume_signal": "数据不足"} - - closes = [d["close"] for d in daily] - volume = [d["volume"] for d in daily] - n = len(daily) - - # 基准:最近20日均量(不足20日则用全部) - lookback = min(20, n - 1) - avg_vol_20d = sum(volume[-lookback-1:-1]) / lookback if lookback > 0 else volume[-1] - - # 最近N日的量比 - recent = min(5, n) - recent_vol_ratios = [] - for i in range(recent): - vol = volume[-i-1] if i+1 <= n else volume[0] - recent_vol_ratios.append(round(vol / avg_vol_20d, 2) if avg_vol_20d > 0 else 1) - - today_ratio = recent_vol_ratios[0] if recent_vol_ratios else 1 - recent_max_ratio = max(recent_vol_ratios) if recent_vol_ratios else 1 - - # 量价配合度 - signals = [] - patterns = {} - - # 1. 放量检测(量比 > 2x) - if today_ratio > 2.0: - signals.append(f"量比{today_ratio:.1f}倍放量") - patterns["volume_surge"] = True - # 放量方向 - if len(closes) >= 2 and closes[-1] > closes[-2]: - patterns["surge_direction"] = "放量上涨" - if today_ratio > 2.5 and closes[-1] > closes[-2] * 1.03: - signals[-1] += "↑主力买入" - else: - signals[-1] += "↑" - elif len(closes) >= 2 and closes[-1] < closes[-2]: - patterns["surge_direction"] = "放量下跌" - if today_ratio > 2.5 and closes[-1] < closes[-2] * 0.97: - signals[-1] += "↓主力出货" - else: - signals[-1] += "↓" - else: - patterns["surge_direction"] = "放量平盘" - elif today_ratio < 0.5: - signals.append(f"量比{today_ratio:.1f}倍缩量") - patterns["volume_shrink"] = True - else: - signals.append(f"量比{today_ratio:.1f}倍正常") - patterns["volume_normal"] = True - - # 2. 量价趋势分析(近5日 vs 前5日) - if len(daily) >= 10: - recent5_vol = sum(volume[-5:]) / 5 - prev5_vol = sum(volume[-10:-5]) / 5 - vol_trend = "增" if recent5_vol > prev5_vol * 1.3 else ("减" if recent5_vol < prev5_vol * 0.7 else "稳") - recent5_price = closes[-5:] - price_trend = "涨" if recent5_price[-1] > recent5_price[0] else ("跌" if recent5_price[-1] < recent5_price[0] * 0.95 else "平") - - if vol_trend == "增" and price_trend == "涨": - patterns["accumulation"] = True # 量价齐升=建仓 - signals.append(f"近5日{vol_trend}量{price_trend}价=建仓型") - elif vol_trend == "增" and price_trend == "跌": - patterns["distribution"] = True # 放量下跌=出货 - signals.append(f"近5日{vol_trend}量{price_trend}价=⚠️出货型") - elif vol_trend == "减" and price_trend == "涨": - patterns["divergence"] = True # 量缩价涨=背离 - signals.append(f"近5日{vol_trend}量{price_trend}价=⬆量价背离") - elif vol_trend == "减" and price_trend == "跌": - patterns["washout"] = True # 缩量下跌=洗盘末端 - signals.append(f"近5日{vol_trend}量{price_trend}价=洗盘特征") - else: - signals.append(f"近5日{vol_trend}量{price_trend}价") - else: - vol_trend = price_trend = "?" - - # 3. 寻找历史放量区间(主力活动痕迹) - surge_days = [] - for i in range(max(0, n - 60), n): - vol_ratio = volume[i] / avg_vol_20d if avg_vol_20d > 0 else 0 - if vol_ratio > 2.0: - surge_days.append({ - "date": daily[i].get("date", ""), - "ratio": round(vol_ratio, 1), - "close": closes[i], - "direction": "涨" if (i > 0 and closes[i] > closes[i-1]) else "跌" - }) - - # 汇总描述 - vol_level = "放量" if today_ratio > 2.0 else ("缩量" if today_ratio < 0.5 else "正常") - price_vol = f"{vol_level}" - if patterns.get("accumulation"): - price_vol = f"量价齐升(建仓特征) | {signals[-1]}" - elif patterns.get("distribution"): - price_vol = f"放量下跌⚠️ | {signals[-1]}" - elif patterns.get("washout"): - price_vol = f"缩量回踩(洗盘末端) | {signals[-1]}" - elif patterns.get("divergence"): - price_vol = f"量价背离 | {signals[-1]}" - - return { - "volume_signal": " ; ".join(signals) if signals else "正常", - "volume_ratio": today_ratio, - "avg_volume_20d": int(avg_vol_20d), - "recent_ratios": recent_vol_ratios, - "surge_count_60d": len(surge_days), - "price_vol_description": price_vol, - "patterns": patterns, - "surge_days": surge_days[-5:] if surge_days else [], - } - - -def full_analysis(code): - """完整技术分析(带30秒缓存,避免分钟级波动)""" - import time - _cache = full_analysis.__dict__.get("_cache", {}) - now = time.time() - cached = _cache.get(code) - if cached and (now - cached["ts"]) < 30: - return cached["data"] - - q = get_quote(code) - if not q or "error" in q: - return q - - sr = calc_support_resistance(q) - candle = analyze_candlestick(q) - vol = analyze_volume(q) - # 深度量价分析(使用日K线历史数据) - vol_deep = {} - try: - vol_deep = analyze_volume_deep(code) - except Exception: - pass # graceful degradation - - # 多周期+均线分析(整合 multi_timeframe) - mtf = {} - try: - from multi_timeframe import full_multi_tf_analysis as _mtf - mtf_raw = _mtf(code) - if mtf_raw and 'daily' in mtf_raw: - d = mtf_raw['daily'] - mtf = { - 'mas': d.get('mas', {}), - 'multi_tf_sr': d.get('support_resistance', {}), - 'trend': d.get('trend', {}), - } - # 周线弱压/弱撑作为中周期参考 - if 'weekly' in mtf_raw: - w = mtf_raw['weekly'] - ws = w.get('support_resistance', {}) - mtf['weekly_sr'] = { - 'weak_resist': ws.get('weak_resist'), - 'weak_support': ws.get('weak_support'), - } - except Exception: - pass # non-critical, graceful degradation - - result = { - "quote": { - "name": q.get("name", code), - "price": q["price"], - "change_pct": q.get("change_pct", 0), - "open": q.get("open", 0), - "high": q.get("high", 0), - "low": q.get("low", 0), - "close_yest": q.get("close_yest", 0), - "volume": q.get("volume", 0), - "amplitude": q.get("amplitude", 0), - }, - "support_resistance": sr, - "candlestick": candle, - "volume": vol, - "volume_deep": vol_deep, - "multi_tf": mtf, - "analyzed_at": datetime.now().strftime("%H:%M"), - } - - # 写入缓存 - _cache[code] = {"ts": now, "data": result} - full_analysis.__dict__["_cache"] = _cache - return result - - -if __name__ == "__main__": - import sys - codes = sys.argv[1:] or ["603259", "002594", "00700"] - for c in codes: - r = full_analysis(c) - print(json.dumps(r, ensure_ascii=False, indent=2)) - print()