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
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ ACCOUNT_REGION=US
NOTIFY_LANG=en
TELEGRAM_TOKEN=
GLOBAL_TELEGRAM_CHAT_ID=
FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON=

# Optional independent email channel for escalated strategy plugin alerts.
CRISIS_ALERT_EMAIL_TO=
CRISIS_ALERT_EMAIL_FROM=
CRISIS_ALERT_SMTP_HOST=
CRISIS_ALERT_SMTP_PORT=587
CRISIS_ALERT_SMTP_USERNAME=
CRISIS_ALERT_SMTP_PASSWORD=
CRISIS_ALERT_SMTP_STARTTLS=true
CRISIS_ALERT_SMTP_SSL=false

# Runtime safety controls.
FIRSTRADE_COOKIE_DIR=.runtime/firstrade-cookies
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ jobs:
FIRSTRADE_STATE_PREFIX: ${{ vars.FIRSTRADE_STATE_PREFIX }}
FIRSTRADE_STRATEGY_CONFIG_PATH: ${{ vars.FIRSTRADE_STRATEGY_CONFIG_PATH }}
FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON }}
CRISIS_ALERT_EMAIL_TO: ${{ vars.CRISIS_ALERT_EMAIL_TO }}
CRISIS_ALERT_EMAIL_FROM: ${{ vars.CRISIS_ALERT_EMAIL_FROM }}
CRISIS_ALERT_SMTP_HOST: ${{ vars.CRISIS_ALERT_SMTP_HOST }}
CRISIS_ALERT_SMTP_PORT: ${{ vars.CRISIS_ALERT_SMTP_PORT }}
CRISIS_ALERT_SMTP_USERNAME: ${{ vars.CRISIS_ALERT_SMTP_USERNAME }}
CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME }}
CRISIS_ALERT_SMTP_STARTTLS: ${{ vars.CRISIS_ALERT_SMTP_STARTTLS }}
CRISIS_ALERT_SMTP_SSL: ${{ vars.CRISIS_ALERT_SMTP_SSL }}
FIRSTRADE_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS: ${{ vars.FIRSTRADE_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS }}
FIRSTRADE_TECH_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS: ${{ vars.FIRSTRADE_TECH_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS }}
INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}
Expand All @@ -69,6 +77,7 @@ jobs:
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}
NOTIFY_LANG: ${{ vars.NOTIFY_LANG }}
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
CRISIS_ALERT_SMTP_PASSWORD: ${{ secrets.CRISIS_ALERT_SMTP_PASSWORD }}
FIRSTRADE_USERNAME: ${{ secrets.FIRSTRADE_USERNAME }}
FIRSTRADE_PASSWORD: ${{ secrets.FIRSTRADE_PASSWORD }}
FIRSTRADE_MFA_SECRET: ${{ secrets.FIRSTRADE_MFA_SECRET }}
Expand Down Expand Up @@ -424,6 +433,13 @@ jobs:
add_optional_env FIRSTRADE_FEATURE_SNAPSHOT_MANIFEST_PATH
add_optional_env FIRSTRADE_STRATEGY_CONFIG_PATH
add_optional_env FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON
add_optional_env CRISIS_ALERT_EMAIL_TO
add_optional_env CRISIS_ALERT_EMAIL_FROM
add_optional_env CRISIS_ALERT_SMTP_HOST
add_optional_env CRISIS_ALERT_SMTP_PORT
add_optional_env CRISIS_ALERT_SMTP_USERNAME
add_optional_env CRISIS_ALERT_SMTP_STARTTLS
add_optional_env CRISIS_ALERT_SMTP_SSL
add_optional_env FIRSTRADE_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS
add_optional_env FIRSTRADE_TECH_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS
add_optional_env INCOME_THRESHOLD_USD
Expand All @@ -433,6 +449,7 @@ jobs:
add_optional_env NOTIFY_LANG

add_optional_secret TELEGRAM_TOKEN TELEGRAM_TOKEN_SECRET_NAME TELEGRAM_TOKEN
add_optional_secret CRISIS_ALERT_SMTP_PASSWORD CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME CRISIS_ALERT_SMTP_PASSWORD
add_optional_secret FIRSTRADE_USERNAME FIRSTRADE_USERNAME_SECRET_NAME FIRSTRADE_USERNAME
add_optional_secret FIRSTRADE_PASSWORD FIRSTRADE_PASSWORD_SECRET_NAME FIRSTRADE_PASSWORD
add_optional_secret FIRSTRADE_MFA_SECRET FIRSTRADE_MFA_SECRET_SECRET_NAME FIRSTRADE_MFA_SECRET
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ commit credentials.
| `NOTIFY_LANG` | Optional | Notification language, `en` or `zh` |
| `TELEGRAM_TOKEN` | Optional | Telegram bot token for strategy-cycle summaries |
| `GLOBAL_TELEGRAM_CHAT_ID` | Optional | Telegram chat ID for strategy-cycle summaries |
| `FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON` | Optional | JSON sidecar plugin mount config. Overrides global `STRATEGY_PLUGIN_MOUNTS_JSON` for this platform |
| `CRISIS_ALERT_EMAIL_TO` | Optional | Comma, semicolon, or newline separated recipients for escalated strategy plugin email alerts |
| `CRISIS_ALERT_EMAIL_FROM` | Optional | SMTP sender address for escalated strategy plugin email alerts |
| `CRISIS_ALERT_SMTP_HOST` | Optional | SMTP host for escalated strategy plugin email alerts |
| `CRISIS_ALERT_SMTP_PORT` | Optional | SMTP port. Defaults to `587` |
| `CRISIS_ALERT_SMTP_USERNAME` | Optional | SMTP username when authentication is required |
| `CRISIS_ALERT_SMTP_PASSWORD` | Optional | SMTP password, preferably supplied from Secret Manager in Cloud Run |
| `CRISIS_ALERT_SMTP_STARTTLS` | Optional | Enable STARTTLS for SMTP. Defaults to `true` |
| `CRISIS_ALERT_SMTP_SSL` | Optional | Use SMTP over SSL. Defaults to `false` |
| `FIRSTRADE_COOKIE_DIR` | Optional | Cookie cache directory, default `.runtime/firstrade-cookies` |
| `FIRSTRADE_ENABLE_LIVE_TRADING` | Optional | Must be `true` before any live order can be submitted |
| `FIRSTRADE_RUN_SMOKE_ON_HTTP` | Optional | Must be `true` before `/smoke` performs a real login/quote |
Expand Down Expand Up @@ -165,10 +174,13 @@ full guarded strategy cycle:
- connect to Firstrade with the unofficial client
- read the selected account, balances, positions, quotes, and OHLC history
- load the selected shared `UsEquityStrategies` runtime
- load configured shared strategy plugin signal artifacts without changing core strategy logic
- map the strategy decision into a value-target Firstrade plan
- route generated orders through the local safety layer
- publish a compact Telegram summary when `TELEGRAM_TOKEN` and
`GLOBAL_TELEGRAM_CHAT_ID` are configured
- send independent SMTP email alerts for escalated strategy plugin signals when
`CRISIS_ALERT_*` is configured

The default mode remains dry-run. A live HTTP-triggered strategy order requires
all of these gates:
Expand Down Expand Up @@ -299,6 +311,8 @@ Firstrade 登录、账户/行情读取、下单转换、安全闸和部署 wirin
- dry-run / preview 下单验证
- `/run` 执行通用美股策略的 dry-run 调仓闭环
- 配置 `TELEGRAM_TOKEN` 和 `GLOBAL_TELEGRAM_CHAT_ID` 后发送运行摘要
- 读取通用策略插件信号,并在危机类插件触发时通过 `CRISIS_ALERT_*`
配置发送独立邮件告警
- 在你再次确认后,才允许极小金额实盘验证
- 通用 `us_equity` 策略 profile 的平台层接入

Expand Down
138 changes: 136 additions & 2 deletions application/rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@
build_semiconductor_rotation_indicators_from_history,
required_semiconductor_rotation_history_lookback,
)
from quant_platform_kit.common.strategy_plugins import (
build_strategy_plugin_alert_messages,
build_strategy_plugin_notification_lines,
build_strategy_plugin_report_payload,
load_configured_strategy_plugin_signals,
parse_strategy_plugin_mounts,
)
from quant_platform_kit.notifications.email import send_smtp_email
from quant_platform_kit.notifications.events import NotificationPublisher, RenderedNotification
from quant_platform_kit.strategy_contracts import build_strategy_evaluation_inputs
from runtime_config_support import PlatformRuntimeSettings, load_platform_runtime_settings
Expand Down Expand Up @@ -162,6 +170,101 @@ def publish_log(text: str) -> None:
return True


def load_strategy_plugin_signals(
raw_mounts,
*,
strategy_profile: str,
parse_mounts_fn=parse_strategy_plugin_mounts,
load_signals_fn=load_configured_strategy_plugin_signals,
):
if not raw_mounts:
return (), None
try:
mounts = parse_mounts_fn(raw_mounts)
if not mounts:
return (), None
return load_signals_fn(mounts, strategy_profile=strategy_profile), None
except Exception as exc:
return (), f"{type(exc).__name__}: {exc}"


def attach_strategy_plugin_result(
result: dict[str, Any],
*,
signals,
error: str | None,
translator: Callable[..., str],
) -> dict[str, Any]:
if signals:
result.update(build_strategy_plugin_report_payload(signals))
notification_lines = build_strategy_plugin_notification_lines(
signals,
translator=translator,
)
if notification_lines:
result["strategy_plugin_lines"] = notification_lines
if error:
result["strategy_plugin_error"] = error
return result


def _call_log_message(log_message: Callable[..., Any], text: str) -> None:
try:
log_message(text, flush=True)
except TypeError:
log_message(text)


def send_crisis_alert_email(
alert_message,
*,
settings: PlatformRuntimeSettings,
smtp_module=None,
log_message: Callable[..., Any] = print,
) -> bool:
send_kwargs: dict[str, Any] = {}
if smtp_module is not None:
send_kwargs["smtp_module"] = smtp_module
return send_smtp_email(
subject=alert_message.subject,
body=alert_message.body,
smtp_host=getattr(settings, "crisis_alert_smtp_host", None),
smtp_port=getattr(settings, "crisis_alert_smtp_port", 587),
sender=getattr(settings, "crisis_alert_email_from", None),
recipients=getattr(settings, "crisis_alert_email_to", ()),
username=getattr(settings, "crisis_alert_smtp_username", None),
password=getattr(settings, "crisis_alert_smtp_password", None),
use_starttls=getattr(settings, "crisis_alert_smtp_starttls", True),
use_ssl=getattr(settings, "crisis_alert_smtp_ssl", False),
printer=lambda text, **_kwargs: _call_log_message(log_message, text),
**send_kwargs,
)


def publish_strategy_plugin_alerts(
signals,
*,
settings: PlatformRuntimeSettings,
translator: Callable[..., str],
log_message: Callable[..., Any] = print,
) -> int:
sent_count = 0
for alert_message in build_strategy_plugin_alert_messages(
signals,
translator=translator,
strategy_label=settings.strategy_profile,
):
if send_crisis_alert_email(
alert_message,
settings=settings,
log_message=log_message,
):
sent_count += 1
if sent_count:
_call_log_message(log_message, f"strategy_plugin_alert_email_sent count={sent_count}")
return sent_count


def _runtime_metadata_with_execution_policy(
metadata: Mapping[str, Any] | None,
*,
Expand All @@ -186,6 +289,11 @@ def run_strategy_cycle(
) -> dict[str, Any]:
now = _utcnow()
settings = runtime_settings or load_platform_runtime_settings(project_id_resolver=get_project_id)
translator = build_translator(settings.notify_lang)
strategy_plugin_signals, strategy_plugin_error = load_strategy_plugin_signals(
settings.strategy_plugin_mounts_json,
strategy_profile=settings.strategy_profile,
)
resolved_credentials = credentials or FirstradeCredentials.from_env(env_reader)
store = state_store or build_gcs_state_store_from_env(env_reader)
persist_strategy_runs = bool(settings.persist_strategy_runs and store is not None)
Expand Down Expand Up @@ -227,7 +335,7 @@ def run_strategy_cycle(
available_inputs=available_inputs,
market_inputs=market_inputs,
portfolio_snapshot=snapshot,
translator=build_translator(settings.notify_lang),
translator=translator,
)
evaluation = strategy_runtime.evaluate(**evaluation_inputs)
plan = map_strategy_decision_to_plan(
Expand Down Expand Up @@ -258,7 +366,7 @@ def run_strategy_cycle(
run_period=run_period,
)
if is_duplicate_live_run(existing_run):
return {
result = {
"ok": True,
"api_kind": "unofficial-reverse-engineered",
"account": masked_account,
Expand All @@ -280,7 +388,24 @@ def run_strategy_cycle(
}
],
"action_done": False,
"strategy_plugin_alert_email_sent_count": 0,
}
return attach_strategy_plugin_result(
result,
signals=strategy_plugin_signals,
error=strategy_plugin_error,
translator=translator,
)
strategy_plugin_alert_email_sent_count = 0
strategy_plugin_alert_email_error = None
try:
strategy_plugin_alert_email_sent_count = publish_strategy_plugin_alerts(
strategy_plugin_signals,
settings=settings,
translator=translator,
)
except Exception as exc:
strategy_plugin_alert_email_error = f"{type(exc).__name__}: {exc}"
strategy_run_persisted = False
strategy_run_persistence_error = None
if persist_strategy_runs:
Expand Down Expand Up @@ -357,6 +482,15 @@ def run_strategy_cycle(
result["funding_blocked"] = True
if strategy_run_persistence_error:
result["strategy_run_persistence_error"] = strategy_run_persistence_error
result["strategy_plugin_alert_email_sent_count"] = strategy_plugin_alert_email_sent_count
if strategy_plugin_alert_email_error:
result["strategy_plugin_alert_email_error"] = strategy_plugin_alert_email_error
attach_strategy_plugin_result(
result,
signals=strategy_plugin_signals,
error=strategy_plugin_error,
translator=translator,
)
if persist_strategy_runs:
completed_state = build_strategy_run_state(
stage=strategy_run_stage,
Expand Down
37 changes: 37 additions & 0 deletions notifications/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@
"quantity_share": "{quantity}股",
"quantity_shares": "{quantity}股",
"signal_label": "信号",
"strategy_plugin_line": "🧩 插件:{plugin} | 状态:{route} | 提醒:{action}",
"strategy_plugin_alert_subject": "🚨 危机插件告警:{plugin} | {route}",
"strategy_plugin_alert_title": "🚨 【危机插件告警】",
"strategy_plugin_alert_strategy": "策略:{strategy}",
"strategy_plugin_alert_as_of": "信号时间:{as_of}",
"strategy_plugin_alert_would_trade": "若启用交易会操作:{value}",
"strategy_plugin_alert_source": "来源:{source}",
"strategy_plugin_name_crisis_response_shadow": "危机观察通知",
"strategy_plugin_mode_shadow": "影子观察",
"strategy_plugin_route_no_action": "未触发危机",
"strategy_plugin_route_true_crisis": "真危机",
"strategy_plugin_route_unknown_route": "未知状态",
"strategy_plugin_action_no_action": "不操作",
"strategy_plugin_action_watch_only": "仅通知",
"strategy_plugin_action_defend": "防守",
"strategy_plugin_action_blocked": "已阻断",
"strategy_plugin_action_monitor": "持续观察",
"strategy_plugin_action_unknown_action": "未知提醒",
"separator": SEPARATOR,
"same_trading_day": "当日执行",
"next_trading_day": "次一交易日执行",
Expand Down Expand Up @@ -118,6 +136,24 @@
"quantity_share": "{quantity} share",
"quantity_shares": "{quantity} shares",
"signal_label": "Signal",
"strategy_plugin_line": "🧩 Plugin: {plugin} | status: {route} | notice: {action}",
"strategy_plugin_alert_subject": "🚨 Crisis plugin alert: {plugin} | {route}",
"strategy_plugin_alert_title": "🚨 【Crisis Plugin Alert】",
"strategy_plugin_alert_strategy": "Strategy: {strategy}",
"strategy_plugin_alert_as_of": "Signal as-of: {as_of}",
"strategy_plugin_alert_would_trade": "Would trade if enabled: {value}",
"strategy_plugin_alert_source": "Source: {source}",
"strategy_plugin_name_crisis_response_shadow": "Crisis Watch Notice",
"strategy_plugin_mode_shadow": "shadow",
"strategy_plugin_route_no_action": "no crisis detected",
"strategy_plugin_route_true_crisis": "true crisis",
"strategy_plugin_route_unknown_route": "unknown status",
"strategy_plugin_action_no_action": "no action",
"strategy_plugin_action_watch_only": "notify only",
"strategy_plugin_action_defend": "defend",
"strategy_plugin_action_blocked": "blocked",
"strategy_plugin_action_monitor": "watch",
"strategy_plugin_action_unknown_action": "unknown notice",
"separator": SEPARATOR,
"same_trading_day": "same trading day",
"next_trading_day": "next trading day",
Expand Down Expand Up @@ -575,6 +611,7 @@ def render_cycle_summary(result: Mapping[str, Any], *, lang: str = "en") -> str:
lines.extend(dashboard_lines)
lines.extend(_format_timing_lines(execution, translator=translator))
lines.extend(_format_signal_lines(execution, translator=translator))
lines.extend(str(line).strip() for line in result.get("strategy_plugin_lines") or ())
lines.append(SEPARATOR)
lines.extend(target_diff_lines)
if submitted:
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ authors = [
]
dependencies = [
"firstrade==0.0.38",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@9c4ea7878a08fb2f518c74c99bda68d8ef8fd0bb",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@ce7887482eeab7f519484610ee8b20cb7bc886a0",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@1b6febbba7df81179ad7579f430c26a811c0e1a8",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@1636271a3e0c17fc0c5da363f67eabe114eeff48",
"google-cloud-storage",
"requests",
]

Expand Down
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
flask
gunicorn
firstrade==0.0.38
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@9c4ea7878a08fb2f518c74c99bda68d8ef8fd0bb
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@ce7887482eeab7f519484610ee8b20cb7bc886a0
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@1b6febbba7df81179ad7579f430c26a811c0e1a8
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@1636271a3e0c17fc0c5da363f67eabe114eeff48
google-cloud-storage
requests
pytest
Loading