diff --git a/README.md b/README.md index d55378c..3543cc3 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Telegram notifications include structured execution and heartbeat messages, with | `ACCOUNT_REGION` | No | Account region marker for platform-style deployment (e.g. `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_SHARES_ENABLED` | No | Defaults to `true`; set `false` to force whole-share sizing. | -| `LONGBRIDGE_ORDER_QUANTITY_STEP` | No | Explicit order quantity step override; e.g. `1` for whole shares or `0.000001` for fractional sizing. | +| `LONGBRIDGE_ORDER_QUANTITY_STEP` | No | Explicit order quantity step override; e.g. `1` for whole shares or `0.0001` for fractional sizing. | | `LONGBRIDGE_MIN_ORDER_NOTIONAL_USD` | No | Minimum buy notional for fractional sizing; defaults to `1.0`. | | `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | No | Set to `true` to log raw LongBridge position quantity and available quantity for troubleshooting. | | `INCOME_THRESHOLD_USD` | No | Optional override for the `tqqq_growth_income` income-layer threshold. Leave unset to use the strategy package default. | @@ -74,7 +74,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) | -Quantity sizing is resolved at runtime: `LONGBRIDGE_ORDER_QUANTITY_STEP` wins when set; otherwise `LONGBRIDGE_FRACTIONAL_SHARES_ENABLED=true` uses a `0.000001` step and `false` uses whole shares. 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. +Quantity sizing is resolved at runtime: `LONGBRIDGE_ORDER_QUANTITY_STEP` wins when set; otherwise `LONGBRIDGE_FRACTIONAL_SHARES_ENABLED=true` uses a `0.0001` step and `false` uses whole shares. 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_hk`), where the **latest version = active access token**. The app refreshes it when expiry is within 30 days. @@ -217,7 +217,7 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换 | `ACCOUNT_REGION` | 否 | 平台化部署时的账户区域标记(如 `HK`、`SG`;默认按 `ACCOUNT_PREFIX` / `DEFAULT` 推断) | | `LONGBRIDGE_DRY_RUN_ONLY` | 否 | 设为 `true` 时,该部署保持 dry-run。 | | `LONGBRIDGE_FRACTIONAL_SHARES_ENABLED` | 否 | 默认 `true`;设为 `false` 时强制按整数股计算。 | -| `LONGBRIDGE_ORDER_QUANTITY_STEP` | 否 | 显式覆盖下单数量步进;如 `1` 表示整数股,`0.000001` 表示碎股数量步进。 | +| `LONGBRIDGE_ORDER_QUANTITY_STEP` | 否 | 显式覆盖下单数量步进;如 `1` 表示整数股,`0.0001` 表示碎股数量步进。 | | `LONGBRIDGE_MIN_ORDER_NOTIONAL_USD` | 否 | 碎股买入的最小名义金额;默认 `1.0`。 | | `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | 否 | 设为 `true` 时输出 LongBridge 原始持仓数量和可卖数量,便于排查。 | | `INCOME_THRESHOLD_USD` | 否 | 可选的 `tqqq_growth_income` 收入层启动阈值覆盖。不填时使用策略包默认值。 | @@ -225,7 +225,7 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换 | `NOTIFY_LANG` | 否 | 通知语言: `en`(英文,默认)或 `zh`(中文) | | `GOOGLE_CLOUD_PROJECT` | 否 | GCP 项目 ID(未设置时使用 ADC 默认项目) | -下单数量在运行时解析:显式设置 `LONGBRIDGE_ORDER_QUANTITY_STEP` 时优先使用该步进;否则 `LONGBRIDGE_FRACTIONAL_SHARES_ENABLED=true` 使用 `0.000001` 步进,`false` 使用整数股。目标市值为 0 时,卖出数量直接按可卖持仓计算,不再用当前报价反推股数,避免因报价漂移留下 1 股残仓。 +下单数量在运行时解析:显式设置 `LONGBRIDGE_ORDER_QUANTITY_STEP` 时优先使用该步进;否则 `LONGBRIDGE_FRACTIONAL_SHARES_ENABLED=true` 使用 `0.0001` 步进,`false` 使用整数股。目标市值为 0 时,卖出数量直接按可卖持仓计算,不再用当前报价反推股数,避免因报价漂移留下 1 股残仓。 Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `longport_token_hk`),**最新版本 = 当前有效的 access token**。Token 到期前 30 天会自动刷新。 diff --git a/requirements.txt b/requirements.txt index 6c584fd..c03cf46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@08ed04ae9796f54a2218ffb700f97e0e33bf312f -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@c9ec484c9a12cdffedf7d87c8906b93b21f50b1c +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@268171a74a1b522f03940af399f71c37e9a32c70 +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@47685bce6187b5e7d3f93bab72b72a7fc0d119d7 pandas requests pytz diff --git a/runtime_config_support.py b/runtime_config_support.py index 2e07de1..c72fc0b 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -110,6 +110,7 @@ def load_platform_runtime_settings( step_env="LONGBRIDGE_ORDER_QUANTITY_STEP", fractional_env="LONGBRIDGE_FRACTIONAL_SHARES_ENABLED", fractional_default=True, + fractional_step=0.0001, ), min_order_notional=resolve_float_env( os.environ, diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index 3580a1b..a907831 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -573,12 +573,12 @@ def test_fractional_quantity_step_allows_small_soxx_target_buy(self): plan, prices={"SOXX.US": 504.60, "SOXL.US": 162.93, "BOXX.US": 116.59}, estimate_max_purchase_quantity_value=10, - quantity_step=0.000001, + quantity_step=0.0001, min_order_notional=1.0, ) self.assertEqual(len(sent_messages), 1) - self.assertIn("限价买入] SOXX: 0.321699股", sent_messages[0]) + self.assertIn("限价买入] SOXX: 0.3216股", sent_messages[0]) self.assertNotIn("不足买入 1 股", sent_messages[0]) def test_zero_target_sell_uses_sellable_quantity_not_price_derived_floor(self): @@ -636,12 +636,12 @@ def test_fractional_buy_uses_budget_when_broker_estimate_is_whole_share_zero(sel plan, prices={"SOXX.US": 504.60}, estimate_max_purchase_quantity_value=0, - quantity_step=0.000001, + quantity_step=0.0001, min_order_notional=1.0, ) self.assertEqual(len(sent_messages), 1) - self.assertIn("限价买入] SOXX: 0.321699股", sent_messages[0]) + self.assertIn("限价买入] SOXX: 0.3216股", sent_messages[0]) self.assertNotIn("券商估算可买数量为 0", sent_messages[0]) def test_zero_investable_cash_reports_buying_power_without_trade_note(self): diff --git a/tests/test_runtime_composer.py b/tests/test_runtime_composer.py index b6323fc..02ed75e 100644 --- a/tests/test_runtime_composer.py +++ b/tests/test_runtime_composer.py @@ -50,7 +50,7 @@ def fake_bootstrap_builder(**kwargs): limit_buy_premium=1.005, order_poll_interval_sec=1, order_poll_max_attempts=8, - quantity_step=0.000001, + quantity_step=0.0001, min_order_notional=1.0, dry_run_only=True, broker_adapters=SimpleNamespace( @@ -113,7 +113,7 @@ def fake_bootstrap_builder(**kwargs): assert runtime.post_submit_order == "post-submit-order" assert config.limit_sell_discount == 0.995 assert config.limit_buy_premium == 1.005 - assert config.quantity_step == 0.000001 + assert config.quantity_step == 0.0001 assert config.min_order_notional == 1.0 assert config.strategy_display_name == "SOXL/SOXX 半导体趋势收益" assert config.dry_run_only is True diff --git a/tests/test_runtime_config_support.py b/tests/test_runtime_config_support.py index 80f0c52..e3fa3f0 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -52,7 +52,7 @@ 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.assertEqual(settings.quantity_step, 0.000001) + self.assertEqual(settings.quantity_step, 0.0001) self.assertEqual(settings.min_order_notional, 1.0) self.assertFalse(settings.debug_position_snapshot) self.assertIsNone(settings.income_threshold_usd)