11from __future__ import annotations
22
3+ import os
34from dataclasses import dataclass
45from pathlib import Path
56from typing import Mapping
@@ -39,10 +40,11 @@ def resolve_bool_value(raw_value: str | None) -> bool:
3940 return str (raw_value or "" ).strip ().lower () in {"1" , "true" , "yes" , "y" , "on" }
4041
4142
42- def resolve_optional_bool_env (
43+ def _resolve_optional_bool_env (
4344 env : Mapping [str , str | None ],
4445 name : str ,
4546) -> bool | None :
47+ """Internal helper: resolve bool from a Mapping (not os.environ)."""
4648 raw_value = env .get (name )
4749 if raw_value is None or str (raw_value ).strip () == "" :
4850 return None
@@ -65,7 +67,7 @@ def resolve_cash_only_execution_env(
6567 if legacy :
6668 candidates .append (legacy )
6769 for name in candidates :
68- value = resolve_optional_bool_env (env , name )
70+ value = _resolve_optional_bool_env (env , name )
6971 if value is not None :
7072 return value
7173 return default
@@ -124,6 +126,116 @@ def resolve_quantity_step_env(
124126 return float (fractional_step ) if fractional_enabled else 1.0
125127
126128
129+ def resolve_optional_bool_env (name : str , default : bool = False ) -> bool :
130+ """Read env var *name* and parse as bool. Returns *default* when unset/empty."""
131+ raw_value = os .getenv (name )
132+ if raw_value is None or str (raw_value ).strip () == "" :
133+ return default
134+ value = str (raw_value ).strip ().lower ()
135+ if value in {"1" , "true" , "yes" , "y" , "on" }:
136+ return True
137+ if value in {"0" , "false" , "no" , "n" , "off" }:
138+ return False
139+ raise ValueError (f"{ name } must be boolean, got { raw_value !r} " )
140+
141+
142+ def resolve_optional_ratio_env (name : str , default : float | None = None ) -> float | None :
143+ """Read env var *name* and parse as a float ratio in [0, 1]. Returns *default* when unset/empty."""
144+ raw_value = os .getenv (name )
145+ if raw_value is None or str (raw_value ).strip () == "" :
146+ return default
147+ value = float (raw_value )
148+ if not (0.0 <= value <= 1.0 ):
149+ raise ValueError (f"{ name } must be in [0,1], got { value } " )
150+ return value
151+
152+
153+ def resolve_optional_positive_float_env (name : str , default : float | None = None ) -> float | None :
154+ """Read env var *name* and parse as a positive float. Returns *default* when unset/empty."""
155+ raw_value = os .getenv (name )
156+ if raw_value is None or str (raw_value ).strip () == "" :
157+ return default
158+ value = float (raw_value )
159+ if value <= 0.0 :
160+ raise ValueError (f"{ name } must be positive, got { value } " )
161+ return value
162+
163+
164+ def resolve_optional_dca_mode_env (name : str , default : str | None = None ) -> str | None :
165+ """Read env var *name* and resolve to 'fixed', 'smart', or *default* when unset."""
166+ raw_value = os .getenv (name )
167+ if raw_value is None or str (raw_value ).strip () == "" :
168+ return default
169+ value = str (raw_value ).strip ().lower ()
170+ aliases = {
171+ "ordinary" : "fixed" ,
172+ "ordinary_dca" : "fixed" ,
173+ "fixed_dca" : "fixed" ,
174+ "smart_dca" : "smart" ,
175+ }
176+ mode = aliases .get (value , value )
177+ if mode not in {"fixed" , "smart" }:
178+ raise ValueError (f"{ name } must be fixed or smart, got { raw_value !r} " )
179+ return mode
180+
181+
182+ def resolve_optional_ibit_zscore_exit_mode_env (name : str , default : str | None = None ) -> str | None :
183+ """Read env var *name* and resolve to 'disabled', 'paper', 'live', or *default* when unset."""
184+ raw_value = os .getenv (name )
185+ if raw_value is None or str (raw_value ).strip () == "" :
186+ return default
187+ value = str (raw_value ).strip ().lower ()
188+ aliases = {
189+ "off" : "disabled" ,
190+ "none" : "disabled" ,
191+ "false" : "disabled" ,
192+ "disable" : "disabled" ,
193+ "enabled" : "live" ,
194+ "shadow" : "paper" ,
195+ "dry_run" : "paper" ,
196+ "dry-run" : "paper" ,
197+ }
198+ mode = aliases .get (value , value )
199+ if mode not in {"disabled" , "paper" , "live" }:
200+ raise ValueError (f"{ name } must be disabled, paper, or live, got { raw_value !r} " )
201+ return mode
202+
203+
204+ def resolve_optional_symbol_env (name : str , default : str | None = None ) -> str | None :
205+ """Read env var *name* and validate as a ticker symbol. Returns *default* when unset/empty."""
206+ raw_value = os .getenv (name )
207+ if raw_value is None or str (raw_value ).strip () == "" :
208+ return default
209+ value = str (raw_value ).strip ().upper ()
210+ if len (value ) > 16 or not value .replace ("." , "" ).replace ("-" , "" ).isalnum ():
211+ raise ValueError (f"{ name } must be a symbol, got { raw_value !r} " )
212+ return value
213+
214+
215+ def resolve_optional_int (name : str , default : int | None = None ) -> int | None :
216+ """Read env var *name* and parse as int. Returns *default* when unset/empty."""
217+ raw_value = os .getenv (name )
218+ if raw_value is None or str (raw_value ).strip () == "" :
219+ return default
220+ return int (raw_value )
221+
222+
223+ def resolve_split_env_list (name : str , separator : str = "," ) -> tuple [str , ...]:
224+ """Read env var *name*, split by *separator*, return deduplicated tuple."""
225+ raw_value = os .getenv (name )
226+ if raw_value is None :
227+ return ()
228+ items = []
229+ seen = set ()
230+ for value in str (raw_value ).replace (";" , "," ).replace ("\n " , "," ).split (separator ):
231+ item = value .strip ()
232+ if not item or item in seen :
233+ continue
234+ items .append (item )
235+ seen .add (item )
236+ return tuple (items )
237+
238+
127239def resolve_strategy_config_path (
128240 * ,
129241 explicit_path : str | None ,
0 commit comments