66configured.
77
88Static mode: fixed weights per leg (default: 30/70 BTC/trend).
9- Dynamic mode: regime-based adjustment — when BTC is below SMA200, reduce
10- trend leg by 50 % and re-allocate to BTC.
9+ Dynamic legacy mode: regime-based adjustment — when BTC is below SMA200,
10+ reduce trend leg by 50 % and re-allocate to BTC.
11+ Dynamic dual-leg mode: opt-in tiered regime adjustment that can cap both BTC
12+ and trend legs, leaving the residual in cash.
1113
1214Usage
1315-----
3638DEFAULT_BTC_WEIGHT = 0.30
3739DEFAULT_TREND_WEIGHT = 0.70
3840DYNAMIC_REGIME_OFF_CUT = 0.50
41+ DYNAMIC_REGIME_MODE_LEGACY = "legacy"
42+ DYNAMIC_REGIME_MODE_DUAL_LEG = "dual_leg"
3943
4044TREND_ONLY_KWARGS = frozenset ({
4145 "trend_pool_size" ,
@@ -58,6 +62,137 @@ def _clamp_ratio(value: float, *, default: float = 1.0) -> float:
5862 return min (1.0 , max (0.0 , numeric ))
5963
6064
65+ def _normalized_regime_mode (value : object ) -> str :
66+ mode = str (value or DYNAMIC_REGIME_MODE_LEGACY ).strip ().lower ().replace ("-" , "_" )
67+ if mode in {"dual" , "dual_leg" , "tiered" , "cash_cap" }:
68+ return DYNAMIC_REGIME_MODE_DUAL_LEG
69+ return DYNAMIC_REGIME_MODE_LEGACY
70+
71+
72+ def _first_finite (payload : dict [str , Any ] | None , * keys : str ) -> float | None :
73+ if not isinstance (payload , dict ):
74+ return None
75+ for key in keys :
76+ value = payload .get (key )
77+ try :
78+ numeric = float (value )
79+ except (TypeError , ValueError ):
80+ continue
81+ if numeric == numeric :
82+ return numeric
83+ return None
84+
85+
86+ def _btc_sma200_ratio (
87+ prices : dict [str , float ],
88+ benchmark_snapshot : dict [str , Any ] | None ,
89+ indicators_map : dict [str , Any ] | None ,
90+ ) -> float | None :
91+ gap = _first_finite (
92+ benchmark_snapshot ,
93+ "sma200_gap" ,
94+ "gap_vs_sma200" ,
95+ "price_vs_sma200" ,
96+ )
97+ if gap is not None :
98+ return 1.0 + gap
99+
100+ ratio = _first_finite (
101+ benchmark_snapshot ,
102+ "price_sma200_ratio" ,
103+ "sma200_ratio" ,
104+ "mayer_multiple" ,
105+ )
106+ if ratio is not None :
107+ return ratio
108+
109+ ma200 = _first_finite (benchmark_snapshot , "ma200" , "sma200" , "sma_200" )
110+ if ma200 is None and isinstance (indicators_map , dict ):
111+ btc_indicators = indicators_map .get ("BTCUSDT" )
112+ ma200 = _first_finite (btc_indicators , "ma200" , "sma200" , "sma_200" )
113+
114+ price = _first_finite (benchmark_snapshot , "close" , "price" )
115+ if price is None :
116+ price = _first_finite (prices , "BTCUSDT" , "BTC" )
117+
118+ if price is None or ma200 is None or ma200 <= 0.0 :
119+ return None
120+ return price / ma200
121+
122+
123+ def _resolve_dynamic_weights (
124+ prices : dict [str , float ],
125+ indicators_map : dict [str , Any ] | None ,
126+ benchmark_snapshot : dict [str , Any ] | None ,
127+ * ,
128+ btc_weight : float ,
129+ trend_weight : float ,
130+ dynamic_mode : bool ,
131+ dynamic_regime_mode : str ,
132+ dynamic_regime_off_cut : float ,
133+ dynamic_hard_sma200_ratio : float ,
134+ dynamic_hard_ma200_slope : float ,
135+ dynamic_soft_sma200_ratio : float ,
136+ dynamic_hard_btc_weight : float ,
137+ dynamic_hard_trend_weight : float ,
138+ dynamic_soft_btc_weight : float ,
139+ dynamic_soft_trend_weight : float ,
140+ ) -> tuple [float , float , dict [str , object ]]:
141+ ratio = _btc_sma200_ratio (prices , benchmark_snapshot , indicators_map )
142+ ma200_slope = _first_finite (benchmark_snapshot , "ma200_slope" , "sma200_slope" )
143+
144+ regime_off = False
145+ if isinstance (benchmark_snapshot , dict ):
146+ regime_on = benchmark_snapshot .get ("regime_on" )
147+ if regime_on is not None :
148+ regime_off = not bool (regime_on )
149+ elif ratio is not None :
150+ regime_off = ratio <= 1.0
151+ else :
152+ btc_snapshot = _extract_btc_snapshot (indicators_map or {})
153+ regime_off = not btc_snapshot .get ("regime_on" , True )
154+
155+ mode = _normalized_regime_mode (dynamic_regime_mode )
156+ regime_tier = "risk_on"
157+ regime_off_cut = 0.0
158+ effective_btc = btc_weight
159+ effective_trend = trend_weight
160+
161+ if dynamic_mode and mode == DYNAMIC_REGIME_MODE_DUAL_LEG :
162+ hard = regime_off and (
163+ (ratio is None and ma200_slope is None )
164+ or (ratio is not None and ratio < dynamic_hard_sma200_ratio )
165+ or (ma200_slope is not None and ma200_slope < dynamic_hard_ma200_slope )
166+ )
167+ soft = (
168+ regime_off
169+ or (ratio is not None and ratio < dynamic_soft_sma200_ratio )
170+ or (ma200_slope is not None and ma200_slope < 0.0 )
171+ )
172+ if hard :
173+ regime_tier = "hard"
174+ effective_btc = _clamp_ratio (dynamic_hard_btc_weight , default = btc_weight )
175+ effective_trend = _clamp_ratio (dynamic_hard_trend_weight , default = 0.0 )
176+ elif soft :
177+ regime_tier = "soft"
178+ effective_btc = _clamp_ratio (dynamic_soft_btc_weight , default = btc_weight )
179+ effective_trend = _clamp_ratio (dynamic_soft_trend_weight , default = trend_weight )
180+ elif dynamic_mode and regime_off :
181+ regime_tier = "legacy_regime_off"
182+ regime_off_cut = _clamp_ratio (dynamic_regime_off_cut , default = DYNAMIC_REGIME_OFF_CUT )
183+ effective_btc = btc_weight + trend_weight * regime_off_cut
184+ effective_trend = trend_weight * (1.0 - regime_off_cut )
185+
186+ return effective_btc , effective_trend , {
187+ "regime_off" : regime_off ,
188+ "regime_mode" : mode ,
189+ "regime_tier" : regime_tier ,
190+ "dynamic_regime_off_cut" : regime_off_cut ,
191+ "btc_sma200_ratio" : ratio ,
192+ "ma200_slope" : ma200_slope ,
193+ }
194+
195+
61196def _compute_btc_leg (
62197 total_equity : float ,
63198 btc_weight : float ,
@@ -208,7 +343,15 @@ def build_target_weights(
208343 btc_weight : float = DEFAULT_BTC_WEIGHT ,
209344 trend_weight : float = DEFAULT_TREND_WEIGHT ,
210345 dynamic_mode : bool = True ,
346+ dynamic_regime_mode : str = DYNAMIC_REGIME_MODE_LEGACY ,
211347 dynamic_regime_off_cut : float = DYNAMIC_REGIME_OFF_CUT ,
348+ dynamic_hard_sma200_ratio : float = 0.97 ,
349+ dynamic_hard_ma200_slope : float = - 0.015 ,
350+ dynamic_soft_sma200_ratio : float = 1.05 ,
351+ dynamic_hard_btc_weight : float = 0.30 ,
352+ dynamic_hard_trend_weight : float = 0.0 ,
353+ dynamic_soft_btc_weight : float = 0.45 ,
354+ dynamic_soft_trend_weight : float = 0.15 ,
212355 translator = None ,
213356 ** kwargs : Any ,
214357) -> tuple [dict [str , float ], dict [str , object ]]:
@@ -233,24 +376,23 @@ def build_target_weights(
233376
234377 state = state or {}
235378
236- # Dynamic regime adjustment
237- regime_off = False
238- if benchmark_snapshot :
239- regime_on = benchmark_snapshot .get ("regime_on" )
240- if regime_on is not None :
241- regime_off = not bool (regime_on )
242- else :
243- btc_snapshot = _extract_btc_snapshot (indicators_map or {})
244- regime_off = not btc_snapshot .get ("regime_on" , True )
245-
246- if dynamic_mode and regime_off :
247- regime_off_cut = _clamp_ratio (dynamic_regime_off_cut , default = DYNAMIC_REGIME_OFF_CUT )
248- effective_btc = btc_weight + trend_weight * regime_off_cut
249- effective_trend = trend_weight * (1.0 - regime_off_cut )
250- else :
251- regime_off_cut = 0.0
252- effective_btc = btc_weight
253- effective_trend = trend_weight
379+ effective_btc , effective_trend , regime_metadata = _resolve_dynamic_weights (
380+ prices ,
381+ indicators_map ,
382+ benchmark_snapshot ,
383+ btc_weight = btc_weight ,
384+ trend_weight = trend_weight ,
385+ dynamic_mode = dynamic_mode ,
386+ dynamic_regime_mode = dynamic_regime_mode ,
387+ dynamic_regime_off_cut = dynamic_regime_off_cut ,
388+ dynamic_hard_sma200_ratio = dynamic_hard_sma200_ratio ,
389+ dynamic_hard_ma200_slope = dynamic_hard_ma200_slope ,
390+ dynamic_soft_sma200_ratio = dynamic_soft_sma200_ratio ,
391+ dynamic_hard_btc_weight = dynamic_hard_btc_weight ,
392+ dynamic_hard_trend_weight = dynamic_hard_trend_weight ,
393+ dynamic_soft_btc_weight = dynamic_soft_btc_weight ,
394+ dynamic_soft_trend_weight = dynamic_soft_trend_weight ,
395+ )
254396
255397 # Compute legs
256398 btc_weights , btc_leg_metadata = _compute_btc_leg (
@@ -295,11 +437,16 @@ def build_target_weights(
295437 "trend_weight" : effective_trend ,
296438 "base_btc_weight" : btc_weight ,
297439 "base_trend_weight" : trend_weight ,
298- "dynamic_regime_off_cut" : regime_off_cut ,
440+ "dynamic_regime_off_cut" : regime_metadata ["dynamic_regime_off_cut" ],
441+ "dynamic_regime_mode" : regime_metadata ["regime_mode" ],
442+ "regime_tier" : regime_metadata ["regime_tier" ],
299443 },
300444 "btc_leg" : {"weights" : btc_weights , ** btc_leg_metadata },
301445 "trend_leg" : {"weights" : trend_weights , ** trend_metadata },
302- "regime_off" : regime_off ,
446+ "regime_off" : regime_metadata ["regime_off" ],
447+ "regime_tier" : regime_metadata ["regime_tier" ],
448+ "btc_sma200_ratio" : regime_metadata ["btc_sma200_ratio" ],
449+ "ma200_slope" : regime_metadata ["ma200_slope" ],
303450 "dynamic_mode" : dynamic_mode ,
304451 "gross_exposure" : sum (combined .values ()),
305452 "selected_count" : len (combined ),
0 commit comments