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
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ This repository uses `QuantPlatformKit` for LongPort token handling, context boo
The `semiconductor_rotation_income` allocation logic is loaded from `UsEquityStrategies`. `LongBridgePlatform` keeps the LongPort runtime, token refresh, execution, and notification flow.

Full strategy documentation now lives in [`UsEquityStrategies`](https://github.com/QuantStrategyLab/UsEquityStrategies#semiconductor_rotation_income). The sections below focus on execution-side defaults and runtime behavior.
This runtime matrix is the authoritative enablement source for LongBridge. `UsEquityStrategies` only carries strategy-layer compatibility and metadata.


**LongBridge profile matrix**

| Canonical profile | Display name | Alias | Enabled | Default | Rollback | Domain | Runtime note |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `semiconductor_rotation_income` | Semiconductor Trend Income | `semiconductor_trend_income` | Yes | Yes | Yes | `us_equity` | current LongBridge default |

**Layers**

Expand Down Expand Up @@ -86,7 +94,7 @@ BOXX: $34,000.00 Cash: $10,000.00
| `LONGPORT_SECRET_NAME` | No | Secret Manager secret name for LongPort token (default: `longport_token_hk`) |
| `ACCOUNT_PREFIX` | No | Alert/log prefix for account/environment (default: `DEFAULT`) |
| `SERVICE_NAME` | No | Alert/log prefix for service identity (default: `longbridge-quant-semiconductor-rotation-income`) |
| `STRATEGY_PROFILE` | No | Strategy profile selector (default: `semiconductor_rotation_income`; supported value: `semiconductor_rotation_income`) |
| `STRATEGY_PROFILE` | No | Strategy profile selector (default: `semiconductor_rotation_income`; supported canonical value: `semiconductor_rotation_income`; alias: `semiconductor_trend_income`) |
| `ACCOUNT_REGION` | No | Account region marker for platform-style deployment (e.g. `HK`, `SG`; defaults to `ACCOUNT_PREFIX` / service-name suffix / `DEFAULT`) |
| `NOTIFY_LANG` | No | Notification language: `en` (English, default) or `zh` (Chinese) |
| `GOOGLE_CLOUD_PROJECT` | No | GCP project ID (defaults to ADC project when unset) |
Expand All @@ -110,7 +118,7 @@ Deploy the same codebase as multiple Cloud Run services (e.g. `HK` and `SG`) by
- `LONGPORT_SECRET_NAME`: point to different secrets (e.g. `longport_token_hk`, `longport_token_sg`)
- `ACCOUNT_PREFIX`: e.g. `HK`, `SG` (all Telegram/log alerts will include `[ACCOUNT_PREFIX/SERVICE_NAME]`)
- `SERVICE_NAME`: e.g. `longbridge-quant-semiconductor-rotation-income-hk`, `longbridge-quant-semiconductor-rotation-income-sg`
- `STRATEGY_PROFILE`: use `semiconductor_rotation_income` for the current LongBridge strategy profile
- `STRATEGY_PROFILE`: use canonical `semiconductor_rotation_income` for the current LongBridge strategy profile; alias `semiconductor_trend_income` resolves to the same strategy
- Current strategy domain is `us_equity`. The repo now keeps a small strategy registry so future strategy switching can grow by domain + profile, instead of mixing platform and strategy in one layer.
- `ACCOUNT_REGION`: explicitly mark the deployed account region (`HK` / `SG`); if unset, the app falls back to `ACCOUNT_PREFIX` or the `-hk` / `-sg` service-name suffix
- `NOTIFY_LANG`: set `en` or `zh` per deployment
Expand Down Expand Up @@ -257,7 +265,7 @@ BOXX: $34,000.00 现金: $10,000.00
| `LONGPORT_SECRET_NAME` | 否 | Secret Manager 中的密钥名称(默认: `longport_token_hk`) |
| `ACCOUNT_PREFIX` | 否 | 通知/日志前缀,区分账户环境(默认: `DEFAULT`) |
| `SERVICE_NAME` | 否 | 通知/日志前缀,区分服务(默认: `longbridge-quant-semiconductor-rotation-income`) |
| `STRATEGY_PROFILE` | 否 | 策略档位选择(默认: `semiconductor_rotation_income`;当前支持值: `semiconductor_rotation_income`) |
| `STRATEGY_PROFILE` | 否 | 策略档位选择(默认: `semiconductor_rotation_income`;当前 canonical 值: `semiconductor_rotation_income`;alias: `semiconductor_trend_income`) |
| `ACCOUNT_REGION` | 否 | 平台化部署时的账户区域标记(如 `HK`、`SG`;默认按 `ACCOUNT_PREFIX` / 服务名后缀 / `DEFAULT` 推断) |
| `NOTIFY_LANG` | 否 | 通知语言: `en`(英文,默认)或 `zh`(中文) |
| `GOOGLE_CLOUD_PROJECT` | 否 | GCP 项目 ID(未设置时使用 ADC 默认项目) |
Expand All @@ -281,7 +289,7 @@ Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `lo
- `LONGPORT_SECRET_NAME`: 指向不同密钥(如 `longport_token_hk`、`longport_token_sg`)
- `ACCOUNT_PREFIX`: 如 `HK`、`SG`(所有通知/日志将包含 `[ACCOUNT_PREFIX/SERVICE_NAME]`)
- `SERVICE_NAME`: 如 `longbridge-quant-semiconductor-rotation-income-hk`、`longbridge-quant-semiconductor-rotation-income-sg`
- `STRATEGY_PROFILE`: 当前 LongBridge 策略档位使用 `semiconductor_rotation_income`
- `STRATEGY_PROFILE`: 当前 LongBridge 策略档位使用 canonical `semiconductor_rotation_income`;alias `semiconductor_trend_income` 会解析到同一条策略
- 当前策略域是 `us_equity`。本地策略注册表只用于域和 profile 校验。
- `ACCOUNT_REGION`: 显式标记部署账户区域(`HK` / `SG`);未设置时会回退到 `ACCOUNT_PREFIX` 或服务名里的 `-hk` / `-sg` 后缀
- `NOTIFY_LANG`: 每个部署可独立设置 `en` 或 `zh`
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
flask
gunicorn
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@v0.6.0
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@v0.6.0
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@17fa78c22463070b88ba4a884c84e7a761835ccb
pandas
requests
pytz
Expand Down
41 changes: 25 additions & 16 deletions strategy_registry.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
from __future__ import annotations

from us_equity_strategies import get_strategy_definitions as get_us_equity_strategy_definitions

from quant_platform_kit.common.strategies import (
StrategyDefinition,
US_EQUITY_DOMAIN,
get_supported_profiles_for_platform as qpk_get_supported_profiles_for_platform,
resolve_strategy_definition as qpk_resolve_strategy_definition,
from us_equity_strategies.platform_registry_support import (
build_platform_profile_matrix,
get_enabled_profiles_for_platform,
resolve_platform_strategy_definition,
)

LONGBRIDGE_PLATFORM = "longbridge"
from quant_platform_kit.common.strategies import StrategyDefinition, US_EQUITY_DOMAIN

LONGBRIDGE_PLATFORM = "longbridge"

DEFAULT_STRATEGY_PROFILE = "semiconductor_rotation_income"
ROLLBACK_STRATEGY_PROFILE = DEFAULT_STRATEGY_PROFILE

STRATEGY_DEFINITIONS = get_us_equity_strategy_definitions()
LONGBRIDGE_ENABLED_PROFILES = frozenset({"semiconductor_rotation_income"})

PLATFORM_SUPPORTED_DOMAINS: dict[str, frozenset[str]] = {
LONGBRIDGE_PLATFORM: frozenset({US_EQUITY_DOMAIN}),
}

SUPPORTED_STRATEGY_PROFILES = frozenset(STRATEGY_DEFINITIONS)
SUPPORTED_STRATEGY_PROFILES = LONGBRIDGE_ENABLED_PROFILES


def get_supported_profiles_for_platform(platform_id: str) -> frozenset[str]:
return qpk_get_supported_profiles_for_platform(
STRATEGY_DEFINITIONS,
PLATFORM_SUPPORTED_DOMAINS,
platform_id=platform_id,
return get_enabled_profiles_for_platform(
platform_id,
expected_platform_id=LONGBRIDGE_PLATFORM,
enabled_profiles=LONGBRIDGE_ENABLED_PROFILES,
)


def get_platform_profile_matrix() -> list[dict[str, object]]:
return build_platform_profile_matrix(
platform_id=LONGBRIDGE_PLATFORM,
enabled_profiles=LONGBRIDGE_ENABLED_PROFILES,
default_profile=DEFAULT_STRATEGY_PROFILE,
rollback_profile=ROLLBACK_STRATEGY_PROFILE,
)


Expand All @@ -36,10 +44,11 @@ def resolve_strategy_definition(
*,
platform_id: str,
) -> StrategyDefinition:
return qpk_resolve_strategy_definition(
return resolve_platform_strategy_definition(
raw_value,
platform_id=platform_id,
strategy_definitions=STRATEGY_DEFINITIONS,
expected_platform_id=LONGBRIDGE_PLATFORM,
enabled_profiles=LONGBRIDGE_ENABLED_PROFILES,
platform_supported_domains=PLATFORM_SUPPORTED_DOMAINS,
default_profile=DEFAULT_STRATEGY_PROFILE,
)
15 changes: 14 additions & 1 deletion tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
infer_account_region,
load_platform_runtime_settings,
)
from strategy_registry import LONGBRIDGE_PLATFORM, US_EQUITY_DOMAIN, get_supported_profiles_for_platform
from strategy_registry import LONGBRIDGE_PLATFORM, US_EQUITY_DOMAIN, get_platform_profile_matrix, get_supported_profiles_for_platform


class RuntimeConfigSupportTests(unittest.TestCase):
Expand All @@ -44,6 +44,12 @@ def test_platform_supported_profiles_are_filtered_by_registry(self):
frozenset({DEFAULT_STRATEGY_PROFILE}),
)

def test_accepts_human_readable_alias(self):
with patch.dict(os.environ, {"STRATEGY_PROFILE": "semiconductor_trend_income"}, clear=True):
settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1")

self.assertEqual(settings.strategy_profile, DEFAULT_STRATEGY_PROFILE)

def test_account_region_prefers_explicit_env(self):
region = infer_account_region(
"sg",
Expand Down Expand Up @@ -73,6 +79,13 @@ def test_unsupported_strategy_profile_fails_fast(self):
with self.assertRaisesRegex(ValueError, "Unsupported STRATEGY_PROFILE"):
load_platform_runtime_settings(project_id_resolver=lambda: "project-1")

def test_platform_profile_matrix_marks_default(self):
rows = get_platform_profile_matrix()
self.assertEqual(rows[0]["canonical_profile"], DEFAULT_STRATEGY_PROFILE)
self.assertEqual(rows[0]["display_name"], "Semiconductor Trend Income")
self.assertTrue(rows[0]["is_default"])



if __name__ == "__main__":
unittest.main()
15 changes: 15 additions & 0 deletions tests/test_strategy_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ def test_load_allocation_module_resolves_semiconductor_rotation_income(self):
"us_equity_strategies.strategies.semiconductor_rotation_income",
)

def test_load_allocation_module_resolves_semiconductor_rotation_income_alias(self):
try:
from strategy_loader import load_allocation_module

module = load_allocation_module("semiconductor_trend_income")
except ModuleNotFoundError as exc:
if exc.name in {"numpy", "pandas"}:
self.skipTest(f"{exc.name} is not installed")
raise

self.assertEqual(
module.__name__,
"us_equity_strategies.strategies.semiconductor_rotation_income",
)


if __name__ == "__main__":
unittest.main()