105105 "ibkr" : "IBKR_MIN_RESERVED_CASH_USD" ,
106106 "firstrade" : "FIRSTRADE_MIN_RESERVED_CASH_USD" ,
107107}
108+ PLATFORM_CASH_ONLY_EXECUTION_VARIABLES = {
109+ "schwab" : "SCHWAB_CASH_ONLY_EXECUTION" ,
110+ "longbridge" : "LONGBRIDGE_CASH_ONLY_EXECUTION" ,
111+ "ibkr" : "IBKR_CASH_ONLY_EXECUTION" ,
112+ "firstrade" : "FIRSTRADE_CASH_ONLY_EXECUTION" ,
113+ }
108114INCOME_LAYER_VARIABLES = (
109115 "INCOME_LAYER_ENABLED" ,
110116 "INCOME_LAYER_START_USD" ,
132138 for suffix in MARKET_SIGNAL_RUNTIME_SUFFIXES
133139)
134140CASH_ONLY_EXECUTION_VARIABLE = "CASH_ONLY_EXECUTION"
141+ CASH_ONLY_EXECUTION_MODES = frozenset ({"current" , "enabled" , "disabled" })
142+ CASH_ONLY_EXECUTION_CONTROL_FIELD = "cash_only_execution_mode"
135143LEGACY_INCOME_LAYER_VARIABLES = (
136144 "INCOME_THRESHOLD_USD" ,
137145 "QQQI_INCOME_RATIO" ,
@@ -398,6 +406,25 @@ def _normalize_symbol_text(value: str, *, field_name: str) -> str:
398406 return text
399407
400408
409+ def _cash_only_extra_variables (args : argparse .Namespace , platform : str ) -> dict [str , str ]:
410+ mode = str (getattr (args , "cash_only_execution_mode" , None ) or "current" ).strip ().lower ()
411+ if mode not in CASH_ONLY_EXECUTION_MODES :
412+ raise ValueError ("cash_only_execution_mode must be current, enabled, or disabled" )
413+ if mode == "current" :
414+ return {}
415+ variable = PLATFORM_CASH_ONLY_EXECUTION_VARIABLES .get (platform )
416+ if not variable :
417+ return {}
418+ return {variable : env_string (mode == "enabled" )}
419+
420+
421+ def _extract_cash_only_control_fields (extra_variables : dict [str , Any ]) -> dict [str , Any ]:
422+ controls : dict [str , Any ] = {}
423+ if CASH_ONLY_EXECUTION_CONTROL_FIELD in extra_variables :
424+ controls [CASH_ONLY_EXECUTION_CONTROL_FIELD ] = extra_variables .pop (CASH_ONLY_EXECUTION_CONTROL_FIELD )
425+ return controls
426+
427+
401428def _extract_dca_control_fields (extra_variables : dict [str , Any ]) -> dict [str , Any ]:
402429 controls : dict [str , Any ] = {}
403430 for field_name in (DCA_MODE_CONTROL_FIELD , DCA_BASE_INVESTMENT_CONTROL_FIELD ):
@@ -826,9 +853,10 @@ def _preserve_reserved_cash_fields(
826853 for variable in (
827854 PLATFORM_MIN_RESERVED_CASH_VARIABLES .get (platform ),
828855 PLATFORM_RESERVED_CASH_RATIO_VARIABLES .get (platform ),
856+ PLATFORM_CASH_ONLY_EXECUTION_VARIABLES .get (platform ),
857+ CASH_ONLY_EXECUTION_VARIABLE ,
829858 * INCOME_LAYER_VARIABLES ,
830859 * OPTION_OVERLAY_VARIABLES ,
831- CASH_ONLY_EXECUTION_VARIABLE ,
832860 * RUNTIME_TARGET_VARIABLES ,
833861 * DCA_RUNTIME_VARIABLES ,
834862 * IBIT_ZSCORE_EXIT_RUNTIME_VARIABLES ,
@@ -898,8 +926,13 @@ def build_switch_target(args: argparse.Namespace) -> dict[str, Any]:
898926 runtime_target ["scheduler" ] = _scheduler_plan_for_strategy (runtime_target ["strategy_profile" ], mounts )
899927 mounts_variable = f"{ SUPPORTED_PLATFORMS [platform ]['plugin_mounts_prefix' ]} STRATEGY_PLUGIN_MOUNTS_JSON"
900928 extra_variables = _parse_extra_variables (args .extra_variable , args .extra_variables_json )
929+ cash_only_controls = _extract_cash_only_control_fields (extra_variables )
901930 dca_controls = _extract_dca_control_fields (extra_variables )
902931 ibit_zscore_exit_controls = _extract_ibit_zscore_exit_control_fields (extra_variables )
932+ if cash_only_controls .get (CASH_ONLY_EXECUTION_CONTROL_FIELD ):
933+ args .cash_only_execution_mode = str (
934+ cash_only_controls [CASH_ONLY_EXECUTION_CONTROL_FIELD ]
935+ ).strip ().lower ()
903936 _reject_direct_dca_extra_variables (extra_variables )
904937 _reject_direct_ibit_zscore_exit_extra_variables (extra_variables )
905938 _reject_research_only_extra_variables (extra_variables )
@@ -914,6 +947,7 @@ def build_switch_target(args: argparse.Namespace) -> dict[str, Any]:
914947 extra_variables ["INCOME_LAYER_START_USD" ] = args .income_layer_start_usd
915948 if args .income_layer_max_ratio :
916949 extra_variables ["INCOME_LAYER_MAX_RATIO" ] = args .income_layer_max_ratio
950+ extra_variables .update (_cash_only_extra_variables (args , platform ))
917951 extra_variables .update (_option_overlay_extra_variables (args , runtime_target ["strategy_profile" ]))
918952 extra_variables .update (_dca_extra_variables (args , runtime_target ["strategy_profile" ], dca_controls ))
919953 extra_variables .update (
@@ -985,6 +1019,11 @@ def build_parser() -> argparse.ArgumentParser:
9851019 parser .add_argument ("--extra-variable" , action = "append" , default = [], help = "NAME=VALUE non-secret extra variable" )
9861020 parser .add_argument ("--reserved-cash-ratio" , default = "" )
9871021 parser .add_argument ("--min-reserved-cash-usd" , default = "" )
1022+ parser .add_argument (
1023+ "--cash-only-execution-mode" ,
1024+ choices = sorted (CASH_ONLY_EXECUTION_MODES ),
1025+ default = "current" ,
1026+ )
9881027 parser .add_argument ("--income-layer-start-usd" , default = "" )
9891028 parser .add_argument ("--income-layer-max-ratio" , default = "" )
9901029 parser .add_argument ("--option-overlay-mode" , choices = sorted (OPTION_OVERLAY_MODES ), default = "current" )
0 commit comments