feat(盯盘): 推荐操作区域置顶独立 + tag与XMPP动作级信号自动同步
- mofin_db: write_holding_strategy 内置tag同步语义——动作级信号 (买入/可买入/可加仓/卖出/止盈)→current_recommend; 信号降级→ 清除current_recommend; active_manual人工标记永不被自动流覆盖/清除 - 新增 sync_recommend_tag() 供裸SQL调用方 - batch_reassess.save_result / per_stock stage-2 调用同步 - 盯盘Tab: '重点推荐'更名'推荐操作', 区域独立琥珀色视觉, 置顶 同步语义: XMPP买入信号(ACTION级告警)的个股=推荐操作区域个股, 信号消失(重评降级)时区域同步消失
This commit is contained in:
@@ -17,7 +17,7 @@ from datetime import datetime
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, "/home/hmo/MoFin")
|
||||
from llm_client import call_llm, REASSESS_MODEL, gateway_alive, ocg_alive
|
||||
from mofin_db import snapshot_strategy_history
|
||||
from mofin_db import snapshot_strategy_history, sync_recommend_tag
|
||||
|
||||
DB = "/home/hmo/MoFin/data/mofin.db"
|
||||
COOLDOWN_HOURS = 1
|
||||
@@ -359,6 +359,9 @@ def save_result(code, full_text, parsed):
|
||||
sql = f"UPDATE holding_strategies SET {', '.join(updates)} WHERE code=? AND status='active'"
|
||||
conn.execute(sql, params)
|
||||
conn.commit()
|
||||
|
||||
# ── 推荐操作 tag 同步(与 XMPP 动作级信号同源)──
|
||||
sync_recommend_tag(conn, code, parsed.get("signal", ""))
|
||||
|
||||
# 买入信号→推XMPP通知(在conn close前执行)
|
||||
if parsed.get("signal") == "买入":
|
||||
|
||||
@@ -557,8 +557,14 @@ def main():
|
||||
if _sig_line:
|
||||
_sig = '买入' if '买入' in _sig_line[0] else '关注' if '关注' in _sig_line[0] else '观望' if '观望' in _sig_line[0] else '卖出' if '卖出' in _sig_line[0] else ''
|
||||
if _sig:
|
||||
__import__('sqlite3').connect('/home/hmo/MoFin/data/mofin.db').execute(
|
||||
"UPDATE holding_strategies SET timing_signal=? WHERE code=? AND status='active'", (_sig, code)).connection.commit()
|
||||
_ts_conn = __import__('sqlite3').connect('/home/hmo/MoFin/data/mofin.db')
|
||||
_ts_conn.execute(
|
||||
"UPDATE holding_strategies SET timing_signal=? WHERE code=? AND status='active'", (_sig, code))
|
||||
_ts_conn.commit()
|
||||
# 推荐操作 tag 同步(与 XMPP 动作级信号同源)
|
||||
from mofin_db import sync_recommend_tag
|
||||
sync_recommend_tag(_ts_conn, code, _sig)
|
||||
_ts_conn.close()
|
||||
print(f" ✅ LLM信号={_sig} 已写入")
|
||||
# 买入信号→推XMPP
|
||||
if _sig == "买入":
|
||||
|
||||
+41
-5
@@ -1108,6 +1108,28 @@ def get_prices_batch_from_db(codes: list[str]) -> dict:
|
||||
# 核心写函数 — 替代 json.dump(),强制币种约束
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
def sync_recommend_tag(conn, code: str, timing_signal: str):
|
||||
"""裸 SQL 调用方(batch_reassess / per_stock_reassess)的推荐 tag 同步。
|
||||
动作级信号 → current_recommend;信号降级 → 清除 current_recommend;
|
||||
active_manual(人工标记)永不动。与 XMPP 动作级告警同源(红线#12)。"""
|
||||
_ACTION_SIGNALS = ("买入", "可买入", "可加仓", "卖出", "止盈")
|
||||
try:
|
||||
if timing_signal in _ACTION_SIGNALS:
|
||||
conn.execute(
|
||||
"UPDATE holding_strategies SET tag='current_recommend' "
|
||||
"WHERE code=? AND status='active' AND (tag IS NULL OR tag != 'active_manual')",
|
||||
(code,))
|
||||
conn.commit()
|
||||
elif timing_signal:
|
||||
conn.execute(
|
||||
"UPDATE holding_strategies SET tag='' "
|
||||
"WHERE code=? AND status='active' AND tag='current_recommend'",
|
||||
(code,))
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
print(f" [TAG SYNC] {code} 失败: {e}", flush=True)
|
||||
|
||||
|
||||
def snapshot_strategy_history(conn, code: str, source_trigger: str = "write_holding_strategy"):
|
||||
"""在修改前快照当前策略到 strategy_history 表。永不抛异常。"""
|
||||
try:
|
||||
@@ -1162,12 +1184,17 @@ def write_holding_strategy(conn, code: str, name: str, data: dict,
|
||||
quality_issues_j = _json.dumps(data.get('quality_issues', {}), ensure_ascii=False) if isinstance(data.get('quality_issues'), dict) else data.get('quality_issues_json', '')
|
||||
signal_factors_j = _json.dumps(data.get('signal_factors', []), ensure_ascii=False) if isinstance(data.get('signal_factors'), list) else data.get('signal_factors_json', '')
|
||||
|
||||
# 在DELETE前保留现有的full_analysis和reassessed_at(防止被regenerate_all等清空)
|
||||
# ── 推荐操作 tag 同步语义(与 XMPP 动作级信号同源,红线#12)──
|
||||
# 动作级信号 → tag=current_recommend(进盯盘"推荐操作"区)
|
||||
# 信号降级 → 清除 current_recommend(区域同步消失)
|
||||
# active_manual(人工标记)永远不被自动流程覆盖或清除
|
||||
_ACTION_SIGNALS = ("买入", "可买入", "可加仓", "卖出", "止盈")
|
||||
_existing_fa = data.get('full_analysis', '')
|
||||
_existing_ra = data.get('reassessed_at', '')
|
||||
# tag 语义:'tag' 键缺席=保留旧标签;显式传入(含'')= 按传入值(允许清除标签)
|
||||
_tag_absent = 'tag' not in data
|
||||
_existing_tag = data.get('tag', '') or ''
|
||||
_new_sig = data.get('timing_signal', '') or ''
|
||||
_explicit_tag = data.get('tag', None)
|
||||
_old_tag = ''
|
||||
if not _existing_fa or _tag_absent:
|
||||
try:
|
||||
_old = conn.execute("SELECT full_analysis, reassessed_at, tag FROM holding_strategies WHERE code=? ORDER BY id DESC LIMIT 1", (code,)).fetchone()
|
||||
@@ -1175,10 +1202,19 @@ def write_holding_strategy(conn, code: str, name: str, data: dict,
|
||||
if not _existing_fa:
|
||||
if _old[0]: _existing_fa = _old[0]
|
||||
if _old[1]: _existing_ra = _old[1]
|
||||
if _tag_absent and _old[2]:
|
||||
_existing_tag = _old[2]
|
||||
_old_tag = _old[2] or ''
|
||||
except:
|
||||
pass
|
||||
if _old_tag == 'active_manual':
|
||||
_existing_tag = 'active_manual' # 人工标记不可动
|
||||
elif _explicit_tag is not None:
|
||||
_existing_tag = _explicit_tag # 显式传入优先(含''清除)
|
||||
elif _new_sig in _ACTION_SIGNALS:
|
||||
_existing_tag = 'current_recommend' # 动作级信号 → 自动推荐
|
||||
elif _new_sig and _old_tag == 'current_recommend':
|
||||
_existing_tag = '' # 信号降级 → 清除自动推荐
|
||||
else:
|
||||
_existing_tag = _old_tag # 其余保留
|
||||
|
||||
# ── 类型守卫:shares 必须是数值,防止字符串写入导致下游崩溃 ──
|
||||
_shares = data.get('shares', 0)
|
||||
|
||||
+8
-5
@@ -1383,9 +1383,9 @@ async function renderWatch() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 分组渲染
|
||||
// 分组渲染(推荐操作区域独立于持仓/自选,置顶且与XMPP推荐同步)
|
||||
const groups = [
|
||||
{ label: '🔥 重点推荐', g: 0 },
|
||||
{ label: '🔥 推荐操作', g: 0 },
|
||||
{ label: '💼 持仓策略', g: 1 },
|
||||
{ label: '⭐ 自选策略', g: 2 },
|
||||
];
|
||||
@@ -1408,9 +1408,12 @@ async function renderWatch() {
|
||||
const items = stocks.filter(s => s.sort_group === grp.g);
|
||||
if (items.length === 0) continue;
|
||||
|
||||
// 分组标题行
|
||||
html += '<tr class="border-b border-slate-700">' +
|
||||
'<td colspan="10" class="p-2 font-semibold text-slate-300">' + grp.label + ' (' + items.length + ')</td>' +
|
||||
// 分组标题行(推荐操作区域独立视觉)
|
||||
const hdrCls = grp.g === 0
|
||||
? 'border-y border-amber-500/50 bg-amber-900/25'
|
||||
: 'border-b border-slate-700';
|
||||
html += '<tr class="' + hdrCls + '">' +
|
||||
'<td colspan="10" class="p-2 font-semibold ' + (grp.g === 0 ? 'text-amber-300' : 'text-slate-300') + '">' + grp.label + ' (' + items.length + ')</td>' +
|
||||
'</tr>';
|
||||
|
||||
for (const s of items) {
|
||||
|
||||
Reference in New Issue
Block a user