Skip to content

Commit 303f740

Browse files
authored
Add LongBridge SOXL plugin support (#57)
1 parent bdddd62 commit 303f740

15 files changed

Lines changed: 252 additions & 4 deletions

.github/workflows/sync-cloud-run-env.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
LONGBRIDGE_FEATURE_SNAPSHOT_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_PATH }}
3333
LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH }}
3434
LONGBRIDGE_STRATEGY_CONFIG_PATH: ${{ vars.LONGBRIDGE_STRATEGY_CONFIG_PATH }}
35+
LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON }}
3536
# Optional strategy overrides; leave unset to inherit the UsEquityStrategies profile defaults.
3637
INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}
3738
QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}
@@ -300,6 +301,12 @@ jobs:
300301
remove_env_vars+=("LONGBRIDGE_STRATEGY_CONFIG_PATH")
301302
fi
302303
304+
if [ -n "${LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON:-}" ]; then
305+
env_pairs+=("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON=${LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON}")
306+
else
307+
remove_env_vars+=("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON")
308+
fi
309+
303310
if [ -n "${LONGBRIDGE_DRY_RUN_ONLY:-}" ]; then
304311
env_pairs+=("LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}")
305312
else
@@ -362,6 +369,7 @@ jobs:
362369
LONGBRIDGE_FEATURE_SNAPSHOT_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_PATH }}
363370
LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH }}
364371
LONGBRIDGE_STRATEGY_CONFIG_PATH: ${{ vars.LONGBRIDGE_STRATEGY_CONFIG_PATH }}
372+
LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON }}
365373
INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}
366374
QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}
367375
NOTIFY_LANG: ${{ vars.NOTIFY_LANG }}
@@ -634,6 +642,12 @@ jobs:
634642
remove_env_vars+=("LONGBRIDGE_STRATEGY_CONFIG_PATH")
635643
fi
636644
645+
if [ -n "${LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON:-}" ]; then
646+
env_pairs+=("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON=${LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON}")
647+
else
648+
remove_env_vars+=("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON")
649+
fi
650+
637651
if [ -n "${LONGBRIDGE_DRY_RUN_ONLY:-}" ]; then
638652
env_pairs+=("LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}")
639653
else

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Telegram notifications include structured execution and heartbeat messages, with
7070
| `LONGBRIDGE_DRY_RUN_ONLY` | No | Set to `true` to keep the selected deployment in dry-run mode. |
7171
| `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. |
7272
| `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | No | Set to `true` to log raw LongBridge position quantity and available quantity for troubleshooting. |
73+
| `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` | No | Optional LongBridge-side strategy plugin mount JSON. The plugin artifact controls mode; platform config must not set `mode`. |
7374
| `INCOME_THRESHOLD_USD` | No | Optional strategy override for the `tqqq_growth_income` income-layer threshold. Leave unset to use the strategy package default. |
7475
| `QQQI_INCOME_RATIO` | No | Optional strategy override for QQQI's share of the `tqqq_growth_income` income layer, 0–1. |
7576
| `NOTIFY_LANG` | No | Notification language: `en` (English, default) or `zh` (Chinese) |
@@ -229,6 +230,7 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换
229230
| `ACCOUNT_REGION` || 平台化部署时的账户区域标记(如 `PAPER``HK``SG`;默认按 `ACCOUNT_PREFIX` / `DEFAULT` 推断) |
230231
| `LONGBRIDGE_DRY_RUN_ONLY` || 设为 `true` 时,该部署保持 dry-run。 |
231232
| `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` || 设为 `true` 时输出 LongBridge 原始持仓数量和可卖数量,便于排查。 |
233+
| `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` || 可选的 LongBridge 侧策略插件挂载 JSON。插件 artifact 自带模式;平台配置不要设置 `mode`|
232234
| `INCOME_THRESHOLD_USD` || 可选的 `tqqq_growth_income` 收入层启动阈值覆盖(策略 override)。不填时使用策略包默认值。 |
233235
| `QQQI_INCOME_RATIO` || 可选的 QQQI 收入层占比覆盖,0–1(策略 override)。 |
234236
| `NOTIFY_LANG` || 通知语言: `en`(英文,默认)或 `zh`(中文) |

application/rebalance_service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ def fetch_replanned_state():
233233
separator=config.separator,
234234
strategy_display_name=config.strategy_display_name,
235235
dry_run_only=config.dry_run_only,
236+
extra_notification_lines=config.extra_notification_lines,
236237
)
237238
)
238239
else:
@@ -245,5 +246,6 @@ def fetch_replanned_state():
245246
separator=config.separator,
246247
strategy_display_name=config.strategy_display_name,
247248
dry_run_only=config.dry_run_only,
249+
extra_notification_lines=config.extra_notification_lines,
248250
)
249251
)

application/runtime_composer.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def build_rebalance_runtime(self) -> LongBridgeRebalanceRuntime:
168168
post_submit_order=notification_adapters.post_submit_order,
169169
)
170170

171-
def build_rebalance_config(self) -> LongBridgeRebalanceConfig:
171+
def build_rebalance_config(self, *, strategy_plugin_signals=()) -> LongBridgeRebalanceConfig:
172172
return LongBridgeRebalanceConfig(
173173
limit_sell_discount=self.limit_sell_discount,
174174
limit_buy_premium=self.limit_buy_premium,
@@ -181,6 +181,24 @@ def build_rebalance_config(self) -> LongBridgeRebalanceConfig:
181181
post_sell_refresh_attempts=self.order_poll_max_attempts,
182182
post_sell_refresh_interval_sec=self.order_poll_interval_sec,
183183
sleeper=self.sleeper,
184+
extra_notification_lines=getattr(
185+
self.strategy_adapters,
186+
"build_strategy_plugin_notification_lines",
187+
lambda _signals: (),
188+
)(strategy_plugin_signals),
189+
)
190+
191+
def load_strategy_plugin_signals(self, raw_mounts):
192+
return getattr(self.strategy_adapters, "load_strategy_plugin_signals", lambda _raw_mounts: ((), None))(raw_mounts)
193+
194+
def attach_strategy_plugin_report(self, report, *, signals, error: str | None = None):
195+
attach = getattr(self.strategy_adapters, "attach_strategy_plugin_report", None)
196+
if attach is None:
197+
return None
198+
return attach(
199+
report,
200+
signals=signals,
201+
error=error,
184202
)
185203

186204

application/runtime_dependencies.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class LongBridgeRebalanceConfig:
2222
post_sell_refresh_attempts: int = 1
2323
post_sell_refresh_interval_sec: float = 0.0
2424
sleeper: Callable[[float], None] | None = None
25+
extra_notification_lines: tuple[str, ...] = ()
2526

2627

2728
@dataclass(frozen=True)

application/runtime_strategy_adapters.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,54 @@ class LongBridgeRuntimeStrategyAdapters:
2020
calculate_rotation_indicators_fn: Callable[..., Any]
2121
build_strategy_evaluation_inputs_fn: Callable[..., dict[str, Any]]
2222
map_strategy_decision_to_plan_fn: Callable[..., dict[str, Any]]
23+
build_strategy_plugin_report_payload_fn: Callable[..., dict[str, Any]] | None = None
24+
load_configured_strategy_plugin_signals_fn: Callable[..., Any] | None = None
25+
parse_strategy_plugin_mounts_fn: Callable[..., Any] | None = None
26+
27+
def load_strategy_plugin_signals(self, raw_mounts):
28+
if not raw_mounts or self.parse_strategy_plugin_mounts_fn is None or self.load_configured_strategy_plugin_signals_fn is None:
29+
return (), None
30+
try:
31+
mounts = self.parse_strategy_plugin_mounts_fn(raw_mounts)
32+
if not mounts:
33+
return (), None
34+
return (
35+
self.load_configured_strategy_plugin_signals_fn(
36+
mounts,
37+
strategy_profile=self.strategy_profile,
38+
),
39+
None,
40+
)
41+
except Exception as exc:
42+
return (), f"{type(exc).__name__}: {exc}"
43+
44+
def attach_strategy_plugin_report(self, report, *, signals, error: str | None = None):
45+
if signals and self.build_strategy_plugin_report_payload_fn is not None:
46+
report.setdefault("summary", {}).update(self.build_strategy_plugin_report_payload_fn(signals))
47+
if error:
48+
report.setdefault("diagnostics", {})["strategy_plugin_error"] = error
49+
50+
def translate_strategy_plugin_value(self, category: str, raw_value: str | None) -> str:
51+
value = str(raw_value or "").strip() or "unknown"
52+
key = f"strategy_plugin_{category}_{value}"
53+
translated = self.translator(key)
54+
return translated if translated != key else value
55+
56+
def build_strategy_plugin_notification_lines(self, signals) -> tuple[str, ...]:
57+
lines = []
58+
for signal in signals:
59+
route = signal.canonical_route or "unknown_route"
60+
action = signal.suggested_action or "unknown_action"
61+
lines.append(
62+
self.translator(
63+
"strategy_plugin_line",
64+
plugin=self.translate_strategy_plugin_value("name", signal.plugin),
65+
mode=self.translate_strategy_plugin_value("mode", signal.effective_mode),
66+
route=self.translate_strategy_plugin_value("route", route),
67+
action=self.translate_strategy_plugin_value("action", action),
68+
)
69+
)
70+
return tuple(lines)
2371

2472
def calculate_strategy_indicators(self, quote_context):
2573
available_inputs = set(self.available_inputs)
@@ -99,6 +147,9 @@ def build_runtime_strategy_adapters(
99147
calculate_rotation_indicators_fn: Callable[..., Any],
100148
build_strategy_evaluation_inputs_fn: Callable[..., dict[str, Any]],
101149
map_strategy_decision_to_plan_fn: Callable[..., dict[str, Any]],
150+
build_strategy_plugin_report_payload_fn: Callable[..., dict[str, Any]] | None = None,
151+
load_configured_strategy_plugin_signals_fn: Callable[..., Any] | None = None,
152+
parse_strategy_plugin_mounts_fn: Callable[..., Any] | None = None,
102153
) -> LongBridgeRuntimeStrategyAdapters:
103154
return LongBridgeRuntimeStrategyAdapters(
104155
strategy_runtime=strategy_runtime,
@@ -112,4 +163,7 @@ def build_runtime_strategy_adapters(
112163
calculate_rotation_indicators_fn=calculate_rotation_indicators_fn,
113164
build_strategy_evaluation_inputs_fn=build_strategy_evaluation_inputs_fn,
114165
map_strategy_decision_to_plan_fn=map_strategy_decision_to_plan_fn,
166+
build_strategy_plugin_report_payload_fn=build_strategy_plugin_report_payload_fn,
167+
load_configured_strategy_plugin_signals_fn=load_configured_strategy_plugin_signals_fn,
168+
parse_strategy_plugin_mounts_fn=parse_strategy_plugin_mounts_fn,
115169
)

main.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
finalize_runtime_report,
2525
persist_runtime_report,
2626
)
27+
from quant_platform_kit.common.strategy_plugins import (
28+
build_strategy_plugin_report_payload,
29+
load_configured_strategy_plugin_signals,
30+
parse_strategy_plugin_mounts,
31+
)
2732
from quant_platform_kit.strategy_contracts import build_strategy_evaluation_inputs
2833
from runtime_logging import build_run_id, emit_runtime_log
2934
from quant_platform_kit.longbridge import (
@@ -133,6 +138,9 @@ def log_position_snapshot(message):
133138
calculate_rotation_indicators_fn=calculate_rotation_indicators,
134139
build_strategy_evaluation_inputs_fn=build_strategy_evaluation_inputs,
135140
map_strategy_decision_to_plan_fn=map_strategy_decision_to_plan,
141+
build_strategy_plugin_report_payload_fn=build_strategy_plugin_report_payload,
142+
load_configured_strategy_plugin_signals_fn=load_configured_strategy_plugin_signals,
143+
parse_strategy_plugin_mounts_fn=parse_strategy_plugin_mounts,
136144
)
137145

138146

@@ -184,6 +192,14 @@ def run_strategy():
184192
reporting_adapters = composer.build_reporting_adapters()
185193
log_context, report = reporting_adapters.start_run()
186194
notification_adapters = composer.build_notification_adapters()
195+
strategy_plugin_signals, strategy_plugin_error = composer.load_strategy_plugin_signals(
196+
getattr(RUNTIME_SETTINGS, "strategy_plugin_mounts_json", None)
197+
)
198+
composer.attach_strategy_plugin_report(
199+
report,
200+
signals=strategy_plugin_signals,
201+
error=strategy_plugin_error,
202+
)
187203
try:
188204
reporting_adapters.log_event(
189205
log_context,
@@ -220,7 +236,7 @@ def run_strategy():
220236
return
221237
run_rebalance_cycle(
222238
runtime=composer.build_rebalance_runtime(),
223-
config=composer.build_rebalance_config(),
239+
config=composer.build_rebalance_config(strategy_plugin_signals=strategy_plugin_signals),
224240
)
225241
finalize_runtime_report(report, status="ok")
226242
reporting_adapters.log_event(

notifications/renderers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ def _append_strategy_line(lines, *, strategy_display_name, translator):
171171
lines.append(translator("strategy_label", name=name))
172172

173173

174+
def _append_extra_notification_lines(lines, extra_notification_lines) -> None:
175+
for line in extra_notification_lines or ():
176+
text = str(line or "").strip()
177+
if text:
178+
lines.append(text)
179+
180+
174181
def render_rebalance_notification(
175182
*,
176183
execution,
@@ -181,12 +188,14 @@ def render_rebalance_notification(
181188
separator,
182189
strategy_display_name,
183190
dry_run_only,
191+
extra_notification_lines=(),
184192
) -> RenderedNotification:
185193
formatted_logs = "\n".join(f" - {log}" for log in [*logs, *skip_logs, *note_logs])
186194
detailed_lines = [translator("rebalance_title")]
187195
_append_strategy_line(detailed_lines, strategy_display_name=strategy_display_name, translator=translator)
188196
if dry_run_only:
189197
detailed_lines.append(translator("dry_run_banner"))
198+
_append_extra_notification_lines(detailed_lines, extra_notification_lines)
190199
_append_dashboard_lines(detailed_lines, execution=execution)
191200
_append_timing_lines(detailed_lines, execution=execution, translator=translator)
192201
_append_status_lines(
@@ -201,6 +210,7 @@ def render_rebalance_notification(
201210
_append_strategy_line(compact_lines, strategy_display_name=strategy_display_name, translator=translator)
202211
if dry_run_only:
203212
compact_lines.append(translator("dry_run_banner"))
213+
_append_extra_notification_lines(compact_lines, extra_notification_lines)
204214
_append_dashboard_lines(compact_lines, execution=execution)
205215
_append_timing_lines(compact_lines, execution=execution, translator=translator)
206216
_append_compact_status_lines(
@@ -225,11 +235,13 @@ def render_heartbeat_notification(
225235
separator,
226236
strategy_display_name,
227237
dry_run_only,
238+
extra_notification_lines=(),
228239
) -> RenderedNotification:
229240
detailed_lines = [translator("heartbeat_title")]
230241
_append_strategy_line(detailed_lines, strategy_display_name=strategy_display_name, translator=translator)
231242
if dry_run_only:
232243
detailed_lines.append(translator("dry_run_banner"))
244+
_append_extra_notification_lines(detailed_lines, extra_notification_lines)
233245
_append_dashboard_lines(detailed_lines, execution=execution)
234246
_append_timing_lines(detailed_lines, execution=execution, translator=translator)
235247
detailed_lines.append(separator)
@@ -263,6 +275,7 @@ def render_heartbeat_notification(
263275
_append_strategy_line(compact_lines, strategy_display_name=strategy_display_name, translator=translator)
264276
if dry_run_only:
265277
compact_lines.append(translator("dry_run_banner"))
278+
_append_extra_notification_lines(compact_lines, extra_notification_lines)
266279
_append_dashboard_lines(compact_lines, execution=execution)
267280
_append_timing_lines(compact_lines, execution=execution, translator=translator)
268281
_append_compact_status_lines(

notifications/telegram.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"signal_blend_gate_overlay_capped": "{trend_symbol} 仍在 {window} 日门槛线上方,但触发过热降档({reasons}),目标仓位 {allocation_text}",
8383
"blend_gate_reason_rsi_cap": "RSI 超阈值",
8484
"blend_gate_reason_bollinger_cap": "突破布林上轨",
85+
"blend_gate_reason_volatility_delever": "{symbol} {window} 日年化波动率 {volatility} 高于 {threshold},SOXL 转向 {redirect_symbol}",
8586
"signal_hold": "趋势持有",
8687
"signal_entry": "入场信号",
8788
"signal_reduce": "减仓信号",
@@ -95,6 +96,20 @@
9596
"strategy_name_tech_communication_pullback_enhancement": "科技通信回调增强",
9697
"strategy_name_qqq_tech_enhancement": "科技通信回调增强",
9798
"strategy_name_mega_cap_leader_rotation_top50_balanced": "Mega Cap Top50 平衡龙头轮动",
99+
"strategy_plugin_line": "🧩 插件:{plugin} | 模式:{mode} | 路由:{route} | 建议:{action}",
100+
"strategy_plugin_name_crisis_response_shadow": "危机响应观察",
101+
"strategy_plugin_mode_shadow": "影子观察",
102+
"strategy_plugin_route_no_action": "不操作",
103+
"strategy_plugin_route_true_crisis": "真危机",
104+
"strategy_plugin_route_taco_fake_crisis": "TACO 假危机",
105+
"strategy_plugin_route_unknown_route": "未知路由",
106+
"strategy_plugin_action_no_action": "不操作",
107+
"strategy_plugin_action_watch_only": "仅观察",
108+
"strategy_plugin_action_small_taco": "小仓 TACO",
109+
"strategy_plugin_action_defend": "防守",
110+
"strategy_plugin_action_blocked": "已阻断",
111+
"strategy_plugin_action_monitor": "监控",
112+
"strategy_plugin_action_unknown_action": "未知建议",
98113
},
99114
"en": {
100115
"rebalance_title": "🔔 【Trade Execution Report】",
@@ -163,6 +178,7 @@
163178
"signal_blend_gate_overlay_capped": "{trend_symbol} stays above the {window}d gate, but overlay cap ({reasons}) cuts exposure to {allocation_text}",
164179
"blend_gate_reason_rsi_cap": "RSI over threshold",
165180
"blend_gate_reason_bollinger_cap": "price above upper band",
181+
"blend_gate_reason_volatility_delever": "{symbol} {window}d annualized volatility {volatility} is above {threshold}; redirect SOXL to {redirect_symbol}",
166182
"signal_hold": "Trend Hold",
167183
"signal_entry": "Entry Signal",
168184
"signal_reduce": "Reduce Signal",
@@ -176,6 +192,20 @@
176192
"strategy_name_tech_communication_pullback_enhancement": "Tech/Communication Pullback Enhancement",
177193
"strategy_name_qqq_tech_enhancement": "Tech/Communication Pullback Enhancement",
178194
"strategy_name_mega_cap_leader_rotation_top50_balanced": "Mega Cap Leader Rotation Top50 Balanced",
195+
"strategy_plugin_line": "🧩 Plugin: {plugin} | mode: {mode} | route: {route} | action: {action}",
196+
"strategy_plugin_name_crisis_response_shadow": "Crisis Response Shadow",
197+
"strategy_plugin_mode_shadow": "shadow",
198+
"strategy_plugin_route_no_action": "no action",
199+
"strategy_plugin_route_true_crisis": "true crisis",
200+
"strategy_plugin_route_taco_fake_crisis": "TACO fake crisis",
201+
"strategy_plugin_route_unknown_route": "unknown route",
202+
"strategy_plugin_action_no_action": "no action",
203+
"strategy_plugin_action_watch_only": "watch only",
204+
"strategy_plugin_action_small_taco": "small TACO",
205+
"strategy_plugin_action_defend": "defend",
206+
"strategy_plugin_action_blocked": "blocked",
207+
"strategy_plugin_action_monitor": "monitor",
208+
"strategy_plugin_action_unknown_action": "unknown action",
179209
},
180210
}
181211

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
flask
22
gunicorn
3-
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@573fc9e9917cf1f2c1acda9232c5a23a8a05d797
4-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@53911cbe32f6932e759522e54aa38ca5350aa44e
3+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@8769362096227320bc05c791b5244d4b3e88db50
4+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@ed55a6af0245323dbed82060e89be96d8f77f756
55
pandas
66
requests
77
pytz

0 commit comments

Comments
 (0)