Skip to content

Commit e8101d0

Browse files
committed
Use native Firstrade strategy adapter
1 parent 9dd1c7a commit e8101d0

8 files changed

Lines changed: 81 additions & 69 deletions

.env.example

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ FIRSTRADE_ACCOUNT=
1616
# Shared US equity strategy runtime.
1717
STRATEGY_PROFILE=
1818
FIRSTRADE_DRY_RUN_ONLY=true
19-
FIRSTRADE_STRATEGY_ADAPTER_SOURCE_PLATFORM=longbridge
2019
ACCOUNT_PREFIX=FIRSTRADE
2120
ACCOUNT_REGION=US
2221

README.md

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,25 +43,17 @@ This platform is intended to mirror the role of `InteractiveBrokersPlatform`,
4343
account reads, market data reads, order translation, runtime safety controls,
4444
and deployment wiring.
4545

46-
Firstrade is not yet a first-class `platform_id` inside the pinned
47-
`UsEquityStrategies` version. Until that support lands upstream, this
48-
repository reports runtime identity as `firstrade` while loading the same
49-
value-native strategy adapter shape used by LongBridge/Schwab. The default
50-
source is:
51-
52-
```bash
53-
FIRSTRADE_STRATEGY_ADAPTER_SOURCE_PLATFORM=longbridge
54-
```
46+
Firstrade is a first-class `platform_id` in `UsEquityStrategies`. It is treated
47+
as a value-native US equity platform for strategy adapter purposes, so weight
48+
strategies receive the same `portfolio_snapshot` input needed for platform-side
49+
`weight -> value` translation.
5550

5651
Print the current Firstrade strategy matrix:
5752

5853
```bash
5954
.venv/bin/python scripts/print_strategy_profile_status.py
6055
```
6156

62-
The long-term target is first-class `firstrade` coverage in
63-
`UsEquityStrategies` so this bridge can be removed.
64-
6557
## Environment
6658

6759
Copy `.env.example` into your secret manager or shell environment. Do not
@@ -79,7 +71,6 @@ commit credentials.
7971
| `FIRSTRADE_ACCOUNT` | Optional | Required when multiple accounts are returned |
8072
| `STRATEGY_PROFILE` | Yes for runtime | Shared US equity strategy profile |
8173
| `FIRSTRADE_DRY_RUN_ONLY` | Optional | Defaults to `true` for platform runtime |
82-
| `FIRSTRADE_STRATEGY_ADAPTER_SOURCE_PLATFORM` | Optional | `longbridge` default; `schwab` also allowed |
8374
| `ACCOUNT_PREFIX` | Optional | Alert/log prefix, default `FIRSTRADE` |
8475
| `ACCOUNT_REGION` | Optional | Runtime account scope, default `US` |
8576
| `FIRSTRADE_COOKIE_DIR` | Optional | Cookie cache directory, default `.runtime/firstrade-cookies` |
@@ -190,7 +181,5 @@ Firstrade 登录、账户/行情读取、下单转换、安全闸和部署 wirin
190181
开源协议方面:本仓库使用 MIT;上游 `firstrade` 包也是 MIT。发布或二次分发
191182
时保留 `NOTICE.md` 和上游项目信息。
192183

193-
注意:当前 `UsEquityStrategies` 尚未内置 `firstrade` 平台 adapter。本仓库
194-
临时复用 LongBridge/Schwab 的 value-native 策略输入形状,并在运行报告中保留
195-
Firstrade 平台身份。后续应在 `UsEquityStrategies` 里补齐 first-class
196-
`firstrade` 兼容矩阵。
184+
`UsEquityStrategies` 已经内置 `firstrade` 平台 adapter。本仓库按 value-native
185+
美股平台接入通用策略,策略逻辑不读取 Firstrade 环境变量,也不包含券商分支。

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ authors = [
1414
]
1515
dependencies = [
1616
"firstrade==0.0.38",
17-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@663e80be60b0da80e81513b711c579d221a2111d",
18-
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@3c8262d1df7d11e47e5ffbff83784544d10f4b9b",
17+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@df32c4b6414c10d1fbdce7443986a4a8ad7e9b64",
18+
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@4d5cd0f5dc389edebc648028202fd116934ca325",
1919
]
2020

2121
[tool.pytest.ini_options]

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
flask
22
gunicorn
33
firstrade==0.0.38
4-
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@663e80be60b0da80e81513b711c579d221a2111d
5-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@3c8262d1df7d11e47e5ffbff83784544d10f4b9b
4+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@df32c4b6414c10d1fbdce7443986a4a8ad7e9b64
5+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@4d5cd0f5dc389edebc648028202fd116934ca325
66
requests
77
pytest

scripts/print_strategy_profile_status.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ def main(argv: list[str] | None = None) -> int:
2929
"Eligible",
3030
"Enabled",
3131
"Domain",
32-
"Adapter source",
3332
]
3433
print(" | ".join(headers))
3534
print(" | ".join("---" for _ in headers))
@@ -42,7 +41,6 @@ def main(argv: list[str] | None = None) -> int:
4241
"Yes" if row["eligible"] else "No",
4342
"Yes" if row["enabled"] else "No",
4443
str(row["domain"]),
45-
str(row["strategy_adapter_source_platform"]),
4644
]
4745
)
4846
)
@@ -51,4 +49,3 @@ def main(argv: list[str] | None = None) -> int:
5149

5250
if __name__ == "__main__":
5351
raise SystemExit(main())
54-

strategy_loader.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
from strategy_registry import (
1111
FIRSTRADE_PLATFORM,
12-
get_strategy_adapter_source_platform,
1312
resolve_strategy_definition,
1413
)
1514

@@ -24,10 +23,9 @@ def load_strategy_definition(raw_profile: str | None) -> StrategyDefinition:
2423
def load_strategy_entrypoint_for_profile(raw_profile: str | None) -> StrategyEntrypoint:
2524
definition = load_strategy_definition(raw_profile)
2625
runtime_adapter = load_strategy_runtime_adapter_for_profile(raw_profile)
27-
source_platform = get_strategy_adapter_source_platform()
2826
return load_strategy_entrypoint(
2927
definition,
30-
platform_id=source_platform,
28+
platform_id=FIRSTRADE_PLATFORM,
3129
available_inputs=runtime_adapter.available_inputs,
3230
available_capabilities=runtime_adapter.available_capabilities,
3331
)
@@ -37,6 +35,5 @@ def load_strategy_runtime_adapter_for_profile(raw_profile: str | None) -> Strate
3735
definition = load_strategy_definition(raw_profile)
3836
return get_platform_runtime_adapter(
3937
definition.profile,
40-
platform_id=get_strategy_adapter_source_platform(),
38+
platform_id=FIRSTRADE_PLATFORM,
4139
)
42-

strategy_registry.py

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,68 @@
11
from __future__ import annotations
22

3-
import os
4-
53
from quant_platform_kit.common.strategies import (
4+
PlatformCapabilityMatrix,
65
PlatformStrategyPolicy,
76
StrategyDefinition,
87
StrategyMetadata,
98
US_EQUITY_DOMAIN,
109
build_platform_profile_matrix,
1110
build_platform_profile_status_matrix,
11+
derive_enabled_profiles_for_platform,
12+
derive_eligible_profiles_for_platform,
1213
get_catalog_strategy_metadata,
14+
get_enabled_profiles_for_platform,
1315
resolve_platform_strategy_definition,
1416
)
1517
from us_equity_strategies import (
18+
get_platform_runtime_adapter,
1619
get_runtime_enabled_profiles,
1720
get_strategy_catalog,
1821
)
1922

2023
FIRSTRADE_PLATFORM = "firstrade"
2124

22-
# Firstrade is not first-class in UsEquityStrategies yet. Until that repo adds
23-
# a native adapter key, this platform uses the same value-native strategy input
24-
# shape as LongBridge/Schwab while reporting runtime identity as Firstrade.
25-
DEFAULT_STRATEGY_ADAPTER_SOURCE_PLATFORM = "longbridge"
26-
SUPPORTED_STRATEGY_ADAPTER_SOURCE_PLATFORMS = frozenset({"longbridge", "schwab"})
27-
2825
PLATFORM_SUPPORTED_DOMAINS: dict[str, frozenset[str]] = {
2926
FIRSTRADE_PLATFORM: frozenset({US_EQUITY_DOMAIN}),
3027
}
3128

3229
STRATEGY_CATALOG = get_strategy_catalog()
3330
FIRSTRADE_ROLLOUT_ALLOWLIST = get_runtime_enabled_profiles()
34-
FIRSTRADE_ENABLED_PROFILES = frozenset(sorted(FIRSTRADE_ROLLOUT_ALLOWLIST))
31+
PLATFORM_CAPABILITY_MATRIX = PlatformCapabilityMatrix(
32+
platform_id=FIRSTRADE_PLATFORM,
33+
supported_domains=PLATFORM_SUPPORTED_DOMAINS[FIRSTRADE_PLATFORM],
34+
supported_target_modes=frozenset({"weight", "value"}),
35+
supported_inputs=frozenset(
36+
{
37+
"benchmark_history",
38+
"market_history",
39+
"portfolio_snapshot",
40+
"derived_indicators",
41+
"feature_snapshot",
42+
"indicators",
43+
"account_state",
44+
"snapshot",
45+
}
46+
),
47+
supported_capabilities=frozenset(),
48+
)
49+
ELIGIBLE_STRATEGY_PROFILES = derive_eligible_profiles_for_platform(
50+
STRATEGY_CATALOG,
51+
capability_matrix=PLATFORM_CAPABILITY_MATRIX,
52+
runtime_adapter_loader=lambda profile: get_platform_runtime_adapter(
53+
profile,
54+
platform_id=FIRSTRADE_PLATFORM,
55+
),
56+
)
57+
FIRSTRADE_ENABLED_PROFILES = derive_enabled_profiles_for_platform(
58+
STRATEGY_CATALOG,
59+
capability_matrix=PLATFORM_CAPABILITY_MATRIX,
60+
runtime_adapter_loader=lambda profile: get_platform_runtime_adapter(
61+
profile,
62+
platform_id=FIRSTRADE_PLATFORM,
63+
),
64+
rollout_allowlist=FIRSTRADE_ROLLOUT_ALLOWLIST,
65+
)
3566
PLATFORM_POLICY = PlatformStrategyPolicy(
3667
platform_id=FIRSTRADE_PLATFORM,
3768
supported_domains=PLATFORM_SUPPORTED_DOMAINS[FIRSTRADE_PLATFORM],
@@ -49,31 +80,14 @@ def _without_selection_role_fields(row: dict[str, object]) -> dict[str, object]:
4980
return {key: value for key, value in row.items() if key not in _SELECTION_ROLE_FIELDS}
5081

5182

52-
def get_strategy_adapter_source_platform() -> str:
53-
value = os.getenv(
54-
"FIRSTRADE_STRATEGY_ADAPTER_SOURCE_PLATFORM",
55-
DEFAULT_STRATEGY_ADAPTER_SOURCE_PLATFORM,
56-
)
57-
normalized = str(value or "").strip().lower()
58-
if normalized not in SUPPORTED_STRATEGY_ADAPTER_SOURCE_PLATFORMS:
59-
supported = ", ".join(sorted(SUPPORTED_STRATEGY_ADAPTER_SOURCE_PLATFORMS))
60-
raise ValueError(
61-
"FIRSTRADE_STRATEGY_ADAPTER_SOURCE_PLATFORM must be one of: "
62-
f"{supported}"
63-
)
64-
return normalized
65-
66-
6783
def get_eligible_profiles_for_platform(platform_id: str) -> frozenset[str]:
6884
if platform_id != FIRSTRADE_PLATFORM:
6985
return frozenset()
70-
return FIRSTRADE_ENABLED_PROFILES
86+
return ELIGIBLE_STRATEGY_PROFILES
7187

7288

7389
def get_supported_profiles_for_platform(platform_id: str) -> frozenset[str]:
74-
if platform_id != FIRSTRADE_PLATFORM:
75-
return frozenset()
76-
return FIRSTRADE_ENABLED_PROFILES
90+
return get_enabled_profiles_for_platform(platform_id, policy=PLATFORM_POLICY)
7791

7892

7993
def get_platform_profile_matrix() -> list[dict[str, object]]:
@@ -84,22 +98,14 @@ def get_platform_profile_matrix() -> list[dict[str, object]]:
8498

8599

86100
def get_platform_profile_status_matrix() -> list[dict[str, object]]:
87-
rows = [
101+
return [
88102
_without_selection_role_fields(row)
89103
for row in build_platform_profile_status_matrix(
90104
STRATEGY_CATALOG,
91105
policy=PLATFORM_POLICY,
92-
eligible_profiles=FIRSTRADE_ENABLED_PROFILES,
106+
eligible_profiles=ELIGIBLE_STRATEGY_PROFILES,
93107
)
94108
]
95-
source_platform = get_strategy_adapter_source_platform()
96-
for row in rows:
97-
row["strategy_adapter_source_platform"] = source_platform
98-
row["runtime_note"] = (
99-
"enabled through value-native adapter shape pending first-class "
100-
"firstrade support in UsEquityStrategies"
101-
)
102-
return rows
103109

104110

105111
def resolve_strategy_definition(
@@ -122,4 +128,3 @@ def resolve_strategy_metadata(
122128
) -> StrategyMetadata:
123129
definition = resolve_strategy_definition(raw_value, platform_id=platform_id)
124130
return get_catalog_strategy_metadata(STRATEGY_CATALOG, definition.profile)
125-

tests/test_strategy_registry.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
from strategy_loader import load_strategy_runtime_adapter_for_profile
4+
from strategy_registry import (
5+
FIRSTRADE_PLATFORM,
6+
get_platform_profile_status_matrix,
7+
get_supported_profiles_for_platform,
8+
)
9+
10+
11+
def test_firstrade_strategy_registry_uses_native_platform_adapter():
12+
adapter = load_strategy_runtime_adapter_for_profile("global_etf_rotation")
13+
14+
assert adapter.available_inputs == frozenset({"market_history", "portfolio_snapshot"})
15+
assert adapter.portfolio_input_name == "portfolio_snapshot"
16+
17+
18+
def test_profile_status_matrix_reports_firstrade_without_bridge_metadata():
19+
rows = get_platform_profile_status_matrix()
20+
21+
assert rows
22+
assert all(row["platform"] == FIRSTRADE_PLATFORM for row in rows)
23+
assert all("strategy_adapter_source_platform" not in row for row in rows)
24+
assert "global_etf_rotation" in get_supported_profiles_for_platform(FIRSTRADE_PLATFORM)
25+

0 commit comments

Comments
 (0)