feat: strategy_lifecycle + stale_detector + server — DB-first price reads, Tencent API as fallback only

This commit is contained in:
知微
2026-07-01 22:48:48 +08:00
parent 849495c4ba
commit 8ed755bff9
3 changed files with 227 additions and 32 deletions
+151 -8
View File
@@ -354,13 +354,37 @@ def load_macro_context():
def batch_fetch_prices(codes):
"""批量获取实时价格,合并为一次API调用(自动分批,每批15只)"""
"""获取实时价格。优先从 DB 读取(price_monitor 每 2 分钟更新),失败才拉腾讯 API。"""
if not codes:
return {}
# 分批处理,避免单次请求过大导致超时
batch_size = 15
all_results = {}
# 主通道:从 DB 读取(price_monitor 唯一价格入口)
try:
import sqlite3
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
db.row_factory = sqlite3.Row
for raw_code in codes:
raw_code = str(raw_code).split('_')[0]
if not raw_code: continue
row = db.execute(
"SELECT price, change_pct FROM holdings WHERE code=? AND is_active=1", (raw_code,)
).fetchone()
if not row:
row = db.execute(
"SELECT price, change_pct FROM holding_strategies WHERE code=? AND status='active' ORDER BY updated_at DESC LIMIT 1", (raw_code,)
).fetchone()
if row and row['price']:
all_results[raw_code] = (row['price'], 0, row['change_pct'] or 0)
db.close()
if all_results:
return all_results
except Exception:
pass
# Fallback: 腾讯 API(仅当 DB 无数据时)
batch_size = 15
for batch_start in range(0, len(codes), batch_size):
batch = codes[batch_start:batch_start + batch_size]
symbols = []
@@ -423,16 +447,33 @@ def batch_fetch_prices(codes):
def get_price_tencent(code):
"""获取实时价格,港股转CNY统一存CNY"""
"""获取实时价格。优先 DBprice_monitor 维护),失败才拉腾讯。港股价格已是 CNY"""
raw_code = str(code).split('_')[0]
if not raw_code:
return None
# 主通道: DB
try:
import sqlite3
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
db.row_factory = sqlite3.Row
row = db.execute("SELECT price FROM holdings WHERE code=? AND is_active=1", (raw_code,)).fetchone()
if not row:
row = db.execute("SELECT price FROM holding_strategies WHERE code=? AND status='active' ORDER BY updated_at DESC LIMIT 1", (raw_code,)).fetchone()
if row and row['price']:
db.close()
return row['price']
db.close()
except Exception:
pass
# Fallback: 腾讯 API
try:
from mo_models import to_cny, is_hk_stock
except ImportError:
to_cny = lambda v, r=None: v
is_hk_stock = lambda c: len(str(c).strip()) == 5 and str(c).strip().isdigit()
try:
raw_code = code.split('_')[0]
if not raw_code:
return None
if is_hk_stock(raw_code):
prefix = "hk"
elif raw_code.startswith("6") or raw_code.startswith("5"):
@@ -786,11 +827,104 @@ def reassess_strategy(code, name, price, cost, shares, current_action,
if entry_high < max_change:
entry_high = round(max_change, 2)
# ----- 买入时机信号 -----
# ----- 买入时机信号(三维分析:大盘+行业+个股,基本面+消息面+技术面+资金流)-----
# [2026-07-01] 扩展:不再只看volume_signal + candlestick_sentiment
# 融合大盘趋势、行业板块强弱、基本面估值作为修正因子
volume_signal = vol.get("volume_signal", "")
candlestick_sentiment = candle.get("sentiment", "neutral")
timing_signal = "neutral"
# --- 三维分析数据装载 ---
# 因子1: 大盘环境(从macro_context_log读)
market_bearish = False
market_bullish = False
try:
import sqlite3
_db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=5)
_mc = _db.execute(
"SELECT structure FROM macro_context_log WHERE has_valid_data=1 ORDER BY rowid DESC LIMIT 1"
).fetchone()
if _mc and _mc[0]:
_s = json.loads(_mc[0])
_overall = _s.get("overall", "")
if "bearish" in _overall:
market_bearish = True
elif _overall == "bullish":
market_bullish = True
_db.close()
except Exception:
pass
# 因子2: 行业板块强弱
sector_strong = False
sector_weak = False
try:
_db2 = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=5)
_rows2 = _db2.execute(
"SELECT name, change_pct FROM sector_snapshots ORDER BY change_pct DESC"
).fetchall()
if _rows2:
# 找到该股所属行业(简单匹配name或通过stock_sectors
_my_sectors = _db2.execute(
"SELECT sector_name FROM stock_sectors WHERE code=?",
(code,)
).fetchall()
if _my_sectors:
for (_sn,) in _my_sectors:
for r_name, r_chg in _rows2:
if _sn in r_name or r_name in _sn:
_rank = [r[0] for r in _rows2].index(r_name) if r_name in [x[0] for x in _rows2] else -1
_total = len(_rows2)
if _rank >= 0:
if _rank < _total * 0.2:
sector_strong = True
if _rank > _total * 0.8:
sector_weak = True
break
_db2.close()
except Exception:
pass
# 因子3: 基本面估值
is_value_stock = False
try:
_db3 = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=5)
_fd = _db3.execute(
"SELECT pe, eps FROM stock_fundamentals WHERE code=?", (code,)
).fetchone()
if _fd:
_pe, _eps = _fd
is_value_stock = (0 < (_pe or 0) < 25 and (_eps or 0) > 0.3)
_db3.close()
except Exception:
pass
# --- 三维修正规则 ---
# 大盘偏弱时收紧买入信号,大盘偏强时放宽
# 行业领先加分,行业落后减分
# 低估值加分(有安全边际)
def _adjust_timing(signal, market_b, market_bb, sec_s, sec_w, is_val):
"""根据三维因子修正 timing_signal"""
# 大盘偏弱时降级买入信号
if market_b:
if signal in ("买入", "加仓"):
if not sec_s: # 大盘弱+行业不强→降级
return "关注"
# 大盘偏强时放宽
if market_bb:
if signal == "关注" and (sec_s or is_val):
return "买入"
# 行业弱势时降级买入信号
if sec_w:
if signal in ("买入", "加仓"):
return "关注"
# 行业强势+低估时升级关注
if sec_s and is_val:
if signal == "关注":
return "买入"
return signal
if is_new_entry:
# 新买入时机
if volume_signal == "主动买盘占优" and candlestick_sentiment == "bullish":
@@ -803,6 +937,15 @@ def reassess_strategy(code, name, price, cost, shares, current_action,
timing_signal = "买入"
elif ws and price < ws * 1.02:
timing_signal = "关注"
# 新买入时三维修正:大盘向上+行业强→升级,大盘弱→降级
_pre_signal = timing_signal
timing_signal = _adjust_timing(timing_signal, market_bearish, market_bullish,
sector_strong, sector_weak, is_value_stock)
if timing_signal != _pre_signal:
print(f" 三维修正(新入): {_pre_signal}{timing_signal} "
f"| 大盘{'' if market_bearish else '' if market_bullish else '中性'}"
f"| 行业{'' if sector_strong else '' if sector_weak else '中性'}"
f"| 估值{'' if is_value_stock else '一般'}")
else:
# 已持仓时机(用于加仓/减仓参考)
if is_short_term_strong_trend: