@@ -59,12 +59,30 @@ def _override_with_env(settings: dict) -> dict:
5959 settings ["kis_api" ]["app_key" ] = os .environ .get ("KIS_APP_KEY" , "" )
6060 settings ["kis_api" ]["app_secret" ] = os .environ .get ("KIS_APP_SECRET" , "" )
6161 settings ["kis_api" ]["account_no" ] = os .environ .get ("KIS_ACCOUNT_NO" , settings ["kis_api" ].get ("account_no" , "" ))
62- # 전략별 계좌 (다중 계좌): KIS_ACCOUNT_NO_SCORING, KIS_ACCOUNT_NO_MEAN_REVERSION 등으로 덮어씀
62+ # 전략별 계좌 (다중 계좌): KIS_ACCOUNT_NO_SCORING, KIS_ACCOUNT_NO_MEAN_REVERSION 등으로 덮어씀.
63+ # 키 파생 시 영숫자 외 문자(':', '-')는 '_'로 정규화한다 — 바스켓 승인 단위
64+ # ('basket_rebalance:<name>')처럼 콜론이 든 키도 env로 설정 가능해야 한다
65+ # (콜론은 Windows env 이름에 쓸 수 없어 기존 파생식으로는 영구 설정 불가였다).
66+ import re as _re
6367 accounts = settings ["kis_api" ].get ("accounts" , {}) or {}
68+ consumed_env_keys = set ()
6469 for key in list (accounts .keys ()):
65- env_key = f"KIS_ACCOUNT_NO_{ key .upper ().replace ('-' , '_' )} "
70+ env_key = "KIS_ACCOUNT_NO_" + _re .sub (r"[^A-Z0-9]" , "_" , key .upper ())
71+ consumed_env_keys .add (env_key )
6672 accounts [key ] = os .environ .get (env_key , accounts [key ])
6773 settings ["kis_api" ]["accounts" ] = accounts
74+ # YAML에 선언되지 않은 KIS_ACCOUNT_NO_* env는 조용히 무시되면 운영자가
75+ # "덮어썼다"고 믿은 채 기본 계좌로 라우팅된다(침묵 공유) — 명시 경고로 드러낸다.
76+ for env_name in os .environ :
77+ if (
78+ env_name .startswith ("KIS_ACCOUNT_NO_" )
79+ and env_name not in consumed_env_keys
80+ ):
81+ logging .getLogger ("config_loader" ).warning (
82+ "%s 환경변수가 설정돼 있지만 kis_api.accounts에 대응하는 키가 없어 "
83+ "무시됩니다 — settings.yaml의 accounts에 해당 전략 키를 선언하세요." ,
84+ env_name ,
85+ )
6886 if "MAX_CALLS_PER_SEC" in os .environ :
6987 settings ["kis_api" ]["max_calls_per_sec" ] = float (os .environ ["MAX_CALLS_PER_SEC" ])
7088 if "MAX_CALLS_PER_MIN" in os .environ :
@@ -499,15 +517,31 @@ def resolved_hash(self) -> str:
499517 """환경변수 반영 후 실행 설정 해시."""
500518 return self ._resolved_hash
501519
520+ # live에서 기본 계좌 폴백 경고를 전략당 1회만 내기 위한 기록 (프로세스 전역)
521+ _default_account_warned : set = set ()
522+
502523 def get_account_no (self , strategy : str = "" ) -> str :
503524 """
504525 전략에 해당하는 계좌번호 반환 (다중 계좌 분리).
505526 kis_api.accounts에 전략명이 있으면 해당 계좌, 없으면 kis_api.account_no(기본) 사용.
527+
528+ live 모드에서 전략 키가 미선언/빈 값이라 기본 계좌로 폴백하면 경고를 남긴다 —
529+ 침묵 폴백은 여러 전략·바스켓이 모르게 같은 실계좌(자본 풀)를 공유하게 만들고,
530+ DB상 account_key는 서로 달라 보여 공유 사실이 가려진다.
506531 """
507532 kis = self .kis_api
508533 accounts = kis .get ("accounts" , {}) or {}
509- if strategy and strategy in accounts :
510- return accounts [strategy ] or kis .get ("account_no" , "" )
534+ if strategy and strategy in accounts and accounts [strategy ]:
535+ return accounts [strategy ]
536+ if strategy and str (self .trading .get ("mode" , "paper" )).lower () == "live" :
537+ if strategy not in Config ._default_account_warned :
538+ Config ._default_account_warned .add (strategy )
539+ logging .getLogger ("config_loader" ).warning (
540+ "live 계좌 라우팅: 전략 '%s'의 계좌가 kis_api.accounts에 %s — "
541+ "기본 계좌로 폴백합니다(다른 전략과 자본 풀 공유 가능)." ,
542+ strategy ,
543+ "선언되지 않음" if strategy not in accounts else "빈 값" ,
544+ )
511545 return kis .get ("account_no" , "" )
512546
513547 def with_strategy_overrides (self , strategy_name : str , overrides : dict ) -> "ConfigOverlay" :
0 commit comments