팩터의 IC가 양수로 나왔다고 해서 다음 행동이 곧바로 가중치를 올리는 것이어서는 안 된다. 오히려 그 순간이 가장 위험할 수 있다.

이전 글에서는 signal-time snapshot을 고정한 뒤 Bucket Spread, Rank IC, 여러 horizon의 outcome을 보면서 “이 팩터에 실제로 순위 정보가 있는가”를 확인했다. 자동 리웨이팅 단계로 넘어가면 질문은 달라진다.

최근 20개 period에서 더 강하게 보였다는 이유만으로 왜 active model을 바로 덮어써야 하는가?

이 두 문제는 쉽게 섞인다.

후보 팩터가 실제 정보를 갖고 있어도 최근 window의 추정치가 우연히 과대평가될 수 있다. 반대로 regime이 정말 바뀌었는데 보호 장치가 너무 강해서 모델이 따라가지 못할 수도 있다. 자동 리웨이팅은 연구용 IC를 새 weight vector로 변환하는 순간 끝나는 작업이 아니다. 더 어려운 문제는 그 상태 변경을 언제 운영에 반영할지다.

이번에는 과거 운영 가중치나 Notion에 적어 두었던 임계값을 증거로 쓰지 않았다. 대신 ground truth를 알고 있는 synthetic regime-switching experiment를 만들었다. 팩터 관계가 실제로 바뀌는 시점을 미리 정의한 뒤 세 가지 activation policy를 비교했다.

  • 새 candidate weights를 매번 즉시 active로 적용
  • 매번 적용하되 각 팩터의 최대 weight change를 제한
  • candidate를 먼저 shadow로 운영하고 active보다 연속으로 우수할 때만 promotion, promotion 시에도 delta cap 적용

12개의 고정 random seed를 사용했다. 각 seed는 240개의 연속 period, period당 200 synthetic assets, 4 factors로 구성했다. regime은 period 80과 160에서 명시적으로 전환된다. candidate는 5 period마다 만들고, 추정에는 그 이전 20 period만 사용했다. 평가는 반드시 이후 5 period에서만 수행했다.

이 시간 경계는 일부러 엄격하게 잡았다. scikit-learn의 TimeSeriesSplit 문서도 시간 순서가 있는 데이터를 일반적인 cross-validation처럼 섞으면 미래 관측치로 학습한 뒤 과거를 평가하는 잘못된 분할이 생길 수 있다고 설명한다.

candidate weight가 이미 미래를 본다면 shadow, regime, drift budget을 아무리 잘 설계해도 의미가 없다.

빠르게 조정하는 것과 안정적으로 조정하는 것은 얼마나 다른가

candidate weight 추정은 일부러 단순하게 했다. trailing window에서 각 factor의 cross-sectional Rank IC를 계산하고, 음수는 0으로 자른 뒤 남은 양의 evidence를 정규화해 candidate weights로 만든다.

변하는 것은 activation policy뿐이다.

이번에 기록한 지표는 다음과 같다.

  • future block의 combined-score Rank IC
  • 누적 L1 weight turnover
  • 한 번의 최대 weight jump
  • promotion 횟수
  • promotion 직후 다음 evaluation block에서 새 profile이 이전 active profile보다 못한 비율
  • 실제 regime switch 이후 active profile의 최대 가중치가 새 regime의 가장 강한 factor로 이동하는 데 걸린 시간

마지막 지표를 adaptation lag라고 불렀다. 금융 업계의 표준 지표가 아니라, 이 실험에서 “모델이 regime 변화에 얼마나 늦게 반응하는가”를 수치화하기 위한 정의다.

12개 seed 평균은 다음과 같다.

PolicyCapShadow confirmationsMean OOS Rank ICL1 turnoverMax weight jumpFalse promotion rateAdaptation lag
Immediate——0.21997.3050.33339.0%11.67
Delta capped5%—0.20925.5910.05027.8%22.08
Shadow first5%10.20014.1970.05020.6%29.79
Shadow first5%20.18822.4090.05015.0%52.29
Shadow first5%30.18501.6590.05010.2%75.83
Delta capped10%—0.21726.8840.10036.2%14.58
Shadow first10%10.20775.0150.10027.6%22.71
Shadow first10%20.19373.4720.10013.7%37.08
Shadow first10%30.18692.4110.10011.2%47.71

이 표는 하나의 “가장 안전한 설정”을 알려주지 않는다.

Immediate는 mean OOS Rank IC가 가장 높고 regime 변화에도 가장 빨리 적응했다. 평균 adaptation lag는 11.67 period에 불과했다. 대신 누적 turnover가 가장 크고, 44개 checkpoint에서 거의 매번 가중치를 바꾸며 false promotion rate가 약 39%였다.

실제로 5%를 지키는 delta cap을 넣으면 mean OOS Rank IC는 0.2199에서 0.2092로 내려가고 적응 속도도 느려진다. 대신 L1 turnover는 7.305에서 5.591로, false promotion rate는 39.0%에서 27.8%로 줄었다.

Shadow를 더하면 변화가 더 뚜렷하다. 5% cap에 shadow 3회 연속 승리를 요구하면 false promotion rate는 약 10.2%, turnover는 1.659까지 내려간다. 매우 안정적으로 보이지만 adaptation lag는 75.83 period로 늘어난다.

“잘못된 promotion을 최소화한다”는 목표만 보면 마지막 설정이 매력적이다. 하지만 실제 regime이 바뀌었을 때 모델을 거의 움직이지 못할 정도로 보호했을 수도 있다.

이번 실험에서 가장 분명해진 것은 guardrail은 공짜 안전장치가 아니라 모델의 일부라는 점이다.

AWS SageMaker의 shadow tests도 비슷한 production pattern을 사용한다. 새로운 variant에 production request를 미러링하되 response는 caller에게 반환하지 않고, production variant와 비교한 뒤 promotion을 결정한다. 이 패턴은 퀀트 시스템에도 잘 맞지만 SageMaker 문서는 “몇 번 연속 이기면 올려야 하는지”나 “weight cap을 몇 %로 해야 하는지”를 정해주지 않는다.

그 임계값은 자신의 outcome, turnover cost, regime 변화에 얼마나 빨리 반응해야 하는지에서 찾아야 한다.

Regime은 단순한 라벨이 아니라 어떤 과거를 믿을지 결정한다

동적 가중치에서 흔한 착각 중 하나는 표본이 많아지면 추정이 무조건 더 안전해진다는 생각이다.

관계가 안정적이라면 observation이 늘어나는 것이 도움이 된다. 하지만 factor behavior가 regime에 따라 달라진다면 오래전의 다른 환경을 같은 trailing window에 모두 섞는 것이 현재 필요한 관계를 희석할 수 있다.

Shu와 Mulvey가 2025년 Journal of Portfolio Management에 발표한 Dynamic Factor Allocation Leveraging Regime-Switching Signals는 factor-specific regime inference를 이용한 dynamic allocation을 다룬다. 이번 실험은 그 모델을 재현하지 않았고 그 논문이 내 시스템을 검증해주는 것도 아니다. 다만 regime-aware factor allocation이 프로젝트 내부 아이디어가 아니라 실제 연구 주제라는 근거는 된다.

자동 리웨이팅 시스템에서는 market / regime을 나중에 붙이는 label보다 evaluation context로 보는 편이 낫다.

같은 momentum factor라도 evidence context는 이렇게 생각할 수 있다.

momentum evidence
    + market
    + regime
    + session / universe
    + data quality
        ↓
candidate evidence

모든 것을 작은 bucket으로 완전히 분리할 필요는 없다. hierarchical shrinkage처럼 일부 정보를 함께 쓰는 접근도 가능하다. 중요한 것은 “과거 전체에서 잘 됐다”를 바로 “지금 가중치를 높여야 한다”로 번역하지 않는 것이다.

미국 연방준비제도의 현재 model risk management guidance는 은행 모델 거버넌스에 관한 자료이지 트레이딩 전략 설명서가 아니다. 그래도 일반화 가능한 원칙이 있다. ongoing monitoring에서는 data relevance, exposure, activity, market conditions가 변했는지 보고, 모델이 예상대로 작동하지 않으면 adjustment나 redevelopment를 검토해야 한다.

이 관점은 퀀트 시스템에서도 유용하다. weight vector는 한 번 training한 뒤 영원히 고정되는 상수도 아니고, 매 window마다 가볍게 다시 계산하는 임시 변수도 아니다. version, context, 명시적인 change condition이 필요하다.

그래서 이 글 앞에 팩터 유효성 검증 글을 둔다. 먼저 factor evidence를 믿을 만한지 확인하고, 그다음 candidate weight를 어떻게 바꿀지 결정한다. 순서를 뒤집으면 모델은 노이즈에 매우 정교하게 반응하는 시스템이 될 수 있다.

5% 상한이 normalize 이후 약 9.1%까지 커졌다

이번 실험에서 가장 남길 가치가 있었던 것은 결과표보다 중간에 발견한 구현 실수였다.

첫 번째 delta cap 구현은 자연스러웠다.

target weight
    ↓
각 factor를 old ± 5%로 clip
    ↓
normalize해서 합계를 1로 맞춤

이미 ±5%로 잘랐으니 마지막 normalize는 합계를 고치는 것뿐이라고 생각하기 쉽다.

하지만 실제로 돌려보니 명목상 5% cap인데 observed max weight jump가 평균 약 **9.1%**까지 커졌다.

이유는 간단하다. clip은 normalize 이전 좌표만 제한한다. 그 뒤 모든 가중치를 다시 스케일하면 일부 좌표가 old에서 더 멀어질 수 있다.

즉 다음 코드는 안전해 보이지만,

clipped = np.clip(target, old - cap, old + cap)
weights = clipped / clipped.sum()

다음을 보장하지 않는다.

abs(weights[i] - old[i]) <= cap

이런 bug는 IC 소수점 한 자리를 잘못 계산하는 것보다 위험할 수 있다. 모니터링 화면에는 cap=0.05라고 표시되지만 실제 모델은 그보다 훨씬 크게 움직일 수 있기 때문이다.

최종 구현은 bounded simplex projection으로 바꿨다. 새 weight vector는 동시에 세 조건을 만족해야 한다: 비음수, 합계 1, 각 차원이 old ± cap 범위 안에 있을 것.

핵심 제약은 다음과 같다.

sum(w) = 1
0 <= w_i <= 1
old_i - cap <= w_i <= old_i + cap

작은 이분 탐색으로 projection offset을 찾는다.

lower = np.maximum(0.0, old - cap)
upper = np.minimum(1.0, old + cap)

lo = np.min(target - upper) - 1.0
hi = np.max(target - lower) + 1.0

for _ in range(80):
    lam = (lo + hi) / 2.0
    w = np.clip(target - lam, lower, upper)

    if w.sum() > 1.0:
        lo = lam
    else:
        hi = lam

수정 후 최종 실험에서 5% cap의 observed max jump는 0.05, 10% cap은 0.10에 맞았다. 남는 차이는 floating-point epsilon 수준이다.

이 실패 이후 drift budget은 설정 파일에만 존재하면 안 된다고 생각하게 됐다.

max_weight_delta: 0.05

라고 적었다면 runtime에서도 다음을 assert하는 편이 낫다.

max(abs(new_weight - old_weight)) <= max_weight_delta + epsilon

그렇지 않으면 constraint가 아니라 parameter일 뿐이다.

Shadow의 목적은 예측을 더 잘하는 것이 아니라 연구 결론과 운영 상태를 분리하는 것이다

이제 adaptive factor lifecycle을 네 단계로 나누는 편이 낫다고 본다.

observe
   ↓
candidate
   ↓
shadow
   ↓
active

Observe는 evidence만 수집하고 live scoring에는 영향을 주지 않는다.

Candidate는 연구 단계에서 비교할 가치가 있는 weight profile이 나온 상태지만 아직 production state는 아니다.

Shadow 단계에서 같은 future outcome을 기준으로 candidate와 active를 병렬 비교한다. 새 데이터를 사용했다는 이유만으로 deployment 자격이 생겨서는 안 된다.

Promotion이 실제 state change다.

이 네 층을 분리하면 다음 차이가 명확해진다.

factor IC 개선
≠ candidate가 반드시 더 좋음

candidate가 최근 한 번 승리
≠ promotion해야 함

promotion 허용
≠ 가중치를 무제한으로 움직여도 됨

이번 실험에서 false promotion은 좁게 정의했다. 새 profile을 promote한 뒤 다음 future evaluation block에서 이전 active profile보다 못하면 false promotion 한 번으로 계산했다.

유일한 정의는 아니지만 문제를 드러내기에는 충분하다. 최근 estimation window에서 강해 보인 candidate가 실제 전환 직후 바로 질 수 있다.

Shadow confirmation을 늘리면 이런 경우는 줄어들지만, confirmation이 많아질수록 실제 regime change에 대한 반응도 늦어진다.

그래서 production pipeline을 설계한다면 처음부터

3번 연속 승리 -> deploy

처럼 고정하지 않을 것이다.

먼저 다음을 저장하고,

candidate_version
active_version
market / regime
evidence_window
future_eval_window
weight_delta
turnover
shadow_outcome
promotion_decision

실제 settled outcome을 이용해 confirmation count와 drift budget이 무엇을 교환하는지 확인하는 편이 낫다.

예전 Notion 메모에는 2회, 3회 확인이나 5%, 8%, 12–15% drift threshold 같은 값이 있었다. 이번 실험은 그 숫자 중 어느 것도 옳다고 증명하지 않았다. 이 control들이 모델 행동을 실제로 바꾸고, tradeoff가 관측 가능하다는 것만 보여줬다.

자동 리웨이팅은 “제약이 있는 모델 변경 workflow”로 다루고 싶다

이번 결과를 시스템 설계로 압축하면 최소 루프는 복잡하지 않다.

1. 현재 active weights를 고정
2. 과거 window만으로 factor evidence 추정
3. market / regime context 연결
4. candidate weights 생성
5. hard constraint / drift check
6. candidate를 shadow로 이동
7. future outcome이 확정될 때까지 대기
8. active와 shadow 비교
9. promotion rule을 만족할 때만 새 active version 생성
10. 전체 decision trail 저장

중요한 것은 9번을 단순한 if ic > threshold로 만들지 않는 것이다.

최소한 다음은 알고 싶다.

  • candidate가 얼마나 오래 이기고 있는가
  • 그 승리가 모두 같은 regime에서 나온 것은 아닌가
  • promotion이 얼마나 큰 weight jump를 만들까
  • 최근 누적 turnover가 얼마나 높은가
  • candidate와 active의 차이가 한 factor 때문인가, 전체 profile 때문인가
  • 지금 promotion하지 않으면 실제 regime change 대응이 얼마나 느려지는가

“최근 Rank IC = 0.08”만으로는 어느 질문에도 답할 수 없다.

더 나아가 promotion decision 자체를 outcome tracking 대상으로 삼는 것이 좋다. 시스템이 “지금 가중치를 바꿔야 한다”고 판단할 때마다 나중에 다시 돌아와 묻는다. 그때 바꾸지 않았다면 어땠을까? 한 window 더 기다렸다면 어땠을까?

그 기록이 충분히 쌓여야 shadow confirmation을 1, 2, 3 중 무엇으로 할지, delta cap을 5%나 10% 중 무엇으로 할지 실제 evidence를 바탕으로 이야기할 수 있다.

여기까지 오면 단순한 parameter tuning과는 성격이 달라진다.

adaptive weighting system이 배워야 하는 것은 “최근 어떤 factor가 잘 작동하는가”만이 아니다. 자신의 model-change rule이 과거에 실제로 도움이 되었는가도 학습해야 한다.

이번 synthetic experiment는 그 지점까지만 보여준다. Immediate는 빠르게 반응하지만 churn이 크다. Shadow는 신중하지만 너무 느리면 regime을 놓칠 수 있다. delta cap은 한 번의 움직임 크기를 제한할 수 있지만 future outcome validation을 대신할 수 없다.

이 표에서 숫자 하나를 골라 production config에 그대로 넣을 생각은 없다.

더 유용한 다음 단계는 이 guardrail들을 실제 settled outcome에 대해 shadow로 한동안 돌리는 것이다. 시스템이 promotion하고 싶었던 candidate를 모두 기록한 뒤, 실제로 계속 우수했는지 확인한다. 실데이터가 충분히 쌓이고 나면 어느 정도의 false promotion을 감수할 수 있는지, 안정성을 위해 얼마만큼의 adaptation lag를 지불할 수 있는지 판단할 수 있다.