fix(journal): 장중 일지 자동 생성 + 전략명 unknown + profit_factor 단위 오류
- 장중 10분 주기 journal_intraday_scheduler 신설 (SELECT 전용, 쓰기는 db_worker 큐) - 매도 후 3분 유예(JOURNAL_INTRADAY_GRACE_MIN), trade_id in-flight TTL 30분 - order_id 로 해소된 trade_id 에 일지가 있으면 INSERT skip (중복 방지) - 16:00 배치 enqueue_trade_journal 이 strategy_type 을 넘기지 않아 전 일지가 'unknown' 이던 결함 수정 - analyze_performance profit_factor 를 손익률(%) 합이 아닌 금액 기준으로 (1.22 → 0.62) - 일지 대상 SELL 상태에 PARTIAL 포함 (성과 집계 필터는 불변) - journals 단건 생성: 중복 행 시 500 대신 409, JOURNAL_SELL_STATUSES 사용 - 16:00 배치 조회일수 config 화, 손익 0 기간 PF 오경고 제거 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -247,6 +247,85 @@ async def batch_analysis_scheduler():
|
||||
raise
|
||||
|
||||
|
||||
# ── 매매일지 큐 등록 (16:00 배치·장중 주기 공용) ─────────────────────────────
|
||||
# db_worker 큐는 비동기라 enqueue → INSERT 커밋 사이에 같은 uncovered 조회가 다시 돌면
|
||||
# 같은 trade_id 를 또 큐에 넣는다. 프로세스 내 in-flight 집합으로 막는다
|
||||
# (재시작 시에는 DB 의 trade_id 커버 판정이 단일 진실). 무한 증가 방지로 상한을 둔다.
|
||||
_JOURNAL_ENQUEUED_IDS: dict[int, float] = {} # trade_id -> 등록 monotonic 시각(TTL 만료)
|
||||
_JOURNAL_ENQUEUED_MAX = 5000
|
||||
|
||||
|
||||
async def _enqueue_journals_v2(trades, perf=None) -> int:
|
||||
"""uncovered SELL 거래를 일지로 만들어 db_worker 큐에 등록한다. 등록 건수 반환.
|
||||
|
||||
멱등: 이미 이번 프로세스에서 등록한 trade_id 는 건너뛴다. DB 쓰기는 큐 경유(§3.2).
|
||||
"""
|
||||
from app.services.trade_analyzer import generate_journal_for_trade
|
||||
from app.services.db_worker import enqueue_trade_journal
|
||||
|
||||
import time as _time
|
||||
|
||||
_now_m = _time.monotonic()
|
||||
_ttl = max(1, int(settings.JOURNAL_ENQUEUE_TTL_MIN)) * 60
|
||||
for _k in [k for k, ts in _JOURNAL_ENQUEUED_IDS.items() if _now_m - ts > _ttl]:
|
||||
del _JOURNAL_ENQUEUED_IDS[_k]
|
||||
if len(_JOURNAL_ENQUEUED_IDS) > _JOURNAL_ENQUEUED_MAX:
|
||||
_JOURNAL_ENQUEUED_IDS.clear()
|
||||
|
||||
count = 0
|
||||
for trade in trades:
|
||||
tid = getattr(trade, "id", None)
|
||||
if tid is not None and tid in _JOURNAL_ENQUEUED_IDS:
|
||||
continue
|
||||
try:
|
||||
content = generate_journal_for_trade(trade, perf)
|
||||
await enqueue_trade_journal(trade, content)
|
||||
if tid is not None:
|
||||
_JOURNAL_ENQUEUED_IDS[tid] = _now_m
|
||||
count += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"일지 생성 실패 ({getattr(trade, 'stock_code', '?')}): {e}")
|
||||
return count
|
||||
|
||||
|
||||
async def journal_intraday_scheduler():
|
||||
"""장중 N분 주기로 uncovered SELL 을 일지화한다 (16:00 배치의 장중 보충).
|
||||
|
||||
별도 배치 태스크라 실시간 계층(체결 콜백)에서 DB I/O 를 하지 않는다. 읽기만 하고
|
||||
쓰기는 db_worker 큐 경유. 멱등성은 trade_id 커버 판정 + in-flight 집합이 보장한다.
|
||||
"""
|
||||
await asyncio.sleep(90) # 부팅 안정화
|
||||
logger.info(
|
||||
"📝 Intraday journal scheduler started (every %s min)",
|
||||
settings.JOURNAL_INTRADAY_INTERVAL_MIN,
|
||||
)
|
||||
try:
|
||||
while True:
|
||||
interval = max(1, int(settings.JOURNAL_INTRADAY_INTERVAL_MIN)) * 60
|
||||
if settings.JOURNAL_INTRADAY_ENABLED and is_market_open():
|
||||
try:
|
||||
from app.services.trade_analyzer import get_uncovered_trades_v2
|
||||
|
||||
async with AsyncSessionLocal() as db: # SELECT 전용, commit 없음
|
||||
uncovered = await get_uncovered_trades_v2(
|
||||
db,
|
||||
days=settings.JOURNAL_LOOKBACK_DAYS,
|
||||
min_age_minutes=settings.JOURNAL_INTRADAY_GRACE_MIN,
|
||||
)
|
||||
n = await _enqueue_journals_v2(uncovered, None)
|
||||
if n:
|
||||
logger.info("📝 [IntradayJournal] 일지 %d건 큐 등록", n)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.warning("⚠️ [IntradayJournal] 실행 실패: %s", e, exc_info=True)
|
||||
await asyncio.sleep(interval)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("⏹️ Intraday journal scheduler stopped")
|
||||
raise
|
||||
|
||||
|
||||
|
||||
async def post_market_analyzer_scheduler():
|
||||
"""장 종료 후 16:00 KST 1회 실행 — 매매일지 자동 생성 + 전략 자동 최적화.
|
||||
|
||||
@@ -300,18 +379,9 @@ async def post_market_analyzer_scheduler():
|
||||
from sqlalchemy import select, func, and_
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
uncovered = await get_uncovered_trades(db, days=7)
|
||||
uncovered = await get_uncovered_trades(db, days=settings.JOURNAL_LOOKBACK_DAYS)
|
||||
perf = await analyze_performance(db, days=30)
|
||||
journal_count = 0
|
||||
for trade in uncovered:
|
||||
try:
|
||||
content = generate_journal_for_trade(trade, perf)
|
||||
# [FIX] CLAUDE.md 3.2: 스케줄러에서 직접 DB 쓰기 금지 → db_worker 큐 경유
|
||||
from app.services.db_worker import enqueue_trade_journal
|
||||
await enqueue_trade_journal(trade, content)
|
||||
journal_count += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"일지 생성 실패 ({trade.stock_code}): {e}")
|
||||
journal_count = await _enqueue_journals_v2(uncovered, perf)
|
||||
if journal_count > 0:
|
||||
logger.info(f"📝 [PostMarket] 일지 {journal_count}건 큐 등록 완료")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user