diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index 55d809b..b33bacf 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -54,7 +54,6 @@ jobs: NOTIFY_LANG: ${{ vars.NOTIFY_LANG }} EXECUTION_REPORT_GCS_URI: ${{ vars.EXECUTION_REPORT_GCS_URI }} LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }} - LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET: ${{ vars.LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET }} GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }} TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} steps: diff --git a/README.md b/README.md index b95179e..a514ba2 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,6 @@ Telegram notifications include structured execution and heartbeat messages, with | `STRATEGY_PROFILE` | Yes | Strategy profile selector for compatibility and strategy routing. Set explicitly per deployment; enabled values include `global_etf_confidence_vol_gate`, `global_etf_rotation`, `mega_cap_leader_rotation_top50_balanced`, `russell_1000_multi_factor_defensive`, `soxl_soxx_trend_income`, `tech_communication_pullback_enhancement`, and `tqqq_growth_income`. The structured runtime target is carried separately as `RUNTIME_TARGET_JSON`. | | `ACCOUNT_REGION` | No | Account region marker for platform-style deployment (e.g. `PAPER`, `HK`, `SG`; defaults to `ACCOUNT_PREFIX` / `DEFAULT`) | | `LONGBRIDGE_DRY_RUN_ONLY` | No | Set to `true` to keep the selected deployment in dry-run mode. | -| `LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET` | No | Set to `true` to convert fractional `limit buy` orders to `market buy` orders instead of skipping them. | | `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`. | | `INCOME_THRESHOLD_USD` | No | Optional strategy override for the `tqqq_growth_income` income-layer threshold. Leave unset to use the strategy package default. | @@ -76,7 +75,7 @@ Telegram notifications include structured execution and heartbeat messages, with | `NOTIFY_LANG` | No | Notification language: `en` (English, default) or `zh` (Chinese) | | `GOOGLE_CLOUD_PROJECT` | No | GCP project ID (defaults to ADC project when unset) | -Strategy allocation can still target fractional dollar values and fractional position weights. The LongBridge execution layer keeps the tested conservative rule: `limit buy` orders stay whole-share only by default, while `market buy` / `market sell` and `limit sell` can preserve fractional quantities when the target quantity is at least 1 share. If you set `LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET=true`, fractional `limit buy` orders are downgraded to market buys instead of being skipped. When a target value is zero, sell sizing uses the sellable position quantity instead of re-deriving shares from current price, so liquidation targets do not leave a residual share because of quote drift. +Strategy allocation can still target fractional dollar values and fractional position weights. The LongBridge execution layer now keeps a whole-share-only rule for every broker order: sell sizing floors to whole shares, buy sizing floors to whole shares, and fractional orders are skipped rather than downgraded. When a target value is zero, sell sizing uses the sellable position quantity instead of re-deriving shares from current price, so liquidation targets do not leave a residual share because of quote drift. Secret Manager must contain the secret named by `LONGPORT_SECRET_NAME` (default: `longport_token_paper`), where the **latest version = active access token**. The app refreshes it when expiry is within 30 days. diff --git a/application/execution_service.py b/application/execution_service.py index 84ac13f..8d99a95 100644 --- a/application/execution_service.py +++ b/application/execution_service.py @@ -138,33 +138,19 @@ def _floor_whole_share_quantity(quantity): return normalize_order_quantity(floor_to_quantity_step(quantity, 1.0)) -def _supports_fractional_quantity(*, order_kind: str, side: str) -> bool: - return not (order_kind == "limit" and side == "buy") - - -def _normalize_trade_quantity(quantity, *, allow_fractional: bool): +def _normalize_trade_quantity(quantity): raw_quantity = max(0.0, float(quantity or 0.0)) if raw_quantity <= 0.0: return 0 - if allow_fractional: - if raw_quantity < 1.0: - return 0 - return normalize_order_quantity(raw_quantity) return _floor_whole_share_quantity(raw_quantity) -def _has_fractional_quantity(quantity) -> bool: - value = float(quantity or 0.0) - return value > 0.0 and not value.is_integer() - - def _sell_order_quantity( *, current_value, target_value, price, sellable_quantity, - allow_fractional: bool, ): sellable = max(0.0, float(sellable_quantity or 0.0)) if sellable <= 0.0: @@ -172,17 +158,13 @@ def _sell_order_quantity( target = max(0.0, float(target_value or 0.0)) if target <= 0.0: - return _normalize_trade_quantity( - sellable, - allow_fractional=allow_fractional, - ) + return _normalize_trade_quantity(sellable) sell_value = max(0.0, float(current_value or 0.0) - target) if sell_value <= 0.0 or float(price or 0.0) <= 0.0: return 0 return _normalize_trade_quantity( min(sell_value / float(price), sellable), - allow_fractional=allow_fractional, ) @@ -229,7 +211,7 @@ def _estimate_buy_quantity_candidate( notify_issue, dry_run_only=False, ): - budget_quantity = floor_to_quantity_step(can_buy_value / ref_price, 0.0001) + budget_quantity = floor_to_quantity_step(can_buy_value / ref_price, 1.0) cash_limit_quantity = estimate_cash_buy_quantity_safe( trade_context, symbol, @@ -240,11 +222,8 @@ def _estimate_buy_quantity_candidate( ) if cash_limit_quantity is None: return None - if dry_run_only and float(cash_limit_quantity or 0.0) <= 0.0: - cash_limit_quantity = budget_quantity candidate_quantity = _normalize_trade_quantity( min(budget_quantity, float(cash_limit_quantity)), - allow_fractional=True, ) return candidate_quantity, budget_quantity, float(cash_limit_quantity) @@ -267,7 +246,6 @@ def execute_rebalance_cycle( limit_sell_discount, limit_buy_premium, dry_run_only=False, - fractional_limit_buy_fallback_to_market=False, post_sell_refresh_attempts=1, post_sell_refresh_interval_sec=0.0, sleeper=_noop_sleep, @@ -305,15 +283,10 @@ def append_order_id_suffix(log_message, order_id): return f"{log_message} {suffix}" def submit_order_via_port(symbol, order_type, side, quantity, log_message, *, submitted_price=None): - allow_fractional = _supports_fractional_quantity(order_kind=order_type, side=side) order_intent = OrderIntent( symbol=symbol, side=side, - quantity=( - _normalize_trade_quantity(quantity, allow_fractional=allow_fractional) - if allow_fractional - else _floor_whole_share_quantity(quantity) - ), + quantity=_floor_whole_share_quantity(quantity), order_type=order_type, limit_price=float(submitted_price) if submitted_price is not None else None, ) @@ -394,10 +367,6 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): target_value=target_values[symbol], price=price, sellable_quantity=sellable_quantities[symbol], - allow_fractional=_supports_fractional_quantity( - order_kind="limit" if symbol in limit_order_symbols else "market", - side="sell", - ), ) if quantity > 0: quantity_text = format_quantity(quantity) @@ -629,45 +598,10 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): if limit_candidate is None: continue limit_candidate_quantity, limit_budget_quantity, limit_cash_limit_quantity = limit_candidate - if is_limit_order: - limit_quantity = _normalize_trade_quantity( - limit_candidate_quantity, - allow_fractional=False, - ) - else: - limit_quantity = limit_candidate_quantity + limit_quantity = _normalize_trade_quantity(limit_candidate_quantity) order_kind = limit_order_kind ref_price = limit_ref_price quantity = limit_quantity - if is_limit_order and fractional_limit_buy_fallback_to_market and _has_fractional_quantity( - limit_candidate_quantity - ): - market_ref_price = round(price, 2) - market_candidate = _estimate_buy_quantity_candidate( - trade_context, - f"{symbol}.US", - "market", - market_ref_price, - can_buy_value=can_buy_value, - estimate_max_purchase_quantity=estimate_max_purchase_quantity, - notify_issue=notify_issue, - dry_run_only=dry_run_only, - ) - if market_candidate is not None: - market_quantity, _market_budget_quantity, _market_cash_limit_quantity = market_candidate - if market_quantity >= 1.0: - order_kind = "market" - ref_price = market_ref_price - quantity = market_quantity - fallback_message = translator( - "fractional_limit_buy_fallback_to_market", - symbol=symbol, - qty=format_quantity(quantity), - limit_price=limit_ref_price, - market_price=market_ref_price, - ) - note_logs.append(fallback_message) - print(with_prefix(fallback_message), flush=True) cost_estimate = 0.0 if quantity <= 0: record_note_log( diff --git a/application/rebalance_service.py b/application/rebalance_service.py index 9c8fd9f..754dc54 100644 --- a/application/rebalance_service.py +++ b/application/rebalance_service.py @@ -211,7 +211,6 @@ def fetch_replanned_state(): limit_sell_discount=config.limit_sell_discount, limit_buy_premium=config.limit_buy_premium, dry_run_only=config.dry_run_only, - fractional_limit_buy_fallback_to_market=config.fractional_limit_buy_fallback_to_market, 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, diff --git a/application/runtime_composer.py b/application/runtime_composer.py index a353ce6..706e624 100644 --- a/application/runtime_composer.py +++ b/application/runtime_composer.py @@ -39,7 +39,6 @@ class LongBridgeRuntimeComposer: order_poll_interval_sec: int order_poll_max_attempts: int dry_run_only: bool = False - fractional_limit_buy_fallback_to_market: bool = False broker_adapters: Any = None strategy_adapters: Any = None estimate_max_purchase_quantity_fn: Callable[..., float] | None = None @@ -177,7 +176,6 @@ def build_rebalance_config(self, *, strategy_plugin_signals=()) -> LongBridgeReb with_prefix=self.with_prefix, strategy_display_name=self.strategy_display_name_localized, dry_run_only=self.dry_run_only, - fractional_limit_buy_fallback_to_market=self.fractional_limit_buy_fallback_to_market, post_sell_refresh_attempts=self.order_poll_max_attempts, post_sell_refresh_interval_sec=self.order_poll_interval_sec, sleeper=self.sleeper, @@ -225,7 +223,6 @@ def build_runtime_composer( order_poll_interval_sec: int, order_poll_max_attempts: int, dry_run_only: bool, - fractional_limit_buy_fallback_to_market: bool, dry_run_only_override: bool | None = None, broker_adapters: Any, strategy_adapters: Any, @@ -267,7 +264,6 @@ def build_runtime_composer( order_poll_interval_sec=int(order_poll_interval_sec), order_poll_max_attempts=int(order_poll_max_attempts), dry_run_only=bool(dry_run_only if dry_run_only_override is None else dry_run_only_override), - fractional_limit_buy_fallback_to_market=bool(fractional_limit_buy_fallback_to_market), broker_adapters=broker_adapters, strategy_adapters=strategy_adapters, estimate_max_purchase_quantity_fn=estimate_max_purchase_quantity_fn, diff --git a/application/runtime_dependencies.py b/application/runtime_dependencies.py index 55a4b89..00faf7f 100644 --- a/application/runtime_dependencies.py +++ b/application/runtime_dependencies.py @@ -18,7 +18,6 @@ class LongBridgeRebalanceConfig: with_prefix: Callable[[str], str] strategy_display_name: str = "" dry_run_only: bool = False - fractional_limit_buy_fallback_to_market: bool = False post_sell_refresh_attempts: int = 1 post_sell_refresh_interval_sec: float = 0.0 sleeper: Callable[[float], None] | None = None diff --git a/main.py b/main.py index e4b80b1..10cbeaf 100644 --- a/main.py +++ b/main.py @@ -173,7 +173,6 @@ def build_composer(*, dry_run_only_override: bool | None = None): order_poll_max_attempts=ORDER_POLL_MAX_ATTEMPTS, dry_run_only=RUNTIME_SETTINGS.dry_run_only, dry_run_only_override=dry_run_only_override, - fractional_limit_buy_fallback_to_market=RUNTIME_SETTINGS.fractional_limit_buy_fallback_to_market, broker_adapters=BROKER_ADAPTERS, strategy_adapters=STRATEGY_ADAPTERS, estimate_max_purchase_quantity_fn=estimate_max_purchase_quantity, diff --git a/notifications/telegram.py b/notifications/telegram.py index ec9d2aa..ca94a47 100644 --- a/notifications/telegram.py +++ b/notifications/telegram.py @@ -62,7 +62,6 @@ "buy_deferred_small_cash": "{symbol} 目标差额 ${diff},但可投资现金 ${investable} 不足买入 1 股(价格 ${price})", "buy_deferred_cash_limit": "{symbol} 目标差额 ${diff},预算可买 {budget_qty} 股,但券商估算可买数量为 0;可能有未完成挂单、结算或购买力占用", "cash_sweep_rebuy": "🏦 [尾部回补] 剩余可投资现金回补 {symbol}: {qty}股 @ ${price}", - "fractional_limit_buy_fallback_to_market": "{symbol} 限价买入碎股不稳定,已改为市价买入 {qty} 股;限价参考价 ${limit_price},市价参考价 ${market_price}", "limit_buy": "📈 [限价买入] {symbol}: {qty}股 @ ${price}", "market_buy": "📈 [市价买入] {symbol}: {qty}股 @ ${price}", "limit_sell": "📉 [限价卖出] {symbol}: {qty}股 @ ${price}", @@ -157,7 +156,6 @@ "buy_deferred_small_cash": "{symbol} target gap ${diff}, but investable cash ${investable} is not enough for 1 share at ${price}", "buy_deferred_cash_limit": "{symbol} target gap ${diff}, budget supports {budget_qty} shares, but broker estimate returned 0; an open order, settlement, or buying-power hold may still be blocking funds", "cash_sweep_rebuy": "🏦 [tail rebuy] residual investable cash rebought {symbol}: {qty} shares @ ${price}", - "fractional_limit_buy_fallback_to_market": "{symbol} fractional limit buy is unstable, falling back to market buy for {qty} shares; limit reference ${limit_price}, market reference ${market_price}", "limit_buy": "📈 [Limit buy] {symbol}: {qty} shares @ ${price}", "market_buy": "📈 [Market buy] {symbol}: {qty} shares @ ${price}", "limit_sell": "📉 [Limit sell] {symbol}: {qty} shares @ ${price}", diff --git a/runtime_config_support.py b/runtime_config_support.py index eb5c5f8..7cae507 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -38,7 +38,6 @@ class PlatformRuntimeSettings: tg_token: str | None tg_chat_id: str | None dry_run_only: bool - fractional_limit_buy_fallback_to_market: bool = False debug_position_snapshot: bool = False income_threshold_usd: float | None = None qqqi_income_ratio: float | None = None @@ -113,9 +112,6 @@ 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")), - fractional_limit_buy_fallback_to_market=resolve_bool_value( - os.getenv("LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET") - ), 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(), diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index 3e9a942..d7fe0a3 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -399,7 +399,6 @@ def _run_strategy( portfolio_snapshots=None, estimate_max_purchase_quantity_value=0, dry_run_only=False, - fractional_limit_buy_fallback_to_market=False, strategy_display_name="SOXL/SOXX 半导体趋势收益", post_sell_refresh_attempts=1, ): @@ -471,7 +470,6 @@ def fake_resolve_rebalance_plan(*, indicators, snapshot): with_prefix=lambda message: f"[HK/LongBridgeQuant] {message}", strategy_display_name=strategy_display_name, dry_run_only=dry_run_only, - fractional_limit_buy_fallback_to_market=fractional_limit_buy_fallback_to_market, post_sell_refresh_attempts=post_sell_refresh_attempts, post_sell_refresh_interval_sec=0.0, sleeper=observed_sleeps.append, @@ -546,7 +544,7 @@ def test_buy_skip_without_orders_is_sent_in_single_heartbeat_message(self): self.assertIn("可投资现金", sent_messages[0]) self.assertIn("SOXX.US", sent_messages[0]) - def test_fractional_strategy_target_rebuys_cash_sweep_symbol_after_buy_skip(self): + def test_strategy_target_rebuys_cash_sweep_symbol_after_buy_skip(self): plan = _build_plan( strategy_symbols=("SOXL", "SOXX", "BOXX"), risk_symbols=("SOXL", "SOXX"), @@ -583,7 +581,7 @@ def test_fractional_strategy_target_rebuys_cash_sweep_symbol_after_buy_skip(self self.assertIn("买入说明", sent_messages[0]) self.assertNotIn("限价买入] SOXX", sent_messages[0]) - def test_fractional_strategy_target_buy_floors_to_cash_backed_whole_shares(self): + def test_strategy_target_buy_floors_to_cash_backed_whole_shares(self): plan = _build_plan( strategy_symbols=("SOXL",), risk_symbols=("SOXL",), @@ -614,7 +612,7 @@ def test_fractional_strategy_target_buy_floors_to_cash_backed_whole_shares(self) self.assertIn("限价买入] SOXL: 4股", sent_messages[0]) self.assertNotIn("限价买入] SOXL: 3.", sent_messages[0]) - def test_fractional_limit_buy_can_fallback_to_market(self): + def test_limit_buy_floors_to_whole_shares(self): plan = _build_plan( strategy_symbols=("SOXL",), risk_symbols=("SOXL",), @@ -689,25 +687,20 @@ def estimate_max_purchase_quantity(*args, **kwargs): translator=build_translator("zh"), with_prefix=lambda message: f"[HK/LongBridgeQuant] {message}", strategy_display_name="SOXL/SOXX 半导体趋势收益", - fractional_limit_buy_fallback_to_market=True, ), ) self.assertEqual(len(observed_orders), 1) - self.assertEqual(observed_orders[0].order_type, "market") - self.assertEqual(float(observed_orders[0].quantity), 3.75) + self.assertEqual(observed_orders[0].order_type, "limit") + self.assertEqual(float(observed_orders[0].quantity), 3.0) self.assertGreaterEqual( observed_estimates.count(("limit", 153.69)), 1, ) - self.assertGreaterEqual( - observed_estimates.count(("market", 152.93)), - 1, - ) self.assertEqual(len(sent_messages), 1) - self.assertIn("已改为市价买入", sent_messages[0]) + self.assertIn("限价买入", sent_messages[0]) - def test_fractional_limit_sell_preserves_fractional_quantity(self): + def test_limit_sell_floors_to_whole_shares(self): plan = _build_plan( strategy_symbols=("BOXX",), risk_symbols=("BOXX",), @@ -734,10 +727,10 @@ def test_fractional_limit_sell_preserves_fractional_quantity(self): ) self.assertEqual(len(sent_messages), 1) - self.assertIn("限价卖出] BOXX: 1.5股", sent_messages[0]) - self.assertNotIn("限价卖出] BOXX: 1股", sent_messages[0]) + self.assertIn("限价卖出] BOXX: 1股", sent_messages[0]) + self.assertNotIn("限价卖出] BOXX: 1.5股", sent_messages[0]) - def test_fractional_market_buy_preserves_fractional_quantity(self): + def test_market_buy_floors_to_whole_shares(self): plan = _build_plan( strategy_symbols=("BOXX",), safe_haven_symbols=("BOXX",), @@ -765,8 +758,8 @@ def test_fractional_market_buy_preserves_fractional_quantity(self): ) self.assertEqual(len(sent_messages), 1) - self.assertIn("市价买入] BOXX: 1.5股", sent_messages[0]) - self.assertNotIn("市价买入] BOXX: 1股", sent_messages[0]) + self.assertIn("市价买入] BOXX: 1股", sent_messages[0]) + self.assertNotIn("市价买入] BOXX: 1.5股", sent_messages[0]) def test_zero_target_sell_uses_sellable_quantity_not_price_derived_floor(self): plan = _build_plan( @@ -1130,7 +1123,7 @@ def test_cash_sweep_symbol_does_not_sell_when_sweep_cannot_fund_one_share(self): self.assertNotIn("市价卖出", sent_messages[0]) self.assertNotIn("限价买入", sent_messages[0]) - def test_cash_sweep_symbol_sells_full_fractional_position_when_helper_cannot_fund_buy(self): + def test_cash_sweep_symbol_sells_full_position_when_helper_cannot_fund_buy(self): plan = _build_plan( strategy_symbols=("SOXL", "SOXX", "BOXX"), risk_symbols=("SOXL", "SOXX"), @@ -1176,8 +1169,8 @@ def test_cash_sweep_symbol_sells_full_fractional_position_when_helper_cannot_fun plan, refreshed_plan=refreshed_plan, portfolio_snapshots=[ - _build_snapshot(plan, phase="before_cash_sweep_fractional"), - _build_snapshot(refreshed_plan, phase="after_cash_sweep_fractional"), + _build_snapshot(plan, phase="before_cash_sweep_whole_share"), + _build_snapshot(refreshed_plan, phase="after_cash_sweep_whole_share"), ], prices={"SOXL.US": 167.79, "SOXX.US": 200.0, "BOXX.US": 116.59}, estimate_max_purchase_quantity_value=10, @@ -1222,10 +1215,10 @@ def test_dry_run_cash_sweep_can_simulate_buy_after_sell_settlement(self): self.assertEqual(len(sent_messages), 1) self.assertIn("🧪 模拟运行模式", sent_messages[0]) self.assertIn("🧪 模拟市价卖出 BOXX.US", sent_messages[0]) - self.assertIn("🧪 模拟限价买入 SOXL.US", sent_messages[0]) + self.assertIn("🧪 模拟限价买入 SOXL.US: 4股 @ $100.50", sent_messages[0]) self.assertNotIn("买入说明", sent_messages[0]) - def test_dry_run_cash_sweep_ignores_zero_estimator_after_sell_settlement(self): + def test_dry_run_cash_sweep_reports_note_when_estimator_is_zero(self): initial_plan = _build_plan( strategy_symbols=("SOXL", "SOXX", "BOXX"), risk_symbols=("SOXL", "SOXX"), @@ -1257,8 +1250,11 @@ def test_dry_run_cash_sweep_ignores_zero_estimator_after_sell_settlement(self): self.assertEqual(len(sent_messages), 1) self.assertIn("🧪 模拟运行模式", sent_messages[0]) self.assertIn("🧪 模拟市价卖出 BOXX.US", sent_messages[0]) - self.assertIn("🧪 模拟限价买入 SOXL.US", sent_messages[0]) - self.assertNotIn("券商估算可买数量为 0", sent_messages[0]) + self.assertIn( + "ℹ️ [买入说明] SOXL.US 目标差额 $500.00,预算可买 4 股,但券商估算可买数量为 0;可能有未完成挂单、结算或购买力占用", + sent_messages[0], + ) + self.assertNotIn("🧪 模拟限价买入 SOXL.US", sent_messages[0]) def test_dry_run_rebuys_cash_sweep_symbol_with_remaining_investable_cash(self): initial_plan = _build_plan( diff --git a/tests/test_request_handling.py b/tests/test_request_handling.py index 3a8c5e3..4a80067 100644 --- a/tests/test_request_handling.py +++ b/tests/test_request_handling.py @@ -66,7 +66,6 @@ def run(self, *args, **kwargs): tg_token=None, tg_chat_id="shared-chat-id", dry_run_only=False, - fractional_limit_buy_fallback_to_market=False, runtime_target=build_runtime_target( platform_id="longbridge", strategy_profile="soxl_soxx_trend_income", diff --git a/tests/test_runtime_composer.py b/tests/test_runtime_composer.py index 5289d14..46ed20f 100644 --- a/tests/test_runtime_composer.py +++ b/tests/test_runtime_composer.py @@ -52,7 +52,6 @@ def fake_bootstrap_builder(**kwargs): order_poll_interval_sec=1, order_poll_max_attempts=8, dry_run_only=True, - fractional_limit_buy_fallback_to_market=True, runtime_target=build_runtime_target( platform_id="longbridge", strategy_profile="soxl_soxx_trend_income", @@ -127,4 +126,3 @@ def fake_bootstrap_builder(**kwargs): assert config.limit_buy_premium == 1.005 assert config.strategy_display_name == "SOXL/SOXX 半导体趋势收益" assert config.dry_run_only is True - assert config.fractional_limit_buy_fallback_to_market is True diff --git a/tests/test_runtime_config_support.py b/tests/test_runtime_config_support.py index 8b2277b..85645ad 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -99,7 +99,6 @@ def test_load_platform_runtime_settings_uses_defaults_with_explicit_strategy_pro self.assertIsNone(settings.tg_token) self.assertIsNone(settings.tg_chat_id) self.assertFalse(settings.dry_run_only) - self.assertFalse(settings.fractional_limit_buy_fallback_to_market) self.assertFalse(settings.debug_position_snapshot) self.assertIsNotNone(settings.runtime_target) self.assertEqual(settings.runtime_target.platform_id, "longbridge") @@ -164,19 +163,6 @@ def test_dry_run_only_is_loaded_from_env(self): self.assertTrue(settings.dry_run_only) - def test_fractional_limit_buy_fallback_is_loaded_from_env(self): - with patch.dict( - os.environ, - { - "RUNTIME_TARGET_JSON": runtime_target_json(SAMPLE_STRATEGY_PROFILE), - "LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET": "true", - }, - clear=True, - ): - settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") - - self.assertTrue(settings.fractional_limit_buy_fallback_to_market) - def test_debug_position_snapshot_is_loaded_from_env(self): with patch.dict( os.environ, diff --git a/tests/test_shared_chat_id_fallback.py b/tests/test_shared_chat_id_fallback.py index fb5ca3f..1b25af6 100644 --- a/tests/test_shared_chat_id_fallback.py +++ b/tests/test_shared_chat_id_fallback.py @@ -67,7 +67,6 @@ def run(self, *args, **kwargs): tg_token=None, tg_chat_id="shared-chat-id", dry_run_only=False, - fractional_limit_buy_fallback_to_market=False, runtime_target=build_runtime_target( platform_id="longbridge", strategy_profile="soxl_soxx_trend_income", diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index 796bbc6..0d2182b 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -47,7 +47,6 @@ grep -Fq 'LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.LONGBRIDGE_STRATEGY_P grep -Fq 'INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}' "$workflow_file" grep -Fq 'QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}' "$workflow_file" grep -Fq 'LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }}' "$workflow_file" -grep -Fq 'LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET: ${{ vars.LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET }}' "$workflow_file" grep -Fq 'RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }}' "$workflow_file" grep -Fq 'ACCOUNT_REGION: ${{ vars.ACCOUNT_REGION || matrix.target.default_account_region }}' "$workflow_file" grep -Fq 'echo "enabled=false" >> "$GITHUB_OUTPUT"' "$workflow_file" @@ -81,7 +80,6 @@ grep -Fq 'LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH=${LONGBRIDGE_FEATURE_SNAPSHO grep -Fq 'LONGBRIDGE_STRATEGY_CONFIG_PATH=${LONGBRIDGE_STRATEGY_CONFIG_PATH}' "$workflow_file" grep -Fq 'LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON=${LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON}' "$workflow_file" grep -Fq 'LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}' "$workflow_file" -grep -Fq 'LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET=${LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET}' "$workflow_file" grep -Fq 'INCOME_THRESHOLD_USD=${INCOME_THRESHOLD_USD}' "$workflow_file" grep -Fq 'QQQI_INCOME_RATIO=${QQQI_INCOME_RATIO}' "$workflow_file" grep -Fq 'STRATEGY_PROFILE=${STRATEGY_PROFILE}' "$workflow_file"