feat: CIO 종합 에이전트 — 오늘의 결정안 (dry-run)
/decisions/generate + /decisions: 점수+보팅+핫검증+시장레짐 종합해 매수 결정안 (확신도1~5·제안비중·논거·리스크노트) 생성→daily_decisions 저장. 약세장 사이즈 축소, 분식의심/이익품질/핫검증 통과를 리스크노트로. 평일 09:20 텔레그램 보고(자동실행 OFF). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3052,6 +3052,10 @@ async def startup():
|
||||
scheduler.add_job(hot_validate_report_job, "cron",
|
||||
day_of_week="mon-fri", hour=9, minute=35,
|
||||
id="hot_validate", replace_existing=True)
|
||||
# CIO 오늘의 결정안: 평일 09:20 생성+보고 (dry-run, 자동실행 OFF)
|
||||
scheduler.add_job(cio_decisions_job, "cron",
|
||||
day_of_week="mon-fri", hour=9, minute=20,
|
||||
id="cio_decisions", replace_existing=True)
|
||||
# 성과 추적: 매일 18시 가격 업데이트
|
||||
scheduler.add_job(update_performance_prices, "cron",
|
||||
day_of_week="mon-fri", hour=18, minute=0,
|
||||
@@ -3340,6 +3344,106 @@ async def hot_validate_report_job():
|
||||
await send_telegram("\n".join(lines))
|
||||
logger.info("hot_validate.report.sent", passed=v["passed_count"])
|
||||
|
||||
# ── 🧭 CIO 종합 에이전트 (오늘의 결정안, dry-run) ──────────────────
|
||||
def _conviction(score: float, votes: int) -> int:
|
||||
if score >= 70 and votes >= 3: return 5
|
||||
if score >= 55 and votes >= 2: return 4
|
||||
if score >= 45: return 3
|
||||
if score >= 35: return 2
|
||||
return 1
|
||||
|
||||
async def run_cio_decisions(top: int = 8) -> dict:
|
||||
"""점수+보팅+리스크+핫검증 종합 → 오늘의 매수 결정안 생성·저장 (dry-run, 승인 전)."""
|
||||
today = date.today()
|
||||
async with pg_pool.acquire() as conn:
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS daily_decisions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
decision_date DATE NOT NULL,
|
||||
stock_code VARCHAR(10) NOT NULL,
|
||||
stock_name VARCHAR(100) DEFAULT '',
|
||||
action VARCHAR(20) NOT NULL,
|
||||
conviction INTEGER DEFAULT 0,
|
||||
size_pct FLOAT DEFAULT 0,
|
||||
total_score FLOAT,
|
||||
thesis TEXT DEFAULT '',
|
||||
risk_notes TEXT DEFAULT '',
|
||||
status VARCHAR(20) DEFAULT 'proposed',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(decision_date, stock_code)
|
||||
)
|
||||
""")
|
||||
regime_label, _ = await calc_market_regime(conn)
|
||||
hotv = await compute_hot_validate(30)
|
||||
hot_pass = {p["code"] for p in (hotv.get("passed") or [])} if not hotv.get("error") else set()
|
||||
cands = await conn.fetch("""
|
||||
SELECT s.stock_code, COALESCE(d.corp_name, s.stock_code) name,
|
||||
s.total_score, s.recommendation, s.buy_votes,
|
||||
s.position_size_pct, s.top_reasons,
|
||||
s.beneish_score, s.earnings_quality
|
||||
FROM stock_scores s JOIN dart_corps d ON d.stock_code = s.stock_code
|
||||
WHERE s.score_date = $1 AND d.is_active = true
|
||||
AND s.recommendation IN ('강력매수', '매수관심') AND s.buy_votes >= 1
|
||||
ORDER BY s.total_score DESC LIMIT $2
|
||||
""", today, top)
|
||||
saved = []
|
||||
for c in cands:
|
||||
conv = _conviction(c["total_score"] or 0, c["buy_votes"] or 0)
|
||||
if c["stock_code"] in hot_pass: conv = min(5, conv + 1)
|
||||
size = round(c["position_size_pct"] or (conv * 2.0), 1)
|
||||
if regime_label == "약세": size = round(size * 0.5, 1)
|
||||
risk = []
|
||||
if regime_label == "약세": risk.append("시장 약세(사이즈↓)")
|
||||
if (c["beneish_score"] or 0) >= 50: risk.append("분식의심")
|
||||
if c["earnings_quality"] is not None and c["earnings_quality"] < 0: risk.append("이익품질 낮음")
|
||||
if c["stock_code"] in hot_pass: risk.append("핫종목 검증통과")
|
||||
thesis = (c["top_reasons"] or "")[:300]
|
||||
await conn.execute("""
|
||||
INSERT INTO daily_decisions
|
||||
(decision_date, stock_code, stock_name, action, conviction, size_pct,
|
||||
total_score, thesis, risk_notes, status)
|
||||
VALUES ($1,$2,$3,'매수',$4,$5,$6,$7,$8,'proposed')
|
||||
ON CONFLICT (decision_date, stock_code) DO UPDATE SET
|
||||
conviction=$4, size_pct=$5, total_score=$6, thesis=$7, risk_notes=$8
|
||||
""", today, c["stock_code"], c["name"], conv, size,
|
||||
c["total_score"], thesis, " · ".join(risk))
|
||||
saved.append({"code": c["stock_code"], "name": c["name"], "action": "매수",
|
||||
"conviction": conv, "size_pct": size,
|
||||
"score": round(c["total_score"], 1) if c["total_score"] is not None else None,
|
||||
"risk": " · ".join(risk), "thesis": thesis[:120]})
|
||||
return {"decision_date": str(today), "regime": regime_label, "count": len(saved), "decisions": saved}
|
||||
|
||||
@app.post("/decisions/generate")
|
||||
async def decisions_generate(top: int = Query(default=8, ge=1, le=20)):
|
||||
return await run_cio_decisions(top)
|
||||
|
||||
@app.get("/decisions")
|
||||
async def decisions_get():
|
||||
async with pg_pool.acquire() as conn:
|
||||
rows = await conn.fetch("""
|
||||
SELECT stock_code, stock_name, action, conviction, size_pct, total_score,
|
||||
thesis, risk_notes, status
|
||||
FROM daily_decisions WHERE decision_date = CURRENT_DATE
|
||||
ORDER BY conviction DESC, total_score DESC
|
||||
""")
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
async def cio_decisions_job():
|
||||
"""평일 09:20 — CIO 오늘의 결정안 생성 + 텔레그램 보고 (dry-run, 자동실행 OFF)."""
|
||||
try:
|
||||
d = await run_cio_decisions(8)
|
||||
except Exception as e:
|
||||
logger.error("cio.err", error=str(e)); return
|
||||
if not d.get("decisions"): return
|
||||
lines = [f"🧭 <b>오늘의 결정안</b> ({d['decision_date']}, 시장:{d['regime']}) — dry-run"]
|
||||
for x in d["decisions"]:
|
||||
lines.append(f"{'★'*x['conviction']} <b>{x['name']}</b>({x['code']}) {x['action']} "
|
||||
f"비중{x['size_pct']}% (점수{x['score']})"
|
||||
+ (f"\n ⚠️{x['risk']}" if x['risk'] else ""))
|
||||
lines.append("\n(자동실행 OFF — 검증 단계. 정확도 양전환 시 실행 연결)")
|
||||
await send_telegram("\n".join(lines))
|
||||
logger.info("cio.report.sent", count=d["count"])
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
Reference in New Issue
Block a user