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
14 changes: 14 additions & 0 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
LONGBRIDGE_FEATURE_SNAPSHOT_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_PATH }}
LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH }}
LONGBRIDGE_STRATEGY_CONFIG_PATH: ${{ vars.LONGBRIDGE_STRATEGY_CONFIG_PATH }}
LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON }}
# Optional strategy overrides; leave unset to inherit the UsEquityStrategies profile defaults.
INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}
QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}
Expand Down Expand Up @@ -300,6 +301,12 @@ jobs:
remove_env_vars+=("LONGBRIDGE_STRATEGY_CONFIG_PATH")
fi

if [ -n "${LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON:-}" ]; then
env_pairs+=("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON=${LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON}")
else
remove_env_vars+=("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON")
fi

if [ -n "${LONGBRIDGE_DRY_RUN_ONLY:-}" ]; then
env_pairs+=("LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}")
else
Expand Down Expand Up @@ -362,6 +369,7 @@ jobs:
LONGBRIDGE_FEATURE_SNAPSHOT_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_PATH }}
LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH }}
LONGBRIDGE_STRATEGY_CONFIG_PATH: ${{ vars.LONGBRIDGE_STRATEGY_CONFIG_PATH }}
LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON }}
INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}
QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}
NOTIFY_LANG: ${{ vars.NOTIFY_LANG }}
Expand Down Expand Up @@ -634,6 +642,12 @@ jobs:
remove_env_vars+=("LONGBRIDGE_STRATEGY_CONFIG_PATH")
fi

if [ -n "${LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON:-}" ]; then
env_pairs+=("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON=${LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON}")
else
remove_env_vars+=("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON")
fi

if [ -n "${LONGBRIDGE_DRY_RUN_ONLY:-}" ]; then
env_pairs+=("LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}")
else
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Telegram notifications include structured execution and heartbeat messages, with
| `LONGBRIDGE_DRY_RUN_ONLY` | No | Set to `true` to keep the selected deployment in dry-run mode. |
| `LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET` | No | Set to `true` to convert fractional `limit buy` orders to `market buy` orders instead of skipping them. |
| `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | No | Set to `true` to log raw LongBridge position quantity and available quantity for troubleshooting. |
| `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` | No | Optional LongBridge-side strategy plugin mount JSON. The plugin artifact controls mode; platform config must not set `mode`. |
| `INCOME_THRESHOLD_USD` | No | Optional strategy override for the `tqqq_growth_income` income-layer threshold. Leave unset to use the strategy package default. |
| `QQQI_INCOME_RATIO` | No | Optional strategy override for QQQI's share of the `tqqq_growth_income` income layer, 0–1. |
| `NOTIFY_LANG` | No | Notification language: `en` (English, default) or `zh` (Chinese) |
Expand Down Expand Up @@ -229,6 +230,7 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换
| `ACCOUNT_REGION` | 否 | 平台化部署时的账户区域标记(如 `PAPER`、`HK`、`SG`;默认按 `ACCOUNT_PREFIX` / `DEFAULT` 推断) |
| `LONGBRIDGE_DRY_RUN_ONLY` | 否 | 设为 `true` 时,该部署保持 dry-run。 |
| `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | 否 | 设为 `true` 时输出 LongBridge 原始持仓数量和可卖数量,便于排查。 |
| `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` | 否 | 可选的 LongBridge 侧策略插件挂载 JSON。插件 artifact 自带模式;平台配置不要设置 `mode`。 |
| `INCOME_THRESHOLD_USD` | 否 | 可选的 `tqqq_growth_income` 收入层启动阈值覆盖(策略 override)。不填时使用策略包默认值。 |
| `QQQI_INCOME_RATIO` | 否 | 可选的 QQQI 收入层占比覆盖,0–1(策略 override)。 |
| `NOTIFY_LANG` | 否 | 通知语言: `en`(英文,默认)或 `zh`(中文) |
Expand Down
2 changes: 2 additions & 0 deletions application/rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ def fetch_replanned_state():
separator=config.separator,
strategy_display_name=config.strategy_display_name,
dry_run_only=config.dry_run_only,
extra_notification_lines=config.extra_notification_lines,
)
)
else:
Expand All @@ -245,5 +246,6 @@ def fetch_replanned_state():
separator=config.separator,
strategy_display_name=config.strategy_display_name,
dry_run_only=config.dry_run_only,
extra_notification_lines=config.extra_notification_lines,
)
)
20 changes: 19 additions & 1 deletion application/runtime_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def build_rebalance_runtime(self) -> LongBridgeRebalanceRuntime:
post_submit_order=notification_adapters.post_submit_order,
)

def build_rebalance_config(self) -> LongBridgeRebalanceConfig:
def build_rebalance_config(self, *, strategy_plugin_signals=()) -> LongBridgeRebalanceConfig:
return LongBridgeRebalanceConfig(
limit_sell_discount=self.limit_sell_discount,
limit_buy_premium=self.limit_buy_premium,
Expand All @@ -181,6 +181,24 @@ def build_rebalance_config(self) -> LongBridgeRebalanceConfig:
post_sell_refresh_attempts=self.order_poll_max_attempts,
post_sell_refresh_interval_sec=self.order_poll_interval_sec,
sleeper=self.sleeper,
extra_notification_lines=getattr(
self.strategy_adapters,
"build_strategy_plugin_notification_lines",
lambda _signals: (),
)(strategy_plugin_signals),
)

def load_strategy_plugin_signals(self, raw_mounts):
return getattr(self.strategy_adapters, "load_strategy_plugin_signals", lambda _raw_mounts: ((), None))(raw_mounts)

def attach_strategy_plugin_report(self, report, *, signals, error: str | None = None):
attach = getattr(self.strategy_adapters, "attach_strategy_plugin_report", None)
if attach is None:
return None
return attach(
report,
signals=signals,
error=error,
)


Expand Down
1 change: 1 addition & 0 deletions application/runtime_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class LongBridgeRebalanceConfig:
post_sell_refresh_attempts: int = 1
post_sell_refresh_interval_sec: float = 0.0
sleeper: Callable[[float], None] | None = None
extra_notification_lines: tuple[str, ...] = ()


@dataclass(frozen=True)
Expand Down
54 changes: 54 additions & 0 deletions application/runtime_strategy_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,54 @@ class LongBridgeRuntimeStrategyAdapters:
calculate_rotation_indicators_fn: Callable[..., Any]
build_strategy_evaluation_inputs_fn: Callable[..., dict[str, Any]]
map_strategy_decision_to_plan_fn: Callable[..., dict[str, Any]]
build_strategy_plugin_report_payload_fn: Callable[..., dict[str, Any]] | None = None
load_configured_strategy_plugin_signals_fn: Callable[..., Any] | None = None
parse_strategy_plugin_mounts_fn: Callable[..., Any] | None = None

def load_strategy_plugin_signals(self, raw_mounts):
if not raw_mounts or self.parse_strategy_plugin_mounts_fn is None or self.load_configured_strategy_plugin_signals_fn is None:
return (), None
try:
mounts = self.parse_strategy_plugin_mounts_fn(raw_mounts)
if not mounts:
return (), None
return (
self.load_configured_strategy_plugin_signals_fn(
mounts,
strategy_profile=self.strategy_profile,
),
None,
)
except Exception as exc:
return (), f"{type(exc).__name__}: {exc}"

def attach_strategy_plugin_report(self, report, *, signals, error: str | None = None):
if signals and self.build_strategy_plugin_report_payload_fn is not None:
report.setdefault("summary", {}).update(self.build_strategy_plugin_report_payload_fn(signals))
if error:
report.setdefault("diagnostics", {})["strategy_plugin_error"] = error

def translate_strategy_plugin_value(self, category: str, raw_value: str | None) -> str:
value = str(raw_value or "").strip() or "unknown"
key = f"strategy_plugin_{category}_{value}"
translated = self.translator(key)
return translated if translated != key else value

def build_strategy_plugin_notification_lines(self, signals) -> tuple[str, ...]:
lines = []
for signal in signals:
route = signal.canonical_route or "unknown_route"
action = signal.suggested_action or "unknown_action"
lines.append(
self.translator(
"strategy_plugin_line",
plugin=self.translate_strategy_plugin_value("name", signal.plugin),
mode=self.translate_strategy_plugin_value("mode", signal.effective_mode),
route=self.translate_strategy_plugin_value("route", route),
action=self.translate_strategy_plugin_value("action", action),
)
)
return tuple(lines)

def calculate_strategy_indicators(self, quote_context):
available_inputs = set(self.available_inputs)
Expand Down Expand Up @@ -99,6 +147,9 @@ def build_runtime_strategy_adapters(
calculate_rotation_indicators_fn: Callable[..., Any],
build_strategy_evaluation_inputs_fn: Callable[..., dict[str, Any]],
map_strategy_decision_to_plan_fn: Callable[..., dict[str, Any]],
build_strategy_plugin_report_payload_fn: Callable[..., dict[str, Any]] | None = None,
load_configured_strategy_plugin_signals_fn: Callable[..., Any] | None = None,
parse_strategy_plugin_mounts_fn: Callable[..., Any] | None = None,
) -> LongBridgeRuntimeStrategyAdapters:
return LongBridgeRuntimeStrategyAdapters(
strategy_runtime=strategy_runtime,
Expand All @@ -112,4 +163,7 @@ def build_runtime_strategy_adapters(
calculate_rotation_indicators_fn=calculate_rotation_indicators_fn,
build_strategy_evaluation_inputs_fn=build_strategy_evaluation_inputs_fn,
map_strategy_decision_to_plan_fn=map_strategy_decision_to_plan_fn,
build_strategy_plugin_report_payload_fn=build_strategy_plugin_report_payload_fn,
load_configured_strategy_plugin_signals_fn=load_configured_strategy_plugin_signals_fn,
parse_strategy_plugin_mounts_fn=parse_strategy_plugin_mounts_fn,
)
18 changes: 17 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
finalize_runtime_report,
persist_runtime_report,
)
from quant_platform_kit.common.strategy_plugins import (
build_strategy_plugin_report_payload,
load_configured_strategy_plugin_signals,
parse_strategy_plugin_mounts,
)
from quant_platform_kit.strategy_contracts import build_strategy_evaluation_inputs
from runtime_logging import build_run_id, emit_runtime_log
from quant_platform_kit.longbridge import (
Expand Down Expand Up @@ -133,6 +138,9 @@ def log_position_snapshot(message):
calculate_rotation_indicators_fn=calculate_rotation_indicators,
build_strategy_evaluation_inputs_fn=build_strategy_evaluation_inputs,
map_strategy_decision_to_plan_fn=map_strategy_decision_to_plan,
build_strategy_plugin_report_payload_fn=build_strategy_plugin_report_payload,
load_configured_strategy_plugin_signals_fn=load_configured_strategy_plugin_signals,
parse_strategy_plugin_mounts_fn=parse_strategy_plugin_mounts,
)


Expand Down Expand Up @@ -184,6 +192,14 @@ def run_strategy():
reporting_adapters = composer.build_reporting_adapters()
log_context, report = reporting_adapters.start_run()
notification_adapters = composer.build_notification_adapters()
strategy_plugin_signals, strategy_plugin_error = composer.load_strategy_plugin_signals(
getattr(RUNTIME_SETTINGS, "strategy_plugin_mounts_json", None)
)
composer.attach_strategy_plugin_report(
report,
signals=strategy_plugin_signals,
error=strategy_plugin_error,
)
try:
reporting_adapters.log_event(
log_context,
Expand Down Expand Up @@ -220,7 +236,7 @@ def run_strategy():
return
run_rebalance_cycle(
runtime=composer.build_rebalance_runtime(),
config=composer.build_rebalance_config(),
config=composer.build_rebalance_config(strategy_plugin_signals=strategy_plugin_signals),
)
finalize_runtime_report(report, status="ok")
reporting_adapters.log_event(
Expand Down
13 changes: 13 additions & 0 deletions notifications/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ def _append_strategy_line(lines, *, strategy_display_name, translator):
lines.append(translator("strategy_label", name=name))


def _append_extra_notification_lines(lines, extra_notification_lines) -> None:
for line in extra_notification_lines or ():
text = str(line or "").strip()
if text:
lines.append(text)


def render_rebalance_notification(
*,
execution,
Expand All @@ -181,12 +188,14 @@ def render_rebalance_notification(
separator,
strategy_display_name,
dry_run_only,
extra_notification_lines=(),
) -> RenderedNotification:
formatted_logs = "\n".join(f" - {log}" for log in [*logs, *skip_logs, *note_logs])
detailed_lines = [translator("rebalance_title")]
_append_strategy_line(detailed_lines, strategy_display_name=strategy_display_name, translator=translator)
if dry_run_only:
detailed_lines.append(translator("dry_run_banner"))
_append_extra_notification_lines(detailed_lines, extra_notification_lines)
_append_dashboard_lines(detailed_lines, execution=execution)
_append_timing_lines(detailed_lines, execution=execution, translator=translator)
_append_status_lines(
Expand All @@ -201,6 +210,7 @@ def render_rebalance_notification(
_append_strategy_line(compact_lines, strategy_display_name=strategy_display_name, translator=translator)
if dry_run_only:
compact_lines.append(translator("dry_run_banner"))
_append_extra_notification_lines(compact_lines, extra_notification_lines)
_append_dashboard_lines(compact_lines, execution=execution)
_append_timing_lines(compact_lines, execution=execution, translator=translator)
_append_compact_status_lines(
Expand All @@ -225,11 +235,13 @@ def render_heartbeat_notification(
separator,
strategy_display_name,
dry_run_only,
extra_notification_lines=(),
) -> RenderedNotification:
detailed_lines = [translator("heartbeat_title")]
_append_strategy_line(detailed_lines, strategy_display_name=strategy_display_name, translator=translator)
if dry_run_only:
detailed_lines.append(translator("dry_run_banner"))
_append_extra_notification_lines(detailed_lines, extra_notification_lines)
_append_dashboard_lines(detailed_lines, execution=execution)
_append_timing_lines(detailed_lines, execution=execution, translator=translator)
detailed_lines.append(separator)
Expand Down Expand Up @@ -263,6 +275,7 @@ def render_heartbeat_notification(
_append_strategy_line(compact_lines, strategy_display_name=strategy_display_name, translator=translator)
if dry_run_only:
compact_lines.append(translator("dry_run_banner"))
_append_extra_notification_lines(compact_lines, extra_notification_lines)
_append_dashboard_lines(compact_lines, execution=execution)
_append_timing_lines(compact_lines, execution=execution, translator=translator)
_append_compact_status_lines(
Expand Down
30 changes: 30 additions & 0 deletions notifications/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"signal_blend_gate_overlay_capped": "{trend_symbol} 仍在 {window} 日门槛线上方,但触发过热降档({reasons}),目标仓位 {allocation_text}",
"blend_gate_reason_rsi_cap": "RSI 超阈值",
"blend_gate_reason_bollinger_cap": "突破布林上轨",
"blend_gate_reason_volatility_delever": "{symbol} {window} 日年化波动率 {volatility} 高于 {threshold},SOXL 转向 {redirect_symbol}",
"signal_hold": "趋势持有",
"signal_entry": "入场信号",
"signal_reduce": "减仓信号",
Expand All @@ -95,6 +96,20 @@
"strategy_name_tech_communication_pullback_enhancement": "科技通信回调增强",
"strategy_name_qqq_tech_enhancement": "科技通信回调增强",
"strategy_name_mega_cap_leader_rotation_top50_balanced": "Mega Cap Top50 平衡龙头轮动",
"strategy_plugin_line": "🧩 插件:{plugin} | 模式:{mode} | 路由:{route} | 建议:{action}",
"strategy_plugin_name_crisis_response_shadow": "危机响应观察",
"strategy_plugin_mode_shadow": "影子观察",
"strategy_plugin_route_no_action": "不操作",
"strategy_plugin_route_true_crisis": "真危机",
"strategy_plugin_route_taco_fake_crisis": "TACO 假危机",
"strategy_plugin_route_unknown_route": "未知路由",
"strategy_plugin_action_no_action": "不操作",
"strategy_plugin_action_watch_only": "仅观察",
"strategy_plugin_action_small_taco": "小仓 TACO",
"strategy_plugin_action_defend": "防守",
"strategy_plugin_action_blocked": "已阻断",
"strategy_plugin_action_monitor": "监控",
"strategy_plugin_action_unknown_action": "未知建议",
},
"en": {
"rebalance_title": "🔔 【Trade Execution Report】",
Expand Down Expand Up @@ -163,6 +178,7 @@
"signal_blend_gate_overlay_capped": "{trend_symbol} stays above the {window}d gate, but overlay cap ({reasons}) cuts exposure to {allocation_text}",
"blend_gate_reason_rsi_cap": "RSI over threshold",
"blend_gate_reason_bollinger_cap": "price above upper band",
"blend_gate_reason_volatility_delever": "{symbol} {window}d annualized volatility {volatility} is above {threshold}; redirect SOXL to {redirect_symbol}",
"signal_hold": "Trend Hold",
"signal_entry": "Entry Signal",
"signal_reduce": "Reduce Signal",
Expand All @@ -176,6 +192,20 @@
"strategy_name_tech_communication_pullback_enhancement": "Tech/Communication Pullback Enhancement",
"strategy_name_qqq_tech_enhancement": "Tech/Communication Pullback Enhancement",
"strategy_name_mega_cap_leader_rotation_top50_balanced": "Mega Cap Leader Rotation Top50 Balanced",
"strategy_plugin_line": "🧩 Plugin: {plugin} | mode: {mode} | route: {route} | action: {action}",
"strategy_plugin_name_crisis_response_shadow": "Crisis Response Shadow",
"strategy_plugin_mode_shadow": "shadow",
"strategy_plugin_route_no_action": "no action",
"strategy_plugin_route_true_crisis": "true crisis",
"strategy_plugin_route_taco_fake_crisis": "TACO fake crisis",
"strategy_plugin_route_unknown_route": "unknown route",
"strategy_plugin_action_no_action": "no action",
"strategy_plugin_action_watch_only": "watch only",
"strategy_plugin_action_small_taco": "small TACO",
"strategy_plugin_action_defend": "defend",
"strategy_plugin_action_blocked": "blocked",
"strategy_plugin_action_monitor": "monitor",
"strategy_plugin_action_unknown_action": "unknown action",
},
}

Expand Down
4 changes: 2 additions & 2 deletions 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@573fc9e9917cf1f2c1acda9232c5a23a8a05d797
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@53911cbe32f6932e759522e54aa38ca5350aa44e
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@8769362096227320bc05c791b5244d4b3e88db50
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@ed55a6af0245323dbed82060e89be96d8f77f756
pandas
requests
pytz
Expand Down
Loading