Files
MoFin/deploy/profile-scripts/realtime_indicators.py

177 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""realtime_indicators.py — 盘中实时技术指标计算+写入stock_indicators
触发:price_monitor 更新价格后联动调用
输出:MA/支撑阻力/RSI/bias60/dist_ma20/candle_pattern 写入 stock_indicators
"""
import os, sys, sqlite3
from datetime import datetime
DB = "/home/hmo/MoFin/data/mofin.db"
def calc_realtime_indicators(code, price, date_str=None):
"""计算单只股票的实时技术指标"""
if not date_str:
date_str = datetime.now().strftime("%Y-%m-%d")
conn = sqlite3.connect(DB, timeout=30)
# 用 live_prices 的实时价格作为最新价(而非 stock_daily 的收盘价)
lp = conn.execute("SELECT price FROM live_prices WHERE code=?", (code,)).fetchone()
if lp and lp[0]:
latest_price = float(lp[0])
else:
# fallback 到 stock_daily
dr = conn.execute("SELECT close FROM stock_daily WHERE code=? ORDER BY date DESC LIMIT 1", (code,)).fetchone()
latest_price = float(dr[0]) if dr and dr[0] else price
# 读最近60日K线(计算MA/RSI/bias60需要)
rows = conn.execute(
"SELECT date, close FROM stock_daily WHERE code=? ORDER BY date DESC LIMIT 60",
(code,)).fetchall()
if not rows or len(rows) < 5:
conn.close()
return None
closes = [r[1] for r in rows if r[1]]
if latest_price:
closes = [latest_price] + closes # 实时价格在最前
if not closes:
conn.close()
return None
result = {"code": code, "date": date_str, "updated_at": datetime.now().isoformat()}
# MA(用最近N日收盘价,当天价格替代最新收盘价)
closes_with_today = [price] + closes # 最新价在前
result["ma5"] = round(sum(closes_with_today[:5]) / min(5, len(closes_with_today)), 2) if closes_with_today else None
result["ma10"] = round(sum(closes_with_today[:10]) / min(10, len(closes_with_today)), 2) if closes_with_today else None
result["ma20"] = round(sum(closes_with_today[:20]) / min(20, len(closes_with_today)), 2) if closes_with_today else None
result["ma60"] = round(sum(closes_with_today[:60]) / min(60, len(closes_with_today)), 2) if closes_with_today else None
# RSI14日)
if len(closes_with_today) >= 15:
gains = []
losses = []
for i in range(1, min(15, len(closes_with_today))):
diff = closes_with_today[i-1] - closes_with_today[i]
if diff > 0:
gains.append(diff)
losses.append(0)
else:
gains.append(0)
losses.append(abs(diff))
avg_gain = sum(gains) / len(gains) if gains else 0
avg_loss = sum(losses) / len(losses) if losses else 0.01
rs = avg_gain / avg_loss
result["rsi"] = round(100 - 100 / (1 + rs), 2)
# bias60(偏离60日均线百分比)
if result.get("ma60") and result["ma60"] > 0:
result["bias60"] = round((price - result["ma60"]) / result["ma60"] * 100, 2)
# dist_ma20(距MA20百分比)
if result.get("ma20") and result["ma20"] > 0:
result["dist_ma20"] = round((price - result["ma20"]) / result["ma20"] * 100, 2)
# 支撑阻力位(简化版:基于近期高低点)
recent_lows = sorted(closes[:20]) # 近20日最低
recent_highs = sorted(closes[:20], reverse=True)
result["weak_support"] = round(recent_lows[0], 2) if recent_lows else None
result["strong_support"] = round(recent_lows[2], 2) if len(recent_lows) > 2 else result["weak_support"]
result["weak_resist"] = round(recent_highs[0], 2) if recent_highs else None
result["strong_resist"] = round(recent_highs[2], 2) if len(recent_highs) > 2 else result["weak_resist"]
result["pivot"] = round((result.get("strong_support", price) + result.get("strong_resist", price)) / 2, 2)
# ATR14日平均真实波幅)
if len(rows) >= 15:
trs = []
for i in range(14):
h = closes[i] if i < len(closes) else closes[-1]
l = closes[i+1] if i+1 < len(closes) else closes[-1]
c_prev = closes[i+1] if i+1 < len(closes) else closes[-1]
tr = max(h - l, abs(h - c_prev), abs(l - c_prev))
trs.append(tr)
result["atr"] = round(sum(trs) / len(trs), 2) if trs else None
# 量比(当日量/5日均量)
volumes = conn.execute(
"SELECT volume FROM stock_daily WHERE code=? ORDER BY date DESC LIMIT 6",
(code,)).fetchall()
if volumes and len(volumes) >= 2:
today_vol = volumes[0][0] or 0
avg_vol = sum(v[0] for v in volumes[1:6]) / min(5, len(volumes)-1) if len(volumes) > 1 else 1
result["vol_ratio"] = round(today_vol / avg_vol, 2) if avg_vol > 0 else None
conn.close()
return result
def save_indicators(data):
"""写入 stock_indicators 表(INSERT OR REPLACE"""
if not data:
return
conn = sqlite3.connect(DB, timeout=30)
# 检查是否已有该日期记录
existing = conn.execute(
"SELECT code FROM stock_indicators WHERE code=? AND date=?",
(data["code"], data["date"])).fetchone()
fields = [k for k in data if k not in ("code", "date")]
cols = ", ".join(["code", "date"] + fields)
vals = ", ".join(["?"] * (2 + len(fields)))
updates = ", ".join(f"{f}=excluded.{f}" for f in fields)
conn.execute(
f"INSERT INTO stock_indicators ({cols}) VALUES ({vals}) "
f"ON CONFLICT(code, date) DO UPDATE SET {updates}",
[data["code"], data["date"]] + [data[f] for f in fields])
conn.commit()
conn.close()
def update_all_active():
"""批量更新所有活跃股票的实时指标(每日收盘后)"""
conn = sqlite3.connect(DB, timeout=30)
today = datetime.now().strftime("%Y-%m-%d")
codes = set()
for r in conn.execute("SELECT code FROM holdings WHERE is_active=1"):
codes.add(r[0])
for r in conn.execute("SELECT code FROM watchlist_stocks"):
codes.add(r[0])
for r in conn.execute("SELECT code FROM holding_strategies WHERE status='active'"):
codes.add(r[0])
ok = 0
for code in codes:
# 从 stock_daily 读收盘价
row = conn.execute(
"SELECT close FROM stock_daily WHERE code=? ORDER BY date DESC LIMIT 1",
(code,)).fetchone()
if not row or not row[0]:
continue
price = float(row[0])
data = calc_realtime_indicators(code, price, today)
if data:
save_indicators(data)
ok += 1
conn.close()
print(f"实时指标更新: {ok}/{len(codes)} 只")
def update_single(code, price):
"""单只股票实时更新(price_monitor 联动调用)"""
data = calc_realtime_indicators(code, price)
if data:
save_indicators(data)
if __name__ == "__main__":
update_all_active()