@@ -765,6 +765,44 @@ def run_rebalance(args):
765765 # 보유 종목 가격이 전부 확보된 경우에만 저장(가짜 NAV 방지), 멱등 upsert.
766766 if not dry_run :
767767 rebalancer .save_daily_nav_snapshot ()
768+ # 일일 디스코드 리포트: 상시 스케줄러의 장마감 리포트는 일일 CLI 운영
769+ # 에서는 돌지 않아 운영자가 받는 푸시가 0건이었다 — 사이클마다 바스켓
770+ # NAV 요약 카드를 보낸다. 실패해도 사이클에는 영향 없음(채널은 보조).
771+ try :
772+ # 시장가 기준으로 평가해야 한다 — current_prices 없이 호출하면 paper
773+ # 포지션이 avg_price로 평가돼 누적수익 −0.0%·MDD 0.0%가 60일 내내
774+ # 표시된다(자기검토 2라운드 HIGH). 스냅샷 단계가 채운 가격 캐시 재사용.
775+ _snap_cache = getattr (rebalancer , "_market_snapshot" , None ) or {}
776+ _prices = {s : v ["price" ] for s , v in _snap_cache .items ()}
777+ summary_data = rebalancer .portfolio_mgr .get_portfolio_summary (
778+ current_prices = _prices or None ,
779+ )
780+ # 일간 수익률: 직전 스냅샷 대비 (summary에는 누적치만 있다)
781+ daily_ret = 0.0
782+ try :
783+ from database .repositories import get_portfolio_snapshots
784+ snaps = get_portfolio_snapshots (
785+ days = 7 , account_key = live_strategy_name ,
786+ )
787+ if snaps is not None and len (snaps ) >= 2 :
788+ vals = snaps .sort_values ("date" )["total_value" ].astype (float )
789+ prev , last = float (vals .iloc [- 2 ]), float (vals .iloc [- 1 ])
790+ if prev > 0 :
791+ daily_ret = (last / prev - 1 ) * 100
792+ except Exception :
793+ pass
794+ notifier .send_daily_report ({
795+ "total_value" : summary_data .get ("total_value" , 0 ),
796+ "cash" : summary_data .get ("cash" , 0 ),
797+ "daily_return" : daily_ret ,
798+ "cumulative_return" : summary_data .get ("total_return" , 0 ),
799+ "mdd" : summary_data .get ("mdd" , 0 ),
800+ "position_count" : summary_data .get ("position_count" , 0 ),
801+ "total_trades" : (result .get ("executed" , 0 ) if executed else 0 ),
802+ "strategy_diagnosis" : f"바스켓 { name } · paper 트랙레코드 일일 사이클" ,
803+ })
804+ except Exception as e :
805+ logger .debug ("바스켓 '{}' 일일 리포트 발송 실패(무시): {}" , name , e )
768806
769807 except Exception as e :
770808 logger .error ("바스켓 '{}' 리밸런싱 실패: {}" , name , e )
0 commit comments