feat(analysis): systematic daily 12-dim LLM analysis for holdings+watchlist
Gap (reported by user via zhiwei): premarket full review updated technical
params but full_analysis (12-dim LLM matrix) was empty for new holdings
and stale for old ones — batch_reassess existed but was never wired into
the daily pipeline and only covered watchlist.
System fix:
- premarket_full_review.py: new Step 1.5 runs batch_reassess --type
holding --today every trading day 08:10 (force-refresh today's analysis,
timeout 3600s, result in summary.json)
- batch_reassess.py:
- coverage: --type holding|watchlist|all (was watchlist-only)
- staleness: analysis >20h stale gets refreshed (was: skip if any
analysis exists = forever stale)
- --today flag: force re-analyze if not reassessed since 04:00 today
- cash/total read live from portfolio_summary (was hardcoded 321271/
952879 from weeks ago)
- HK stock prefix fix (5-digit codes -> hk, was sending sz00700)
- watchlist_12d_backfill.py: wrapper for hermes cron (no args support)
- cron job '批量补全九维分析-一次性' -> '自选12维分析补全-每日午间'
(daily 12:30 weekdays, covers 109 watchlist stocks missing analysis)
Verified: 300308 got 1920-char 12-dim analysis written to DB at 08:41,
signal=观望, stop/take-profit updated.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import json
|
||||
d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'))
|
||||
jobs = d if isinstance(d, list) else d.get('jobs', [])
|
||||
for j in jobs:
|
||||
if '批量补全' in j.get('name', '') or '九维' in j.get('name', ''):
|
||||
print(json.dumps({k: j.get(k) for k in ('name', 'script', 'no_agent', 'prompt', 'schedule', 'enabled')},
|
||||
ensure_ascii=False, indent=2))
|
||||
@@ -0,0 +1,25 @@
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db')
|
||||
print('=== portfolio_summary schema ===')
|
||||
for r in conn.execute("SELECT sql FROM sqlite_master WHERE name='portfolio_summary'"):
|
||||
print(r[0])
|
||||
print()
|
||||
print('=== latest row ===')
|
||||
cur = conn.execute('SELECT * FROM portfolio_summary ORDER BY id DESC LIMIT 1')
|
||||
cols = [d[0] for d in cur.description]
|
||||
row = cur.fetchone()
|
||||
for c, v in zip(cols, row):
|
||||
print(f' {c} = {v}')
|
||||
print()
|
||||
print('=== decision_type distribution ===')
|
||||
for r in conn.execute("SELECT decision_type, COUNT(*) FROM holding_strategies WHERE status='active' GROUP BY decision_type"):
|
||||
print(f' {r[0]}: {r[1]}')
|
||||
print()
|
||||
print('=== full_analysis staleness ===')
|
||||
for r in conn.execute("""
|
||||
SELECT decision_type,
|
||||
SUM(CASE WHEN full_analysis IS NULL OR LENGTH(full_analysis)<500 THEN 1 ELSE 0 END) as missing,
|
||||
SUM(CASE WHEN LENGTH(full_analysis)>=500 THEN 1 ELSE 0 END) as has_fa,
|
||||
COUNT(*) as total
|
||||
FROM holding_strategies WHERE status='active' GROUP BY decision_type"""):
|
||||
print(f' {r[0]}: missing={r[1]} has={r[2]} total={r[3]}')
|
||||
@@ -0,0 +1,9 @@
|
||||
import ast, sys
|
||||
for p in ['/home/hmo/MoFin/deploy/profile-scripts/batch_reassess.py',
|
||||
'/home/hmo/MoFin/deploy/profile-scripts/premarket_full_review.py']:
|
||||
try:
|
||||
ast.parse(open(p).read())
|
||||
print('OK:', p)
|
||||
except SyntaxError as e:
|
||||
print('SYNTAX ERROR:', p, e)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,27 @@
|
||||
import json, shutil
|
||||
from datetime import datetime
|
||||
|
||||
jf = '/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'
|
||||
shutil.copy(jf, jf + '.bak-20260720')
|
||||
|
||||
d = json.load(open(jf))
|
||||
jobs = d if isinstance(d, list) else d.get('jobs', [])
|
||||
for j in jobs:
|
||||
if j.get('name') == '批量补全九维分析-一次性':
|
||||
j['name'] = '自选12维分析补全-每日午间'
|
||||
j['script'] = 'watchlist_12d_backfill.py'
|
||||
j['schedule'] = {"kind": "cron", "expr": "30 12 * * 1-5", "display": "30 12 * * 1-5"}
|
||||
j['schedule_display'] = "30 12 * * 1-5"
|
||||
j['next_run_at'] = "2026-07-20T12:30:00+08:00"
|
||||
j['state'] = 'scheduled'
|
||||
print('updated job:', j['name'], '| script:', j['script'], '| schedule:', j['schedule_display'])
|
||||
break
|
||||
else:
|
||||
print('job not found!')
|
||||
|
||||
if isinstance(d, list):
|
||||
json.dump(jobs, open(jf, 'w'), ensure_ascii=False, indent=2)
|
||||
else:
|
||||
d['jobs'] = jobs
|
||||
json.dump(d, open(jf, 'w'), ensure_ascii=False, indent=2)
|
||||
print('saved')
|
||||
@@ -0,0 +1,7 @@
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db')
|
||||
r = conn.execute("SELECT code, LENGTH(full_analysis), reassessed_at, timing_signal, stop_loss, take_profit FROM holding_strategies WHERE code='300308'").fetchone()
|
||||
print(r)
|
||||
# also show first 300 chars of the analysis
|
||||
r2 = conn.execute("SELECT substr(full_analysis, 1, 400) FROM holding_strategies WHERE code='300308'").fetchone()
|
||||
print(r2[0] if r2 else 'none')
|
||||
Reference in New Issue
Block a user