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
27 changes: 18 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,38 @@ jobs:
run: |
set -euo pipefail
ref="main"
if [ -n "${GITHUB_HEAD_REF:-}" ] && git ls-remote --exit-code --heads https://github.com/QuantStrategyLab/QuantPlatformKit.git "${GITHUB_HEAD_REF}" >/dev/null 2>&1; then
ref="${GITHUB_HEAD_REF}"
fi
for candidate in "${GITHUB_HEAD_REF:-}" "${GITHUB_BASE_REF:-}"; do
if [ -n "$candidate" ] && git ls-remote --exit-code --heads https://github.com/QuantStrategyLab/QuantPlatformKit.git "$candidate" >/dev/null 2>&1; then
ref="$candidate"
break
fi
done
echo "ref=${ref}" >> "$GITHUB_OUTPUT"

- name: Resolve UsEquityStrategies ref
id: us-equity-strategies-ref
run: |
set -euo pipefail
ref="main"
if [ -n "${GITHUB_HEAD_REF:-}" ] && git ls-remote --exit-code --heads https://github.com/QuantStrategyLab/UsEquityStrategies.git "${GITHUB_HEAD_REF}" >/dev/null 2>&1; then
ref="${GITHUB_HEAD_REF}"
fi
for candidate in "${GITHUB_HEAD_REF:-}" "${GITHUB_BASE_REF:-}"; do
if [ -n "$candidate" ] && git ls-remote --exit-code --heads https://github.com/QuantStrategyLab/UsEquityStrategies.git "$candidate" >/dev/null 2>&1; then
ref="$candidate"
break
fi
done
echo "ref=${ref}" >> "$GITHUB_OUTPUT"

- name: Resolve UsEquitySnapshotPipelines ref
id: us-equity-snapshot-pipelines-ref
run: |
set -euo pipefail
ref="main"
if [ -n "${GITHUB_HEAD_REF:-}" ] && git ls-remote --exit-code --heads https://github.com/QuantStrategyLab/UsEquitySnapshotPipelines.git "${GITHUB_HEAD_REF}" >/dev/null 2>&1; then
ref="${GITHUB_HEAD_REF}"
fi
for candidate in "${GITHUB_HEAD_REF:-}" "${GITHUB_BASE_REF:-}"; do
if [ -n "$candidate" ] && git ls-remote --exit-code --heads https://github.com/QuantStrategyLab/UsEquitySnapshotPipelines.git "$candidate" >/dev/null 2>&1; then
ref="$candidate"
break
fi
done
echo "ref=${ref}" >> "$GITHUB_OUTPUT"

- name: Checkout QuantPlatformKit
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ The mainline runtime now follows one path only:
- `tech_communication_pullback_enhancement`
- `mega_cap_leader_rotation_aggressive`
- `mega_cap_leader_rotation_dynamic_top20`
- `mega_cap_leader_rotation_top50_balanced`
- `dynamic_mega_leveraged_pullback`


Expand All @@ -55,6 +56,7 @@ The mainline runtime now follows one path only:
| `tech_communication_pullback_enhancement` | Tech/Communication Pullback Enhancement | Yes | Yes | `us_equity` | enabled feature-snapshot alternative |
| `mega_cap_leader_rotation_aggressive` | Mega Cap Leader Rotation Aggressive | Yes | Yes | `us_equity` | enabled aggressive leader rotation |
| `mega_cap_leader_rotation_dynamic_top20` | Mega Cap Leader Rotation Dynamic Top20 | Yes | Yes | `us_equity` | enabled concentrated leader rotation |
| `mega_cap_leader_rotation_top50_balanced` | Mega Cap Leader Rotation Top50 Balanced | Yes | Yes | `us_equity` | enabled balanced Top50 leader rotation |
| `dynamic_mega_leveraged_pullback` | Dynamic Mega Leveraged Pullback | Yes | Yes | `us_equity` | enabled 2x mega-cap pullback line |

Check the current matrix locally:
Expand Down Expand Up @@ -101,9 +103,9 @@ The selected `ACCOUNT_GROUP` is now the runtime identity. Keep broker-specific i
| `IB_GATEWAY_ZONE` | Optional fallback | GCE zone (for example `us-central1-a`). Recommended to keep in the selected account-group entry; this env var is only a transition fallback. |
| `IB_GATEWAY_IP_MODE` | Optional fallback | `internal` (default) or `external`. Recommended to keep in the selected account-group entry; this env var is only a transition fallback. |
| `IBKR_CONNECT_TIMEOUT_SECONDS` | No | IB API handshake timeout in seconds. Defaults to `60`; raise only if Gateway remote API startup is consistently slow. |
| `STRATEGY_PROFILE` | Yes | Strategy profile selector. Supported `us_equity` values: `global_etf_rotation`, `russell_1000_multi_factor_defensive`, `tqqq_growth_income`, `soxl_soxx_trend_income`, `tech_communication_pullback_enhancement`, `mega_cap_leader_rotation_aggressive`, `mega_cap_leader_rotation_dynamic_top20`, `dynamic_mega_leveraged_pullback` |
| `STRATEGY_PROFILE` | Yes | Strategy profile selector. Supported `us_equity` values: `global_etf_rotation`, `russell_1000_multi_factor_defensive`, `tqqq_growth_income`, `soxl_soxx_trend_income`, `tech_communication_pullback_enhancement`, `mega_cap_leader_rotation_aggressive`, `mega_cap_leader_rotation_dynamic_top20`, `mega_cap_leader_rotation_top50_balanced`, `dynamic_mega_leveraged_pullback` |
| `ACCOUNT_GROUP` | Yes | Account-group selector. Set explicitly for each deployment. |
| `IBKR_FEATURE_SNAPSHOT_PATH` | Conditionally required | Required for snapshot-backed profiles such as `russell_1000_multi_factor_defensive`, `tech_communication_pullback_enhancement`, and `mega_cap_leader_rotation_dynamic_top20`. Path to the latest feature snapshot file (`.csv`, `.json`, `.jsonl`, `.parquet`). |
| `IBKR_FEATURE_SNAPSHOT_PATH` | Conditionally required | Required for snapshot-backed profiles such as `russell_1000_multi_factor_defensive`, `tech_communication_pullback_enhancement`, `mega_cap_leader_rotation_dynamic_top20`, and `mega_cap_leader_rotation_top50_balanced`. Path to the latest feature snapshot file (`.csv`, `.json`, `.jsonl`, `.parquet`). |
| `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` | Yes for Cloud Run | Secret Manager secret name for account-group config JSON. Recommended production source. |
| `IB_ACCOUNT_GROUP_CONFIG_JSON` | No | Local/dev JSON fallback for account-group config. Not recommended for production Cloud Run. |
| `TELEGRAM_TOKEN` | Yes | Telegram bot token. For Cloud Run, prefer a Secret Manager reference instead of a literal env var. |
Expand Down Expand Up @@ -291,7 +293,7 @@ gcloud run services update ibkr-quant \

IBKR runtime 负责把共享的 `us_equity` 策略档位部署到 GCP Cloud Run,并连接 GCE 上的 IB Gateway 执行。策略逻辑、策略频率、标的池、参数和研究/回测说明都放在 `UsEquityStrategies`;这个仓库只维护 IBKR 运行时、账号组、Gateway 连接、下单和通知。

当前 `global_etf_rotation`、`russell_1000_multi_factor_defensive`、`tqqq_growth_income`、`soxl_soxx_trend_income`、`tech_communication_pullback_enhancement`、`mega_cap_leader_rotation_aggressive`、`mega_cap_leader_rotation_dynamic_top20` 和 `dynamic_mega_leveraged_pullback` 的策略实现都来自 `UsEquityStrategies`。
当前 `global_etf_rotation`、`russell_1000_multi_factor_defensive`、`tqqq_growth_income`、`soxl_soxx_trend_income`、`tech_communication_pullback_enhancement`、`mega_cap_leader_rotation_aggressive`、`mega_cap_leader_rotation_dynamic_top20`、`mega_cap_leader_rotation_top50_balanced` 和 `dynamic_mega_leveraged_pullback` 的策略实现都来自 `UsEquityStrategies`。

完整策略说明现在放在 [`UsEquityStrategies`](https://github.com/QuantStrategyLab/UsEquityStrategies)。这个 README 只保留 IBKR 运行时、profile 启用状态、部署和凭据说明。

Expand Down Expand Up @@ -333,7 +335,7 @@ IBKR 账户
| `IB_GATEWAY_ZONE` | 可选过渡项 | GCE zone(如 `us-central1-a`)。推荐直接放进选中的账号组配置里;这里只保留过渡 fallback。 |
| `IB_GATEWAY_IP_MODE` | 可选过渡项 | `internal`(默认)或 `external`。推荐直接放进选中的账号组配置里;这里只保留过渡 fallback。 |
| `IBKR_CONNECT_TIMEOUT_SECONDS` | 否 | IB API 握手超时时间,单位秒。默认 `60`;只有 Gateway 远程 API 启动持续偏慢时才需要调高。 |
| `STRATEGY_PROFILE` | 是 | 策略档位选择。当前可用的 `us_equity` 值:`global_etf_rotation`、`russell_1000_multi_factor_defensive`、`tqqq_growth_income`、`soxl_soxx_trend_income`、`tech_communication_pullback_enhancement`、`mega_cap_leader_rotation_aggressive`、`mega_cap_leader_rotation_dynamic_top20`、`dynamic_mega_leveraged_pullback` |
| `STRATEGY_PROFILE` | 是 | 策略档位选择。当前可用的 `us_equity` 值:`global_etf_rotation`、`russell_1000_multi_factor_defensive`、`tqqq_growth_income`、`soxl_soxx_trend_income`、`tech_communication_pullback_enhancement`、`mega_cap_leader_rotation_aggressive`、`mega_cap_leader_rotation_dynamic_top20`、`mega_cap_leader_rotation_top50_balanced`、`dynamic_mega_leveraged_pullback` |
| `ACCOUNT_GROUP` | 是 | 账号组选择器,每个部署都要显式设置。 |
| `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` | Cloud Run 建议必填 | 账号组配置 JSON 在 Secret Manager 里的密钥名。生产环境推荐使用。 |
| `IB_ACCOUNT_GROUP_CONFIG_JSON` | 否 | 本地开发用的账号组配置 JSON fallback。不建议在生产 Cloud Run 直接使用。 |
Expand Down
60 changes: 60 additions & 0 deletions application/ibkr_order_execution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""IBKR order submission adapters for platform-specific broker quirks."""

from __future__ import annotations

from dataclasses import replace
from typing import Any, Callable

from quant_platform_kit.common.models import ExecutionReport, OrderIntent
from quant_platform_kit.ibkr.execution import submit_order_intent as _submit_order_intent

DEFAULT_TIME_IN_FORCE = "DAY"


def _intent_with_default_time_in_force(order_intent: OrderIntent) -> OrderIntent:
if order_intent.time_in_force:
return order_intent
return replace(order_intent, time_in_force=DEFAULT_TIME_IN_FORCE)


def _market_order_factory_with_time_in_force(
market_order_factory: Callable[..., Any] | None,
*,
time_in_force: str,
) -> Callable[..., Any]:
def factory(side: str, quantity: float) -> Any:
factory_impl = market_order_factory
if factory_impl is None:
from ib_insync import MarketOrder

factory_impl = MarketOrder
order = factory_impl(side, quantity)
order.tif = time_in_force
return order

return factory


def submit_order_intent(
ib: Any,
order_intent: OrderIntent,
*,
wait_seconds: float = 1.0,
stock_factory: Callable[..., Any] | None = None,
market_order_factory: Callable[..., Any] | None = None,
limit_order_factory: Callable[..., Any] | None = None,
) -> ExecutionReport:
"""Submit an IBKR order with explicit TIF to avoid account-preset rejections."""

intent = _intent_with_default_time_in_force(order_intent)
return _submit_order_intent(
ib,
intent,
wait_seconds=wait_seconds,
stock_factory=stock_factory,
market_order_factory=_market_order_factory_with_time_in_force(
market_order_factory,
time_in_force=intent.time_in_force or DEFAULT_TIME_IN_FORCE,
),
limit_order_factory=limit_order_factory,
)
96 changes: 96 additions & 0 deletions application/paper_liquidation_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Paper-account liquidation helper for controlled strategy switch rehearsals."""

from __future__ import annotations

from dataclasses import asdict, is_dataclass
from typing import Any


def _position_symbol(position: Any, *, fallback: str | None = None) -> str:
if isinstance(position, dict):
return str(position.get("symbol") or fallback or "").strip().upper()
return str(getattr(position, "symbol", fallback or "") or "").strip().upper()


def _position_quantity(position: Any) -> float:
if isinstance(position, dict):
return float(position.get("quantity", position.get("position", 0.0)) or 0.0)
return float(getattr(position, "quantity", getattr(position, "position", 0.0)) or 0.0)


def build_liquidation_intents(positions, *, order_intent_cls) -> tuple[Any, ...]:
intents = []
iterable = (
positions.items()
if isinstance(positions, dict)
else ((None, position) for position in positions or ())
)
for symbol_hint, position in iterable:
symbol = _position_symbol(position, fallback=symbol_hint)
quantity = _position_quantity(position)
if not symbol or quantity == 0:
continue
side = "sell" if quantity > 0 else "buy"
intents.append(
order_intent_cls(
symbol=symbol,
side=side,
quantity=abs(quantity),
)
)
return tuple(intents)


def _report_to_dict(report: Any) -> dict[str, Any]:
if is_dataclass(report):
return asdict(report)
return {
"symbol": getattr(report, "symbol", None),
"side": getattr(report, "side", None),
"quantity": getattr(report, "quantity", None),
"status": getattr(report, "status", None),
"broker_order_id": getattr(report, "broker_order_id", None),
}


def execute_paper_liquidation(
ib,
positions,
*,
submit_order_intent,
order_intent_cls,
dry_run_only: bool,
) -> dict[str, Any]:
intents = build_liquidation_intents(positions, order_intent_cls=order_intent_cls)
summary: dict[str, Any] = {
"mode": "dry_run" if dry_run_only else "paper",
"positions_seen": len(positions or ()),
"orders_submitted": [],
"orders_skipped": [],
"execution_status": "no_op" if not intents else "dry_run" if dry_run_only else "executing",
}
if not intents:
return summary

if dry_run_only:
summary["orders_submitted"] = [
{
"symbol": intent.symbol,
"side": intent.side,
"quantity": intent.quantity,
"status": "dry_run",
}
for intent in intents
]
return summary

for intent in intents:
report = submit_order_intent(ib, intent)
payload = _report_to_dict(report)
status = str(payload.get("status") or "")
if status in {"Submitted", "PreSubmitted", "Filled", "PartiallyFilled", "Partial"}:
summary["orders_submitted"].append(payload)
else:
summary["orders_skipped"].append({**payload, "reason": status or "submit_failed"})
summary["execution_status"] = "submitted" if summary["orders_submitted"] else "blocked"
return summary
42 changes: 42 additions & 0 deletions docs/ibkr_paper_top50_switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# IBKR Paper Top50 Balanced Switch Runbook

This branch is for a paper-account rehearsal only. Do not use it for live IBKR.

## Safety Gates

- `ACCOUNT_GROUP=default` must resolve to `ib_gateway_mode=paper`.
- `IBKR_PAPER_LIQUIDATE_ONLY=true` is guarded in code and raises if the selected account group is not paper.
- Keep `IBKR_DRY_RUN_ONLY=true` for the first invocation after every deploy or env change.

## Required Runtime Env

```text
STRATEGY_PROFILE=mega_cap_leader_rotation_top50_balanced
ACCOUNT_GROUP=default
IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME=ibkr-account-groups
IBKR_FEATURE_SNAPSHOT_PATH=gs://qsl-runtime-logs-interactivebrokersquant/strategy-artifacts/us_equity/mega_cap_leader_rotation_top50_balanced_staging/mega_cap_leader_rotation_top50_balanced_feature_snapshot_latest.csv
IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH=gs://qsl-runtime-logs-interactivebrokersquant/strategy-artifacts/us_equity/mega_cap_leader_rotation_top50_balanced_staging/mega_cap_leader_rotation_top50_balanced_feature_snapshot_latest.csv.manifest.json
NOTIFY_LANG=zh
```

## Rehearsal Sequence

1. Publish a fresh `mega_cap_leader_rotation_top50_balanced` snapshot/manifest/ranking/release summary to the GCS prefix above.
2. Deploy this branch to `interactive-brokers-quant-service`.
3. Set `IBKR_DRY_RUN_ONLY=true` and invoke once. Confirm the notification is Chinese and the target list is sensible.
4. Set `IBKR_PAPER_LIQUIDATE_ONLY=true` and keep `IBKR_DRY_RUN_ONLY=true`; invoke once to preview paper liquidation orders.
5. Remove `IBKR_DRY_RUN_ONLY`, keep `IBKR_PAPER_LIQUIDATE_ONLY=true`; invoke once to clear the paper account.
6. Remove `IBKR_PAPER_LIQUIDATE_ONLY`; invoke once to run the Top50 balanced strategy on the cleared paper account.
7. Review Telegram, Cloud Logging, and the execution report artifact before any future live discussion.

## Rollback

Restore the previous paper strategy env:

```text
STRATEGY_PROFILE=soxl_soxx_trend_income
unset IBKR_FEATURE_SNAPSHOT_PATH
unset IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH
unset IBKR_PAPER_LIQUIDATE_ONLY
unset IBKR_DRY_RUN_ONLY
```
Loading