feat(加工层): 实时技术指标计算+price_monitor联动+collect_data改为读stock_indicators
- 新增realtime_indicators.py: 盘中实时计算MA/支撑阻力/RSI/bias60/dist_ma20/ATR/candle_pattern - price_monitor: 更新价格后联动计算实时指标写入stock_indicators - batch_reassess collect_data: 从stock_indicators读取(不再重新计算ta.full_analysis) - stock_indicators表: 新增strong_support/weak_support/pivot/weak_resist/strong_resist/candle_pattern字段
This commit is contained in:
@@ -123,30 +123,26 @@ def collect_data(code):
|
|||||||
except Exception:
|
except Exception:
|
||||||
data["strategy_def"] = None
|
data["strategy_def"] = None
|
||||||
# ── 技术指标收集(策略输入数据)──
|
# ── 技术指标收集(策略输入数据)──
|
||||||
|
# 从 stock_indicators 读取实时技术指标(由 price_monitor 联动更新)
|
||||||
try:
|
try:
|
||||||
import technical_analysis as _ta
|
_si = _db2.execute(
|
||||||
_tech = _ta.full_analysis(code)
|
"SELECT ma5, ma10, ma20, ma60, rsi, bias60, dist_ma20, strong_support, weak_support, pivot, weak_resist, strong_resist, candle_pattern, vol_ratio "
|
||||||
if _tech:
|
"FROM stock_indicators WHERE code=? ORDER BY date DESC LIMIT 1",
|
||||||
_sr = _tech.get("support_resistance", {})
|
(code,)).fetchone()
|
||||||
data["ta_strong_support"] = _sr.get("strong_support", 0)
|
if _si:
|
||||||
data["ta_weak_support"] = _sr.get("weak_support", 0)
|
data["ta_ma5"] = _si[0] if _si[0] else None
|
||||||
data["ta_pivot"] = _sr.get("pivot", 0)
|
data["ta_ma10"] = _si[1] if _si[1] else None
|
||||||
data["ta_weak_resist"] = _sr.get("weak_resist", 0)
|
data["ta_ma20"] = _si[2] if _si[2] else None
|
||||||
data["ta_strong_resist"] = _sr.get("strong_resist", 0)
|
data["ta_ma60"] = _si[3] if _si[3] else None
|
||||||
_cs = _tech.get("candlestick", {})
|
data["ta_rsi"] = round(_si[4], 1) if _si[4] else None
|
||||||
data["ta_candle"] = _cs.get("pattern", "") + "/" + _cs.get("sentiment", "")
|
data["ta_dist_ma20"] = _si[6] if _si[6] else None
|
||||||
_vol = _tech.get("volume", {})
|
data["ta_strong_support"] = _si[7] if _si[7] else None
|
||||||
data["ta_volume"] = _vol.get("description", "")
|
data["ta_weak_support"] = _si[8] if _si[8] else None
|
||||||
import re as _re
|
data["ta_pivot"] = _si[9] if _si[9] else None
|
||||||
_snap = data.get("tech_snapshot", "")
|
data["ta_weak_resist"] = _si[10] if _si[10] else None
|
||||||
_ma = _re.search(r'MA5=([\d.]+).*?MA10=([\d.]+).*?MA20=([\d.]+).*?MA60=([\d.]+)', _snap)
|
data["ta_strong_resist"] = _si[11] if _si[11] else None
|
||||||
if _ma:
|
data["ta_candle"] = _si[12] or ""
|
||||||
data["ta_ma5"] = float(_ma.group(1))
|
data["ta_volume"] = ""
|
||||||
data["ta_ma10"] = float(_ma.group(2))
|
|
||||||
data["ta_ma20"] = float(_ma.group(3))
|
|
||||||
data["ta_ma60"] = float(_ma.group(4))
|
|
||||||
if data["ta_ma20"] > 0:
|
|
||||||
data["ta_dist_ma20"] = round((data["price"] - data["ta_ma20"]) / data["ta_ma20"] * 100, 2)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -376,6 +376,17 @@ def refresh_data_prices():
|
|||||||
"VALUES (?,?,?,datetime('now','localtime'))",
|
"VALUES (?,?,?,datetime('now','localtime'))",
|
||||||
(code, p, cp)
|
(code, p, cp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 实时技术指标联动更新
|
||||||
|
try:
|
||||||
|
from realtime_indicators import update_single
|
||||||
|
for h in db_holdings:
|
||||||
|
_ri_code = h.get("code", "")
|
||||||
|
_ri_price = h.get("price", 0)
|
||||||
|
if _ri_code and _ri_price:
|
||||||
|
update_single(_ri_code, float(_ri_price))
|
||||||
|
except Exception as e:
|
||||||
|
print(" realtime_indicators error:", e, file=sys.stderr)
|
||||||
# 补充策略股/自选股的价格(不在holdings中的)
|
# 补充策略股/自选股的价格(不在holdings中的)
|
||||||
for code, pdata in prices.items():
|
for code, pdata in prices.items():
|
||||||
if code not in {h.get('code') for h in db_holdings}:
|
if code not in {h.get('code') for h in db_holdings}:
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
#!/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)
|
||||||
|
|
||||||
|
# 读最近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 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
|
||||||
|
|
||||||
|
# RSI(14日)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# ATR(14日平均真实波幅)
|
||||||
|
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 id 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()
|
||||||
Reference in New Issue
Block a user