Skip to content

Commit ab1484f

Browse files
authored
Add IBKR cash reserve policy
1 parent d2e1e20 commit ab1484f

12 files changed

Lines changed: 177 additions & 9 deletions

.github/workflows/sync-cloud-run-env.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ jobs:
3131
IBKR_RECONCILIATION_OUTPUT_PATH: ${{ vars.IBKR_RECONCILIATION_OUTPUT_PATH }}
3232
IBKR_DRY_RUN_ONLY: ${{ vars.IBKR_DRY_RUN_ONLY }}
3333
IBKR_PAPER_LIQUIDATE_ONLY: ${{ vars.IBKR_PAPER_LIQUIDATE_ONLY }}
34+
IBKR_MIN_RESERVED_CASH_USD: ${{ vars.IBKR_MIN_RESERVED_CASH_USD }}
35+
IBKR_RESERVED_CASH_RATIO: ${{ vars.IBKR_RESERVED_CASH_RATIO }}
3436
IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD: ${{ vars.IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD }}
3537
# Strategy-owned defaults continue to come from UsEquityStrategies; this workflow only syncs platform/runtime inputs.
3638
EXECUTION_REPORT_GCS_URI: ${{ vars.EXECUTION_REPORT_GCS_URI }}
@@ -331,6 +333,18 @@ jobs:
331333
remove_env_vars+=("IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD")
332334
fi
333335
336+
if [ -n "${IBKR_MIN_RESERVED_CASH_USD:-}" ]; then
337+
env_pairs+=("IBKR_MIN_RESERVED_CASH_USD=${IBKR_MIN_RESERVED_CASH_USD}")
338+
else
339+
remove_env_vars+=("IBKR_MIN_RESERVED_CASH_USD")
340+
fi
341+
342+
if [ -n "${IBKR_RESERVED_CASH_RATIO:-}" ]; then
343+
env_pairs+=("IBKR_RESERVED_CASH_RATIO=${IBKR_RESERVED_CASH_RATIO}")
344+
else
345+
remove_env_vars+=("IBKR_RESERVED_CASH_RATIO")
346+
fi
347+
334348
if [ -n "${EXECUTION_REPORT_GCS_URI:-}" ]; then
335349
env_pairs+=("EXECUTION_REPORT_GCS_URI=${EXECUTION_REPORT_GCS_URI}")
336350
else

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ For IBKR, keep `paper` as a single account-group entry. If you later add live ac
104104
| `IBKR_FEATURE_SNAPSHOT_PATH` | Conditionally required | Required for snapshot-backed profiles such as `russell_1000_multi_factor_defensive`, `tech_communication_pullback_enhancement`, and `mega_cap_leader_rotation_top50_balanced`. Path to the latest feature snapshot file (`.csv`, `.json`, `.jsonl`, `.parquet`). |
105105
| `IBKR_STRATEGY_PLUGIN_MOUNTS_JSON` | No | Optional IBKR-side strategy plugin mount JSON. The plugin artifact controls mode; platform config must not set `mode`. |
106106
| `IBKR_MIN_ORDER_NOTIONAL_USD` | No | Minimum buy notional for limit buys; defaults to `50.0`. |
107+
| `IBKR_MIN_RESERVED_CASH_USD` | No | Platform-level minimum cash reserve in USD. Defaults to `0`; the effective reserve is the max of this floor and the effective cash reserve ratio. |
108+
| `IBKR_RESERVED_CASH_RATIO` | No | Platform-level minimum cash reserve ratio in `[0,1]`. When unset, IBKR keeps the strategy/runtime `execution_cash_reserve_ratio`; when set, it can raise but not lower that strategy ratio. |
107109
| `IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD` | No | Safe-haven target values below this USD amount are kept as cash instead of buying BOXX/BIL. Defaults to `1000.0`. |
108110
| `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` | Yes for Cloud Run | Secret Manager secret name for account-group config JSON. Recommended production source. |
109111
| `IB_ACCOUNT_GROUP_CONFIG_JSON` | No | Local/dev JSON fallback for account-group config. Not recommended for production Cloud Run. |
@@ -219,7 +221,7 @@ Recommended setup:
219221
- `STRATEGY_PROFILE` (set explicitly to one enabled profile, such as `soxl_soxx_trend_income`)
220222
- `ACCOUNT_GROUP` (recommended: `paper`)
221223
- `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME`
222-
- Optional: `IBKR_STRATEGY_PLUGIN_MOUNTS_JSON`, `IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`
224+
- Optional: `IBKR_STRATEGY_PLUGIN_MOUNTS_JSON`, `IBKR_MIN_RESERVED_CASH_USD`, `IBKR_RESERVED_CASH_RATIO`, `IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`
223225
- `GLOBAL_TELEGRAM_CHAT_ID`
224226
- `NOTIFY_LANG`
225227
- **Repository Secrets**
@@ -341,6 +343,8 @@ IBKR 账户
341343
| `IBKR_FEATURE_SNAPSHOT_PATH` | 条件必填 | `russell_1000_multi_factor_defensive``tech_communication_pullback_enhancement``mega_cap_leader_rotation_top50_balanced` 等快照策略需要。指向最新特征快照文件(`.csv``.json``.jsonl``.parquet`)。 |
342344
| `IBKR_STRATEGY_PLUGIN_MOUNTS_JSON` || 可选的 IBKR 侧策略插件挂载 JSON。插件 artifact 自带模式;平台配置不要设置 `mode`|
343345
| `IBKR_MIN_ORDER_NOTIONAL_USD` || 限价买入的最小名义金额;默认 `50.0`|
346+
| `IBKR_MIN_RESERVED_CASH_USD` || 平台级最低预留现金 USD。默认 `0`;实际预留取该下限和有效预留现金比例中的最大值。 |
347+
| `IBKR_RESERVED_CASH_RATIO` || 平台级最低预留现金比例,取值 `[0,1]`。不设置时沿用策略/运行配置里的 `execution_cash_reserve_ratio`;设置后只会抬高,不会降低策略比例。 |
344348
| `IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD` || `BOXX`/`BIL` 等避险标的目标金额低于该 USD 门槛时保留现金,不买入。默认 `1000.0`|
345349
| `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` | Cloud Run 建议必填 | 账号组配置 JSON 在 Secret Manager 里的密钥名。生产环境推荐使用。 |
346350
| `IB_ACCOUNT_GROUP_CONFIG_JSON` || 本地开发用的账号组配置 JSON fallback。不建议在生产 Cloud Run 直接使用。 |
@@ -422,7 +426,7 @@ IB_GATEWAY_IP_MODE=internal
422426
- `STRATEGY_PROFILE`(显式设置为任一已启用 profile,例如 `soxl_soxx_trend_income`
423427
- `ACCOUNT_GROUP`(建议设为 `paper`
424428
- `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME`
425-
- 可选:`IBKR_STRATEGY_PLUGIN_MOUNTS_JSON``IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`
429+
- 可选:`IBKR_STRATEGY_PLUGIN_MOUNTS_JSON``IBKR_MIN_RESERVED_CASH_USD``IBKR_RESERVED_CASH_RATIO``IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`
426430
- `GLOBAL_TELEGRAM_CHAT_ID`
427431
- `NOTIFY_LANG`
428432
- **仓库级 Secrets**

application/execution_service.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,7 @@ def execute_rebalance(
603603
rebalance_threshold_ratio,
604604
limit_buy_premium,
605605
sell_settle_delay_sec,
606+
cash_reserve_floor_usd=0.0,
606607
quantity_step=1.0,
607608
min_order_notional=50.0,
608609
safe_haven_cash_substitute_threshold_usd=DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD,
@@ -664,7 +665,10 @@ def execute_rebalance(
664665
execution_summary["no_op_reason"] = "no_equity"
665666
return _finalize_result([translator("no_equity")], execution_summary, return_summary=return_summary)
666667

667-
reserved = equity * cash_reserve_ratio
668+
reserved = max(
669+
float(equity) * float(cash_reserve_ratio or 0.0),
670+
max(0.0, float(cash_reserve_floor_usd or 0.0)),
671+
)
668672
investable = equity - reserved
669673
target_weights, substituted_safe_haven_symbols = _apply_safe_haven_cash_substitution_to_weights(
670674
target_weights,

application/runtime_broker_adapters.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class IBKRRuntimeBrokerAdapters:
3636
account_ids: tuple[str, ...]
3737
dry_run_only: bool
3838
cash_reserve_ratio: float
39+
cash_reserve_floor_usd: float
3940
rebalance_threshold_ratio: float
4041
limit_buy_premium: float
4142
quantity_step: float
@@ -157,6 +158,7 @@ def execute_rebalance(
157158
account_ids=self.account_ids,
158159
dry_run_only=self.dry_run_only,
159160
cash_reserve_ratio=self.cash_reserve_ratio,
161+
cash_reserve_floor_usd=self.cash_reserve_floor_usd,
160162
rebalance_threshold_ratio=self.rebalance_threshold_ratio,
161163
limit_buy_premium=self.limit_buy_premium,
162164
quantity_step=self.quantity_step,
@@ -243,6 +245,7 @@ def build_runtime_broker_adapters(
243245
account_ids: tuple[str, ...],
244246
dry_run_only: bool,
245247
cash_reserve_ratio: float,
248+
cash_reserve_floor_usd: float,
246249
rebalance_threshold_ratio: float,
247250
limit_buy_premium: float,
248251
quantity_step: float,
@@ -279,6 +282,7 @@ def build_runtime_broker_adapters(
279282
account_ids=tuple(account_ids),
280283
dry_run_only=bool(dry_run_only),
281284
cash_reserve_ratio=float(cash_reserve_ratio),
285+
cash_reserve_floor_usd=float(cash_reserve_floor_usd),
282286
rebalance_threshold_ratio=float(rebalance_threshold_ratio),
283287
limit_buy_premium=float(limit_buy_premium),
284288
quantity_step=float(quantity_step),

main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ def _env_flag(name: str) -> bool:
234234
NOTIFY_LANG = RUNTIME_SETTINGS.notify_lang
235235

236236
CASH_RESERVE_RATIO = STRATEGY_RUNTIME.cash_reserve_ratio
237+
CASH_RESERVE_FLOOR_USD = getattr(STRATEGY_RUNTIME, "cash_reserve_floor_usd", 0.0)
237238
REBALANCE_THRESHOLD_RATIO = 0.02
238239
LIMIT_BUY_PREMIUM = 1.005
239240
SELL_SETTLE_DELAY_SEC = 3
@@ -323,6 +324,7 @@ def build_broker_adapters():
323324
account_ids=tuple(ACCOUNT_IDS),
324325
dry_run_only=RUNTIME_SETTINGS.dry_run_only,
325326
cash_reserve_ratio=CASH_RESERVE_RATIO,
327+
cash_reserve_floor_usd=CASH_RESERVE_FLOOR_USD,
326328
rebalance_threshold_ratio=REBALANCE_THRESHOLD_RATIO,
327329
limit_buy_premium=LIMIT_BUY_PREMIUM,
328330
quantity_step=RUNTIME_SETTINGS.quantity_step,

runtime_config_support.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import math
45
import os
56
from dataclasses import dataclass
67
from pathlib import Path
@@ -24,6 +25,7 @@
2425
from us_equity_strategies import get_strategy_catalog
2526

2627
DEFAULT_ACCOUNT_GROUP = "default"
28+
DEFAULT_RESERVED_CASH_FLOOR_USD = 0.0
2729
DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0
2830

2931

@@ -60,6 +62,8 @@ class PlatformRuntimeSettings:
6062
dry_run_only: bool
6163
quantity_step: float = 1.0
6264
min_order_notional: float = 50.0
65+
reserved_cash_floor_usd: float = DEFAULT_RESERVED_CASH_FLOOR_USD
66+
reserved_cash_ratio: float | None = None
6367
safe_haven_cash_substitute_threshold_usd: float = DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD
6468
account_group: str = DEFAULT_ACCOUNT_GROUP
6569
service_name: str | None = None
@@ -153,6 +157,11 @@ def load_platform_runtime_settings(
153157
"IBKR_MIN_ORDER_NOTIONAL_USD",
154158
default=50.0,
155159
),
160+
reserved_cash_floor_usd=resolve_non_negative_float_env(
161+
"IBKR_MIN_RESERVED_CASH_USD",
162+
default=DEFAULT_RESERVED_CASH_FLOOR_USD,
163+
),
164+
reserved_cash_ratio=resolve_optional_ratio_env("IBKR_RESERVED_CASH_RATIO"),
156165
safe_haven_cash_substitute_threshold_usd=max(
157166
0.0,
158167
resolve_float_env(
@@ -182,6 +191,25 @@ def resolve_strategy_profile(raw_value: str | None) -> str:
182191
).profile
183192

184193

194+
def resolve_non_negative_float_env(name: str, *, default: float) -> float:
195+
value = resolve_float_env(os.environ, name, default=default)
196+
if not math.isfinite(value):
197+
raise ValueError(f"{name} must be finite, got {value}")
198+
if value < 0.0:
199+
raise ValueError(f"{name} must be non-negative, got {value}")
200+
return float(value)
201+
202+
203+
def resolve_optional_ratio_env(name: str) -> float | None:
204+
raw_value = os.getenv(name)
205+
if raw_value is None or str(raw_value).strip() == "":
206+
return None
207+
value = resolve_non_negative_float_env(name, default=0.0)
208+
if value > 1.0:
209+
raise ValueError(f"{name} must be in [0,1], got {value}")
210+
return value
211+
212+
185213
def resolve_account_group(raw_value: str | None) -> str:
186214
value = (raw_value or "").strip()
187215
if not value:

scripts/print_strategy_switch_env_plan.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ def build_switch_plan(profile: str) -> dict[str, object]:
6969
]
7070
optional_env = [
7171
"IBKR_DRY_RUN_ONLY",
72+
"IBKR_MIN_RESERVED_CASH_USD",
73+
"IBKR_RESERVED_CASH_RATIO",
7274
"IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD",
7375
]
7476
remove_if_present: list[str] = []

strategy_runtime.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class LoadedStrategyRuntime:
5959
merged_runtime_config: Mapping[str, Any] = field(default_factory=dict)
6060
status_icon: str = "🐤"
6161
cash_reserve_ratio: float = DEFAULT_CASH_RESERVE_RATIO
62+
cash_reserve_floor_usd: float = 0.0
6263
logger: Callable[[str], None] = print
6364

6465
@property
@@ -631,18 +632,26 @@ def load_strategy_runtime(
631632

632633
merged_runtime_config = dict(entrypoint.manifest.default_config)
633634
merged_runtime_config.update(runtime_config)
635+
strategy_cash_reserve_ratio = float(
636+
merged_runtime_config.get(
637+
"execution_cash_reserve_ratio",
638+
DEFAULT_CASH_RESERVE_RATIO,
639+
)
640+
)
641+
platform_cash_reserve_ratio = runtime_settings.reserved_cash_ratio
642+
if platform_cash_reserve_ratio is not None:
643+
strategy_cash_reserve_ratio = max(
644+
strategy_cash_reserve_ratio,
645+
float(platform_cash_reserve_ratio),
646+
)
634647
return LoadedStrategyRuntime(
635648
entrypoint=entrypoint,
636649
runtime_adapter=runtime_adapter,
637650
runtime_settings=runtime_settings,
638651
runtime_config=runtime_config,
639652
merged_runtime_config=merged_runtime_config,
640653
status_icon=runtime_adapter.status_icon,
641-
cash_reserve_ratio=float(
642-
merged_runtime_config.get(
643-
"execution_cash_reserve_ratio",
644-
DEFAULT_CASH_RESERVE_RATIO,
645-
)
646-
),
654+
cash_reserve_ratio=strategy_cash_reserve_ratio,
655+
cash_reserve_floor_usd=float(runtime_settings.reserved_cash_floor_usd or 0.0),
647656
logger=logger,
648657
)

tests/test_execution_service.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,41 @@ def fake_fetch_quote_snapshots(_ib, symbols):
130130
assert any(log.startswith("buy VOO") for log in trade_logs)
131131

132132

133+
def test_execute_rebalance_uses_reserved_cash_floor_when_higher(tmp_path):
134+
class FakeIB:
135+
def openTrades(self):
136+
return []
137+
138+
def fills(self):
139+
return []
140+
141+
def accountValues(self):
142+
return [SimpleNamespace(tag="AvailableFunds", currency="USD", value="1000")]
143+
144+
_trade_logs, summary = execute_rebalance(
145+
FakeIB(),
146+
{},
147+
{},
148+
{"equity": 1000.0, "buying_power": 1000.0},
149+
fetch_quote_snapshots=lambda _ib, _symbols: {},
150+
submit_order_intent=lambda _ib, _intent: None,
151+
order_intent_cls=OrderIntent,
152+
translator=translate,
153+
strategy_symbols=[],
154+
signal_metadata=_signal_metadata({}),
155+
dry_run_only=True,
156+
cash_reserve_ratio=0.03,
157+
cash_reserve_floor_usd=250.0,
158+
rebalance_threshold_ratio=0.02,
159+
limit_buy_premium=1.005,
160+
sell_settle_delay_sec=0,
161+
execution_lock_dir=tmp_path,
162+
return_summary=True,
163+
)
164+
165+
assert summary["cash_reserve_dollars"] == 250.0
166+
167+
133168
def test_execute_rebalance_projects_unbuyable_weight_target_to_zero(tmp_path, monkeypatch):
134169
class FakeIB:
135170
def openTrades(self):

tests/test_runtime_config_support.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
import pytest
77

88
from runtime_config_support import (
9+
DEFAULT_RESERVED_CASH_FLOOR_USD,
910
DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD,
1011
load_platform_runtime_settings,
1112
parse_account_group_configs,
13+
resolve_non_negative_float_env,
14+
resolve_optional_ratio_env,
1215
)
1316
from strategy_registry import (
1417
IBKR_PLATFORM,
@@ -122,6 +125,8 @@ def test_load_platform_runtime_settings_uses_minimal_group_config(monkeypatch):
122125
assert settings.dry_run_only is False
123126
assert settings.quantity_step == 1.0
124127
assert settings.min_order_notional == 50.0
128+
assert settings.reserved_cash_floor_usd == DEFAULT_RESERVED_CASH_FLOOR_USD
129+
assert settings.reserved_cash_ratio is None
125130
assert settings.safe_haven_cash_substitute_threshold_usd == DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD
126131
assert settings.account_group == "paper"
127132
assert settings.service_name is None
@@ -209,6 +214,48 @@ def test_load_platform_runtime_settings_uses_whole_share_quantity_step(monkeypat
209214
assert settings.safe_haven_cash_substitute_threshold_usd == 750.0
210215

211216

217+
def test_load_platform_runtime_settings_reads_reserved_cash_policy(monkeypatch):
218+
monkeypatch.setenv("RUNTIME_TARGET_JSON", runtime_target_json(SAMPLE_STRATEGY_PROFILE))
219+
monkeypatch.setenv("ACCOUNT_GROUP", "paper")
220+
monkeypatch.setenv("IB_ACCOUNT_GROUP_CONFIG_JSON", MINIMAL_GROUP_JSON)
221+
monkeypatch.setenv("IBKR_MIN_RESERVED_CASH_USD", "250")
222+
monkeypatch.setenv("IBKR_RESERVED_CASH_RATIO", "0.025")
223+
224+
settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1")
225+
226+
assert settings.reserved_cash_floor_usd == 250.0
227+
assert settings.reserved_cash_ratio == 0.025
228+
229+
230+
def test_load_platform_runtime_settings_rejects_invalid_reserved_cash_ratio(monkeypatch):
231+
monkeypatch.setenv("IBKR_RESERVED_CASH_RATIO", "1.25")
232+
233+
with pytest.raises(ValueError, match="IBKR_RESERVED_CASH_RATIO"):
234+
resolve_optional_ratio_env("IBKR_RESERVED_CASH_RATIO")
235+
236+
237+
@pytest.mark.parametrize("raw_value", ["nan", "inf", "-inf"])
238+
def test_load_platform_runtime_settings_rejects_non_finite_reserved_cash_floor(
239+
monkeypatch,
240+
raw_value,
241+
):
242+
monkeypatch.setenv("IBKR_MIN_RESERVED_CASH_USD", raw_value)
243+
244+
with pytest.raises(ValueError, match="IBKR_MIN_RESERVED_CASH_USD must be finite"):
245+
resolve_non_negative_float_env("IBKR_MIN_RESERVED_CASH_USD", default=0.0)
246+
247+
248+
@pytest.mark.parametrize("raw_value", ["nan", "inf", "-inf"])
249+
def test_load_platform_runtime_settings_rejects_non_finite_reserved_cash_ratio(
250+
monkeypatch,
251+
raw_value,
252+
):
253+
monkeypatch.setenv("IBKR_RESERVED_CASH_RATIO", raw_value)
254+
255+
with pytest.raises(ValueError, match="IBKR_RESERVED_CASH_RATIO must be finite"):
256+
resolve_optional_ratio_env("IBKR_RESERVED_CASH_RATIO")
257+
258+
212259
def test_load_platform_runtime_settings_reads_ibkr_strategy_plugin_mounts(monkeypatch):
213260
mount_config = '{"strategy_plugins":[{"strategy":"soxl_soxx_trend_income","plugin":"crisis_response_shadow","signal_path":"gs://bucket/latest_signal.json"}]}'
214261
monkeypatch.setenv("RUNTIME_TARGET_JSON", runtime_target_json("soxl_soxx_trend_income"))
@@ -431,6 +478,8 @@ def test_print_strategy_switch_env_plan_for_tqqq_growth_income():
431478
assert plan["requires_strategy_config_path"] is False
432479
assert json.loads(plan["set_env"]["RUNTIME_TARGET_JSON"])["strategy_profile"] == "tqqq_growth_income"
433480
assert "ACCOUNT_GROUP" in plan["keep_env"]
481+
assert "IBKR_MIN_RESERVED_CASH_USD" in plan["optional_env"]
482+
assert "IBKR_RESERVED_CASH_RATIO" in plan["optional_env"]
434483
assert "IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD" in plan["optional_env"]
435484
assert "IBKR_FEATURE_SNAPSHOT_PATH" in plan["remove_if_present"]
436485

0 commit comments

Comments
 (0)