Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH }}
LONGBRIDGE_STRATEGY_CONFIG_PATH: ${{ vars.LONGBRIDGE_STRATEGY_CONFIG_PATH }}
LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON }}
LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD: ${{ vars.LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD }}
# Optional strategy overrides; leave unset to inherit the UsEquityStrategies profile defaults.
INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}
QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}
Expand Down Expand Up @@ -337,6 +338,12 @@ jobs:
remove_env_vars+=("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON")
fi

if [ -n "${LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD:-}" ]; then
env_pairs+=("LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD=${LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD}")
else
remove_env_vars+=("LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD")
fi

if [ -n "${LONGBRIDGE_DRY_RUN_ONLY:-}" ]; then
env_pairs+=("LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}")
else
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Telegram notifications include structured execution and heartbeat messages, with
| `LONGBRIDGE_DRY_RUN_ONLY` | No | Set to `true` to keep the selected deployment in dry-run mode. |
| `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | No | Set to `true` to log raw LongBridge position quantity and available quantity for troubleshooting. |
| `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` | No | Optional LongBridge-side strategy plugin mount JSON. The plugin artifact controls mode; platform config must not set `mode`. |
| `LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD` | No | Safe-haven/cash-sweep target values below this USD amount are kept as cash instead of buying BOXX/BIL. Default `1000`. |
| `INCOME_THRESHOLD_USD` | No | Optional strategy override for the `tqqq_growth_income` income-layer threshold. Leave unset to use the strategy package default. |
| `QQQI_INCOME_RATIO` | No | Optional strategy override for QQQI's share of the `tqqq_growth_income` income layer, 0–1. |
| `NOTIFY_LANG` | No | Notification language: `en` (English, default) or `zh` (Chinese) |
Expand Down Expand Up @@ -119,17 +120,17 @@ Recommended setup:
- Optional fallback only: `TELEGRAM_TOKEN`
- **GitHub Environment: `longbridge-paper`**
- Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `RUNTIME_TARGET_JSON`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME`
- Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`)
- Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`)
- Current live example: `STRATEGY_PROFILE=mega_cap_leader_rotation_top50_balanced`
- Recommended secret-name values: `longport-app-key-paper`, `longport-app-secret-paper`
- **GitHub Environment: `longbridge-sg`**
- Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `RUNTIME_TARGET_JSON`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME`
- Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`)
- Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`)
- Current live example: `STRATEGY_PROFILE=soxl_soxx_trend_income`
- Recommended secret-name values: `longport-app-key-sg`, `longport-app-secret-sg`
- **GitHub Environment: `longbridge-hk`**
- Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `RUNTIME_TARGET_JSON`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME`
- Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`)
- Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`)
- Current live example: `STRATEGY_PROFILE=tech_communication_pullback_enhancement`
- Recommended secret-name values: `longport-app-key-hk`, `longport-app-secret-hk`

Expand Down Expand Up @@ -230,6 +231,7 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换
| `LONGBRIDGE_DRY_RUN_ONLY` | 否 | 设为 `true` 时,该部署保持 dry-run。 |
| `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | 否 | 设为 `true` 时输出 LongBridge 原始持仓数量和可卖数量,便于排查。 |
| `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` | 否 | 可选的 LongBridge 侧策略插件挂载 JSON。插件 artifact 自带模式;平台配置不要设置 `mode`。 |
| `LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD` | 否 | `BOXX`/`BIL` 等避险现金替代标的目标金额低于该 USD 门槛时保留现金,不买入。默认 `1000`。 |
| `INCOME_THRESHOLD_USD` | 否 | 可选的 `tqqq_growth_income` 收入层启动阈值覆盖(策略 override)。不填时使用策略包默认值。 |
| `QQQI_INCOME_RATIO` | 否 | 可选的 QQQI 收入层占比覆盖,0–1(策略 override)。 |
| `NOTIFY_LANG` | 否 | 通知语言: `en`(英文,默认)或 `zh`(中文) |
Expand Down Expand Up @@ -279,17 +281,17 @@ Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `lo
- 仅保留为 fallback:`TELEGRAM_TOKEN`
- **GitHub Environment: `longbridge-paper`**
- Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`RUNTIME_TARGET_JSON`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME`
- 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`)
- 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`)
- 当前线上示例:`STRATEGY_PROFILE=mega_cap_leader_rotation_top50_balanced`
- 建议的 secret-name 值:`longport-app-key-paper`、`longport-app-secret-paper`
- **GitHub Environment: `longbridge-sg`**
- Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`RUNTIME_TARGET_JSON`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME`
- 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`)
- 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`)
- 当前线上示例:`STRATEGY_PROFILE=soxl_soxx_trend_income`
- 建议的 secret-name 值:`longport-app-key-sg`、`longport-app-secret-sg`
- **GitHub Environment: `longbridge-hk`**
- Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`RUNTIME_TARGET_JSON`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME`
- 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`)
- 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`)
- 当前线上示例:`STRATEGY_PROFILE=tech_communication_pullback_enhancement`
- 建议的 secret-name 值:`longport-app-key-hk`、`longport-app-secret-hk`

Expand Down
65 changes: 64 additions & 1 deletion application/execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,53 @@ class ExecutionCycleResult:
action_done: bool


DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0


def _noop_sleep(_seconds):
return None


def _safe_haven_cash_symbols(*, portfolio: dict, allocation: dict) -> tuple[str, ...]:
symbols: list[str] = []
for symbol in allocation.get("safe_haven_symbols", ()):
normalized = str(symbol or "").strip().upper()
if normalized:
symbols.append(normalized)
cash_sweep_symbol = str(portfolio.get("cash_sweep_symbol") or "").strip().upper()
if cash_sweep_symbol:
symbols.append(cash_sweep_symbol)
return tuple(dict.fromkeys(symbols))


def _apply_safe_haven_cash_substitution(
*,
plan,
portfolio,
allocation,
threshold_usd,
) -> tuple[dict, dict]:
threshold = max(0.0, float(threshold_usd or 0.0))
target_values = {
str(symbol).strip().upper(): float(value or 0.0)
for symbol, value in dict(allocation.get("targets") or {}).items()
}
if threshold <= 0.0:
return dict(plan or {}), {**dict(allocation or {}), "targets": target_values}

changed = False
for symbol in _safe_haven_cash_symbols(portfolio=portfolio, allocation=allocation):
target_value = float(target_values.get(symbol, 0.0) or 0.0)
if 0.0 < target_value < threshold:
target_values[symbol] = 0.0
changed = True
adjusted_allocation = {**dict(allocation or {}), "targets": target_values}
adjusted_plan = dict(plan or {})
if changed:
adjusted_plan["allocation"] = adjusted_allocation
return adjusted_plan, adjusted_allocation


def _normalize_cash_by_currency(raw_cash) -> dict[str, float]:
if not isinstance(raw_cash, Mapping):
return {}
Expand Down Expand Up @@ -249,6 +292,7 @@ def execute_rebalance_cycle(
post_sell_refresh_attempts=1,
post_sell_refresh_interval_sec=0.0,
sleeper=_noop_sleep,
safe_haven_cash_substitute_threshold_usd=DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD,
) -> ExecutionCycleResult:
logs: list[str] = []
skip_logs: list[str] = []
Expand All @@ -264,6 +308,12 @@ def execute_rebalance_cycle(
market_values = dict(portfolio["market_values"])
quantities = dict(portfolio["quantities"])
sellable_quantities = dict(portfolio["sellable_quantities"])
plan, allocation = _apply_safe_haven_cash_substitution(
plan=plan,
portfolio=portfolio,
allocation=allocation,
threshold_usd=safe_haven_cash_substitute_threshold_usd,
)
target_values = dict(allocation["targets"])
cash_sweep_symbol = str(portfolio.get("cash_sweep_symbol") or "").strip().upper()
available_cash = float(portfolio["liquid_cash"])
Expand Down Expand Up @@ -533,6 +583,12 @@ def record_dry_run(symbol, side, quantity, price, *, order_type):
best_refreshed_state = refreshed_state
break
plan, portfolio, execution, allocation = best_refreshed_state
plan, allocation = _apply_safe_haven_cash_substitution(
plan=plan,
portfolio=portfolio,
allocation=allocation,
threshold_usd=safe_haven_cash_substitute_threshold_usd,
)
threshold_value = float(execution["trade_threshold_value"])
limit_order_symbols = set(
allocation.get("risk_symbols", ()) + allocation.get("income_symbols", ())
Expand Down Expand Up @@ -692,7 +748,14 @@ def record_dry_run(symbol, side, quantity, price, *, order_type):
notify_issue=notify_issue,
)
if cash_sweep_price is not None and cash_sweep_price > 0.0 and investable_cash > cash_sweep_price * 2:
quantity = int(investable_cash // cash_sweep_price)
substitution_threshold = max(
0.0,
float(safe_haven_cash_substitute_threshold_usd or 0.0),
)
if substitution_threshold <= 0.0 or investable_cash >= substitution_threshold:
quantity = int(investable_cash // cash_sweep_price)
else:
quantity = 0
if quantity > 0:
quantity_text = format_quantity(quantity)
if dry_run_only:
Expand Down
1 change: 1 addition & 0 deletions application/rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ def fetch_replanned_state():
post_sell_refresh_attempts=config.post_sell_refresh_attempts,
post_sell_refresh_interval_sec=config.post_sell_refresh_interval_sec,
sleeper=config.sleeper or _noop_sleep,
safe_haven_cash_substitute_threshold_usd=config.safe_haven_cash_substitute_threshold_usd,
)
execution = execution_result.execution
logs = list(execution_result.logs)
Expand Down
4 changes: 4 additions & 0 deletions application/runtime_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class LongBridgeRuntimeComposer:
limit_buy_premium: float
order_poll_interval_sec: int
order_poll_max_attempts: int
safe_haven_cash_substitute_threshold_usd: float
dry_run_only: bool = False
broker_adapters: Any = None
strategy_adapters: Any = None
Expand Down Expand Up @@ -184,6 +185,7 @@ def build_rebalance_config(self, *, strategy_plugin_signals=()) -> LongBridgeReb
dry_run_only=self.dry_run_only,
post_sell_refresh_attempts=self.order_poll_max_attempts,
post_sell_refresh_interval_sec=self.order_poll_interval_sec,
safe_haven_cash_substitute_threshold_usd=self.safe_haven_cash_substitute_threshold_usd,
sleeper=self.sleeper,
extra_notification_lines=getattr(
self.strategy_adapters,
Expand Down Expand Up @@ -228,6 +230,7 @@ def build_runtime_composer(
limit_buy_premium: float,
order_poll_interval_sec: int,
order_poll_max_attempts: int,
safe_haven_cash_substitute_threshold_usd: float,
dry_run_only: bool,
dry_run_only_override: bool | None = None,
broker_adapters: Any,
Expand Down Expand Up @@ -269,6 +272,7 @@ def build_runtime_composer(
limit_buy_premium=float(limit_buy_premium),
order_poll_interval_sec=int(order_poll_interval_sec),
order_poll_max_attempts=int(order_poll_max_attempts),
safe_haven_cash_substitute_threshold_usd=float(safe_haven_cash_substitute_threshold_usd),
dry_run_only=bool(dry_run_only if dry_run_only_override is None else dry_run_only_override),
broker_adapters=broker_adapters,
strategy_adapters=strategy_adapters,
Expand Down
1 change: 1 addition & 0 deletions application/runtime_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class LongBridgeRebalanceConfig:
dry_run_only: bool = False
post_sell_refresh_attempts: int = 1
post_sell_refresh_interval_sec: float = 0.0
safe_haven_cash_substitute_threshold_usd: float = 1000.0
sleeper: Callable[[float], None] | None = None
extra_notification_lines: tuple[str, ...] = ()

Expand Down
12 changes: 12 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def get_project_id():

# Token refresh: days before expiry to trigger refresh
TOKEN_REFRESH_THRESHOLD_DAYS = 30
DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0

SEPARATOR = "━━━━━━━━━━━━━━━━━━"

Expand Down Expand Up @@ -149,6 +150,16 @@ def log_runtime_warning(message):
)


def _safe_haven_cash_substitute_threshold_usd() -> float:
return float(
getattr(
RUNTIME_SETTINGS,
"safe_haven_cash_substitute_threshold_usd",
DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD,
)
)


def build_composer(*, dry_run_only_override: bool | None = None):
return build_runtime_composer(
project_id=PROJECT_ID,
Expand All @@ -171,6 +182,7 @@ def build_composer(*, dry_run_only_override: bool | None = None):
limit_buy_premium=LIMIT_BUY_PREMIUM,
order_poll_interval_sec=ORDER_POLL_INTERVAL_SEC,
order_poll_max_attempts=ORDER_POLL_MAX_ATTEMPTS,
safe_haven_cash_substitute_threshold_usd=_safe_haven_cash_substitute_threshold_usd(),
dry_run_only=RUNTIME_SETTINGS.dry_run_only,
dry_run_only_override=dry_run_only_override,
broker_adapters=BROKER_ADAPTERS,
Expand Down
11 changes: 11 additions & 0 deletions runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

DEFAULT_ACCOUNT_REGION = "DEFAULT"
DEFAULT_LONGPORT_SECRET_NAME = "longport_token_hk"
DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0


@dataclass(frozen=True)
Expand All @@ -38,6 +39,7 @@ class PlatformRuntimeSettings:
tg_token: str | None
tg_chat_id: str | None
dry_run_only: bool
safe_haven_cash_substitute_threshold_usd: float = DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD
debug_position_snapshot: bool = False
income_threshold_usd: float | None = None
qqqi_income_ratio: float | None = None
Expand Down Expand Up @@ -77,6 +79,10 @@ def load_platform_runtime_settings(
project_id_resolver: Callable[[], str | None],
) -> PlatformRuntimeSettings:
account_prefix = os.getenv("ACCOUNT_PREFIX", "DEFAULT")
safe_haven_cash_substitute_threshold_usd = resolve_optional_float_env(
os.environ,
"LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD",
)
runtime_target = resolve_runtime_target_from_env(
env=os.environ,
expected_platform_id=LONGBRIDGE_PLATFORM,
Expand Down Expand Up @@ -112,6 +118,11 @@ def load_platform_runtime_settings(
tg_token=os.getenv("TELEGRAM_TOKEN"),
tg_chat_id=os.getenv("GLOBAL_TELEGRAM_CHAT_ID"),
dry_run_only=resolve_bool_value(os.getenv("LONGBRIDGE_DRY_RUN_ONLY")),
safe_haven_cash_substitute_threshold_usd=(
max(0.0, safe_haven_cash_substitute_threshold_usd)
if safe_haven_cash_substitute_threshold_usd is not None
else DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD
),
debug_position_snapshot=resolve_bool_value(os.getenv("LONGBRIDGE_DEBUG_POSITION_SNAPSHOT")),
income_threshold_usd=resolve_optional_float_env(os.environ, "INCOME_THRESHOLD_USD"),
qqqi_income_ratio=_qqqi_income_ratio_env(),
Expand Down
Loading