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
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ FIRSTRADE_ACCOUNT=
# Shared US equity strategy runtime.
STRATEGY_PROFILE=
FIRSTRADE_DRY_RUN_ONLY=true
FIRSTRADE_STRATEGY_ADAPTER_SOURCE_PLATFORM=longbridge
ACCOUNT_PREFIX=FIRSTRADE
ACCOUNT_REGION=US

Expand Down
23 changes: 6 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,17 @@ This platform is intended to mirror the role of `InteractiveBrokersPlatform`,
account reads, market data reads, order translation, runtime safety controls,
and deployment wiring.

Firstrade is not yet a first-class `platform_id` inside the pinned
`UsEquityStrategies` version. Until that support lands upstream, this
repository reports runtime identity as `firstrade` while loading the same
value-native strategy adapter shape used by LongBridge/Schwab. The default
source is:

```bash
FIRSTRADE_STRATEGY_ADAPTER_SOURCE_PLATFORM=longbridge
```
Firstrade is a first-class `platform_id` in `UsEquityStrategies`. It is treated
as a value-native US equity platform for strategy adapter purposes, so weight
strategies receive the same `portfolio_snapshot` input needed for platform-side
`weight -> value` translation.

Print the current Firstrade strategy matrix:

```bash
.venv/bin/python scripts/print_strategy_profile_status.py
```

The long-term target is first-class `firstrade` coverage in
`UsEquityStrategies` so this bridge can be removed.

## Environment

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

注意:当前 `UsEquityStrategies` 尚未内置 `firstrade` 平台 adapter。本仓库
临时复用 LongBridge/Schwab 的 value-native 策略输入形状,并在运行报告中保留
Firstrade 平台身份。后续应在 `UsEquityStrategies` 里补齐 first-class
`firstrade` 兼容矩阵。
`UsEquityStrategies` 已经内置 `firstrade` 平台 adapter。本仓库按 value-native
美股平台接入通用策略,策略逻辑不读取 Firstrade 环境变量,也不包含券商分支。
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ authors = [
dependencies = [
"firstrade==0.0.38",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@663e80be60b0da80e81513b711c579d221a2111d",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@3c8262d1df7d11e47e5ffbff83784544d10f4b9b",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@4d5cd0f5dc389edebc648028202fd116934ca325",
]

[tool.pytest.ini_options]
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ flask
gunicorn
firstrade==0.0.38
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@663e80be60b0da80e81513b711c579d221a2111d
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@3c8262d1df7d11e47e5ffbff83784544d10f4b9b
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@4d5cd0f5dc389edebc648028202fd116934ca325
requests
pytest
3 changes: 0 additions & 3 deletions scripts/print_strategy_profile_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ def main(argv: list[str] | None = None) -> int:
"Eligible",
"Enabled",
"Domain",
"Adapter source",
]
print(" | ".join(headers))
print(" | ".join("---" for _ in headers))
Expand All @@ -42,7 +41,6 @@ def main(argv: list[str] | None = None) -> int:
"Yes" if row["eligible"] else "No",
"Yes" if row["enabled"] else "No",
str(row["domain"]),
str(row["strategy_adapter_source_platform"]),
]
)
)
Expand All @@ -51,4 +49,3 @@ def main(argv: list[str] | None = None) -> int:

if __name__ == "__main__":
raise SystemExit(main())

7 changes: 2 additions & 5 deletions strategy_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

from strategy_registry import (
FIRSTRADE_PLATFORM,
get_strategy_adapter_source_platform,
resolve_strategy_definition,
)

Expand All @@ -24,10 +23,9 @@ def load_strategy_definition(raw_profile: str | None) -> StrategyDefinition:
def load_strategy_entrypoint_for_profile(raw_profile: str | None) -> StrategyEntrypoint:
definition = load_strategy_definition(raw_profile)
runtime_adapter = load_strategy_runtime_adapter_for_profile(raw_profile)
source_platform = get_strategy_adapter_source_platform()
return load_strategy_entrypoint(
definition,
platform_id=source_platform,
platform_id=FIRSTRADE_PLATFORM,
available_inputs=runtime_adapter.available_inputs,
available_capabilities=runtime_adapter.available_capabilities,
)
Expand All @@ -37,6 +35,5 @@ def load_strategy_runtime_adapter_for_profile(raw_profile: str | None) -> Strate
definition = load_strategy_definition(raw_profile)
return get_platform_runtime_adapter(
definition.profile,
platform_id=get_strategy_adapter_source_platform(),
platform_id=FIRSTRADE_PLATFORM,
)

83 changes: 44 additions & 39 deletions strategy_registry.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,68 @@
from __future__ import annotations

import os

from quant_platform_kit.common.strategies import (
PlatformCapabilityMatrix,
PlatformStrategyPolicy,
StrategyDefinition,
StrategyMetadata,
US_EQUITY_DOMAIN,
build_platform_profile_matrix,
build_platform_profile_status_matrix,
derive_enabled_profiles_for_platform,
derive_eligible_profiles_for_platform,
get_catalog_strategy_metadata,
get_enabled_profiles_for_platform,
resolve_platform_strategy_definition,
)
from us_equity_strategies import (
get_platform_runtime_adapter,
get_runtime_enabled_profiles,
get_strategy_catalog,
)

FIRSTRADE_PLATFORM = "firstrade"

# Firstrade is not first-class in UsEquityStrategies yet. Until that repo adds
# a native adapter key, this platform uses the same value-native strategy input
# shape as LongBridge/Schwab while reporting runtime identity as Firstrade.
DEFAULT_STRATEGY_ADAPTER_SOURCE_PLATFORM = "longbridge"
SUPPORTED_STRATEGY_ADAPTER_SOURCE_PLATFORMS = frozenset({"longbridge", "schwab"})

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

STRATEGY_CATALOG = get_strategy_catalog()
FIRSTRADE_ROLLOUT_ALLOWLIST = get_runtime_enabled_profiles()
FIRSTRADE_ENABLED_PROFILES = frozenset(sorted(FIRSTRADE_ROLLOUT_ALLOWLIST))
PLATFORM_CAPABILITY_MATRIX = PlatformCapabilityMatrix(
platform_id=FIRSTRADE_PLATFORM,
supported_domains=PLATFORM_SUPPORTED_DOMAINS[FIRSTRADE_PLATFORM],
supported_target_modes=frozenset({"weight", "value"}),
supported_inputs=frozenset(
{
"benchmark_history",
"market_history",
"portfolio_snapshot",
"derived_indicators",
"feature_snapshot",
"indicators",
"account_state",
"snapshot",
}
),
supported_capabilities=frozenset(),
)
ELIGIBLE_STRATEGY_PROFILES = derive_eligible_profiles_for_platform(
STRATEGY_CATALOG,
capability_matrix=PLATFORM_CAPABILITY_MATRIX,
runtime_adapter_loader=lambda profile: get_platform_runtime_adapter(
profile,
platform_id=FIRSTRADE_PLATFORM,
),
)
FIRSTRADE_ENABLED_PROFILES = derive_enabled_profiles_for_platform(
STRATEGY_CATALOG,
capability_matrix=PLATFORM_CAPABILITY_MATRIX,
runtime_adapter_loader=lambda profile: get_platform_runtime_adapter(
profile,
platform_id=FIRSTRADE_PLATFORM,
),
rollout_allowlist=FIRSTRADE_ROLLOUT_ALLOWLIST,
)
PLATFORM_POLICY = PlatformStrategyPolicy(
platform_id=FIRSTRADE_PLATFORM,
supported_domains=PLATFORM_SUPPORTED_DOMAINS[FIRSTRADE_PLATFORM],
Expand All @@ -49,31 +80,14 @@ def _without_selection_role_fields(row: dict[str, object]) -> dict[str, object]:
return {key: value for key, value in row.items() if key not in _SELECTION_ROLE_FIELDS}


def get_strategy_adapter_source_platform() -> str:
value = os.getenv(
"FIRSTRADE_STRATEGY_ADAPTER_SOURCE_PLATFORM",
DEFAULT_STRATEGY_ADAPTER_SOURCE_PLATFORM,
)
normalized = str(value or "").strip().lower()
if normalized not in SUPPORTED_STRATEGY_ADAPTER_SOURCE_PLATFORMS:
supported = ", ".join(sorted(SUPPORTED_STRATEGY_ADAPTER_SOURCE_PLATFORMS))
raise ValueError(
"FIRSTRADE_STRATEGY_ADAPTER_SOURCE_PLATFORM must be one of: "
f"{supported}"
)
return normalized


def get_eligible_profiles_for_platform(platform_id: str) -> frozenset[str]:
if platform_id != FIRSTRADE_PLATFORM:
return frozenset()
return FIRSTRADE_ENABLED_PROFILES
return ELIGIBLE_STRATEGY_PROFILES


def get_supported_profiles_for_platform(platform_id: str) -> frozenset[str]:
if platform_id != FIRSTRADE_PLATFORM:
return frozenset()
return FIRSTRADE_ENABLED_PROFILES
return get_enabled_profiles_for_platform(platform_id, policy=PLATFORM_POLICY)


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


def get_platform_profile_status_matrix() -> list[dict[str, object]]:
rows = [
return [
_without_selection_role_fields(row)
for row in build_platform_profile_status_matrix(
STRATEGY_CATALOG,
policy=PLATFORM_POLICY,
eligible_profiles=FIRSTRADE_ENABLED_PROFILES,
eligible_profiles=ELIGIBLE_STRATEGY_PROFILES,
)
]
source_platform = get_strategy_adapter_source_platform()
for row in rows:
row["strategy_adapter_source_platform"] = source_platform
row["runtime_note"] = (
"enabled through value-native adapter shape pending first-class "
"firstrade support in UsEquityStrategies"
)
return rows


def resolve_strategy_definition(
Expand All @@ -122,4 +128,3 @@ def resolve_strategy_metadata(
) -> StrategyMetadata:
definition = resolve_strategy_definition(raw_value, platform_id=platform_id)
return get_catalog_strategy_metadata(STRATEGY_CATALOG, definition.profile)

25 changes: 25 additions & 0 deletions tests/test_strategy_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

from strategy_loader import load_strategy_runtime_adapter_for_profile
from strategy_registry import (
FIRSTRADE_PLATFORM,
get_platform_profile_status_matrix,
get_supported_profiles_for_platform,
)


def test_firstrade_strategy_registry_uses_native_platform_adapter():
adapter = load_strategy_runtime_adapter_for_profile("global_etf_rotation")

assert adapter.available_inputs == frozenset({"market_history", "portfolio_snapshot"})
assert adapter.portfolio_input_name == "portfolio_snapshot"


def test_profile_status_matrix_reports_firstrade_without_bridge_metadata():
rows = get_platform_profile_status_matrix()

assert rows
assert all(row["platform"] == FIRSTRADE_PLATFORM for row in rows)
assert all("strategy_adapter_source_platform" not in row for row in rows)
assert "global_etf_rotation" in get_supported_profiles_for_platform(FIRSTRADE_PLATFORM)