fix: 브리핑 중복발송 쓰로틀 + 람다 async 스케줄러 버그

- send_briefing에 30분 쓰로틀(_last_briefing_sent) — n8n 중복/재시도로 /briefing/send가
  분당 수십 회 호출돼 같은 브리핑이 3~4건+씩 발송되던 폭주를 원천 차단.
- 스케줄러 lambda:coro() → 명명 코루틴 함수(_daily_score_notify/_ta_intraday/_ta_close).
  APScheduler가 lambda 반환 코루틴을 await 안 해 16:30 일간리포트·TA 장마감분석이
  'coroutine never awaited'로 실행 안 되던 버그 수정.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
kyu
2026-06-03 16:41:26 +09:00
parent c5ef30b2be
commit cacdeb7bda
2 changed files with 23 additions and 4 deletions
+15 -2
View File
@@ -2878,9 +2878,17 @@ async def calculate_daily_scores(as_of: date | None = None, notify: bool = False
# ── 정기 브리핑 ───────────────────────────────────────────
_last_briefing_sent: datetime | None = None # 중복/폭주 차단 쓰로틀
async def send_briefing():
"""정기 시황 브리핑 — 종목당 1카드(매매가/포지션/재무/근거 통합)"""
"""정기 시황 브리핑 — 종목당 1카드(매매가/포지션/재무/근거 통합).
30 재호출은 무시 n8n 중복/재시도 폭주로 같은 브리핑이 여러 발송되는 방지."""
global _last_briefing_sent
now = datetime.now()
if _last_briefing_sent and (now - _last_briefing_sent).total_seconds() < 1800:
logger.info("briefing.throttled", since=(now - _last_briefing_sent).total_seconds())
return
_last_briefing_sent = now
ta_redis = aioredis.Redis(
host=REDIS_HOST, port=6379, password=REDIS_PASSWORD, db=5, decode_responses=True)
@@ -3020,6 +3028,11 @@ async def send_briefing():
app = FastAPI(title="종목 점수 엔진")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
async def _daily_score_notify():
"""16:30 정기 일간리포트 — 코루틴 함수로 등록해야 APScheduler가 await함
(lambda로 감싸면 coroutine never awaited 버그)."""
await calculate_daily_scores(notify=True)
@app.on_event("startup")
async def startup():
global pg_pool, redis_cl
@@ -3029,7 +3042,7 @@ async def startup():
redis_cl = aioredis.Redis(
host=REDIS_HOST, port=6379, password=REDIS_PASSWORD, db=3, decode_responses=True)
await init_db()
scheduler.add_job(lambda: calculate_daily_scores(notify=True), "cron",
scheduler.add_job(_daily_score_notify, "cron",
day_of_week="mon-fri", hour=16, minute=30,
id="daily_score", replace_existing=True)
# 텔레그램 정기 알림 하루 2회로 축소 (사용자 요청 2026-06-02):
+8 -2
View File
@@ -783,9 +783,9 @@ async def startup():
redis_cl = aioredis.Redis(
host=REDIS_HOST, port=6379, password=REDIS_PASSWORD, db=5, decode_responses=True)
await init_db()
scheduler.add_job(lambda: job_analyze(limit=500), "cron", day_of_week="mon-fri",
scheduler.add_job(_ta_intraday, "cron", day_of_week="mon-fri",
hour="9-16", minute="*/30", id="ta_30m", replace_existing=True)
scheduler.add_job(lambda: job_analyze(limit=0), "cron", day_of_week="mon-fri",
scheduler.add_job(_ta_close, "cron", day_of_week="mon-fri",
hour=16, minute=15, id="ta_close", replace_existing=True)
scheduler.start()
logger.info("ta-engine.started")
@@ -870,6 +870,12 @@ async def buy_candidates(limit: int = Query(default=20)):
result.append(d)
return result
async def _ta_intraday(): # 코루틴 함수로 등록 (lambda 감싸면 APScheduler가 await 못함)
await job_analyze(limit=500)
async def _ta_close():
await job_analyze(limit=0)
@app.post("/analyze/all")
async def analyze_all(limit: int = 0):
asyncio.create_task(job_analyze(limit=limit))