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
9 changes: 7 additions & 2 deletions application/cycle_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def execute_strategy_cycle(
prices["BTCUSDT"],
btc_snapshot,
btc_target_ratio,
getattr(runtime, "strategy_display_name_localized", "") or getattr(runtime, "strategy_display_name", ""),
notifier_fn=lambda text: runtime_notify(runtime, report, text),
)

Expand Down Expand Up @@ -247,9 +248,13 @@ def run_live_cycle(
platform="binance",
deploy_target=os.getenv("LOG_DEPLOY_TARGET", "vps"),
service_name=os.getenv("SERVICE_NAME", "binance-platform"),
strategy_profile=os.getenv("STRATEGY_PROFILE", "crypto_leader_rotation"),
strategy_profile=str(getattr(runtime, "strategy_profile", "") or os.getenv("STRATEGY_PROFILE", "crypto_leader_rotation")),
run_id=str(getattr(runtime, "run_id", "") or ""),
extra_fields={"dry_run": bool(getattr(runtime, "dry_run", False))},
extra_fields={
"dry_run": bool(getattr(runtime, "dry_run", False)),
"strategy_display_name": str(getattr(runtime, "strategy_display_name", "") or ""),
"strategy_display_name_localized": str(getattr(runtime, "strategy_display_name_localized", "") or ""),
},
)
emit_runtime_log(
log_context,
Expand Down
8 changes: 7 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import sys
import traceback
from entrypoints.cli import run_cli_entrypoint
from notify_i18n_support import translate as t
from notify_i18n_support import build_strategy_display_name, translate as t
from degraded_mode_support import (
format_trend_pool_source_logs as dm_format_trend_pool_source_logs,
load_trend_universe_from_live_pool as dm_load_trend_universe_from_live_pool,
Expand Down Expand Up @@ -501,8 +501,13 @@ def maybe_send_periodic_btc_status_report(
btc_price,
btc_snapshot,
btc_target_ratio,
strategy_display_name=None,
notifier_fn=None,
):
resolved_strategy_display_name = strategy_display_name or build_strategy_display_name(t)(
getattr(STRATEGY_RUNTIME, "profile", "crypto_leader_rotation"),
fallback_name="Crypto Leader Rotation",
)
return report_maybe_send_periodic_btc_status_report(
state,
tg_token,
Expand All @@ -515,6 +520,7 @@ def maybe_send_periodic_btc_status_report(
btc_price,
btc_snapshot,
btc_target_ratio,
resolved_strategy_display_name,
translate_fn=t,
separator=SEPARATOR,
notifier_fn=notifier_fn,
Expand Down
35 changes: 30 additions & 5 deletions notify_i18n_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"firestore_write_failed": "Firestore write failed: {error}",
"telegram_send_failed": "Telegram send failed",
"heartbeat_title": "💓 【Strategy Heartbeat】",
"strategy_label": "🧭 Strategy: {name}",
"time_utc": "🕐 UTC Time",
"total_equity": "💰 Total Equity",
"trend_equity": "📈 Trend Holdings",
Expand Down Expand Up @@ -128,13 +129,15 @@
"usdt_unavailable_for_trend_buy": "USDT unavailable for trend buy",
"usdt_unavailable_for_btc_dca_buy": "USDT unavailable for BTC DCA buy",
"btc_unavailable_for_dca_sell": "BTC unavailable for DCA sell",
"strategy_name_crypto_leader_rotation": "Crypto Leader Rotation",
},
"zh": {
"telegram_prefix": "🤖 加密量化助手",
"firestore_get_state_failed": "Firestore 读取状态失败: {error}",
"firestore_write_failed": "Firestore 写入状态失败: {error}",
"telegram_send_failed": "Telegram 发送失败",
"heartbeat_title": "💓 【策略心跳】",
"strategy_label": "🧭 策略: {name}",
"time_utc": "🕐 UTC 时间",
"total_equity": "💰 总净值",
"trend_equity": "📈 趋势层持仓",
Expand Down Expand Up @@ -250,6 +253,7 @@
"usdt_unavailable_for_trend_buy": "USDT 不足,无法执行趋势买入",
"usdt_unavailable_for_btc_dca_buy": "USDT 不足,无法执行 BTC 定投买入",
"btc_unavailable_for_dca_sell": "BTC 不足,无法执行定投止盈卖出",
"strategy_name_crypto_leader_rotation": "加密领涨轮动",
},
}

Expand All @@ -261,12 +265,33 @@ def get_notify_lang() -> str:
return DEFAULT_NOTIFY_LANG


def build_translator(lang: str):
def translator(key: str, **kwargs) -> str:
active_lang = lang if lang in SUPPORTED_NOTIFY_LANGS else DEFAULT_NOTIFY_LANG
template = _TEXTS.get(active_lang, _TEXTS[DEFAULT_NOTIFY_LANG]).get(key)
if template is None:
template = _TEXTS[DEFAULT_NOTIFY_LANG].get(key, key)
return template.format(**kwargs) if kwargs else template

return translator


def translate(key: str, **kwargs) -> str:
lang = get_notify_lang()
template = _TEXTS.get(lang, _TEXTS[DEFAULT_NOTIFY_LANG]).get(key)
if template is None:
template = _TEXTS[DEFAULT_NOTIFY_LANG].get(key, key)
return template.format(**kwargs) if kwargs else template
return build_translator(get_notify_lang())(key, **kwargs)


def build_strategy_display_name(translate_fn):
def strategy_display_name(profile: str, *, fallback_name: str | None = None) -> str:
key = f"strategy_name_{str(profile or '').strip()}"
translated = translate_fn(key)
if translated != key:
return translated
fallback = str(fallback_name or "").strip()
if fallback:
return fallback
return str(profile or "").strip()

return strategy_display_name


def build_telegram_message(text: str) -> str:
Expand Down
2 changes: 2 additions & 0 deletions reporting/status_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def maybe_send_periodic_btc_status_report(
btc_price,
btc_snapshot,
btc_target_ratio,
strategy_display_name,
*,
translate_fn,
separator,
Expand All @@ -51,6 +52,7 @@ def maybe_send_periodic_btc_status_report(
gate_text = translate_fn("gate_on") if btc_snapshot["regime_on"] else translate_fn("gate_off")
text = (
f"{translate_fn('heartbeat_title')}\n"
f"{translate_fn('strategy_label', name=strategy_display_name)}\n"
f"{translate_fn('time_utc')}: {now_utc.strftime('%Y-%m-%d %H:%M')}\n"
f"{separator}\n"
f"{translate_fn('total_equity')}: ${total_equity:.2f}\n"
Expand Down
20 changes: 20 additions & 0 deletions runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
from datetime import datetime, timezone
from typing import Any, Callable

from notify_i18n_support import build_strategy_display_name, build_translator, get_notify_lang
from runtime_support import ExecutionRuntime
from strategy_registry import (
BINANCE_PLATFORM,
resolve_strategy_definition,
resolve_strategy_metadata,
)


Expand Down Expand Up @@ -38,21 +40,34 @@ class CycleExecutionSettings:
btc_status_report_interval_hours: int
allow_new_trend_entries_on_degraded: bool
strategy_profile: str
strategy_display_name: str
strategy_display_name_localized: str
strategy_domain: str


def load_cycle_execution_settings() -> CycleExecutionSettings:
notify_lang = get_notify_lang()
strategy_definition = resolve_strategy_definition(
os.getenv("STRATEGY_PROFILE"),
platform_id=BINANCE_PLATFORM,
)
strategy_metadata = resolve_strategy_metadata(
strategy_definition.profile,
platform_id=BINANCE_PLATFORM,
)
strategy_display_name_localized = build_strategy_display_name(build_translator(notify_lang))(
strategy_definition.profile,
fallback_name=strategy_metadata.display_name,
)
return CycleExecutionSettings(
btc_status_report_interval_hours=max(1, min(24, get_env_int("BTC_STATUS_REPORT_INTERVAL_HOURS", 24))),
allow_new_trend_entries_on_degraded=get_env_bool(
"TREND_POOL_ALLOW_NEW_ENTRIES_ON_DEGRADED",
False,
),
strategy_profile=strategy_definition.profile,
strategy_display_name=strategy_metadata.display_name,
strategy_display_name_localized=strategy_display_name_localized,
strategy_domain=strategy_definition.domain,
)

Expand All @@ -65,9 +80,14 @@ def build_live_runtime(
notifier: Callable[..., Any] | None = None,
) -> ExecutionRuntime:
runtime_now = now_utc or datetime.now(timezone.utc)
cycle_settings = load_cycle_execution_settings()
return ExecutionRuntime(
dry_run=False,
now_utc=runtime_now,
strategy_profile=cycle_settings.strategy_profile,
strategy_domain=cycle_settings.strategy_domain,
strategy_display_name=cycle_settings.strategy_display_name,
strategy_display_name_localized=cycle_settings.strategy_display_name_localized,
api_key=os.getenv("BINANCE_API_KEY", ""),
api_secret=os.getenv("BINANCE_API_SECRET", ""),
tg_token=os.getenv("TG_TOKEN", ""),
Expand Down
12 changes: 10 additions & 2 deletions runtime_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class ExecutionRuntime:
dry_run: bool = False
run_id: str = ""
now_utc: Optional[datetime] = None
strategy_profile: str = ""
strategy_domain: str = ""
strategy_display_name: str = ""
strategy_display_name_localized: str = ""
client: Any = None
api_key: str = ""
api_secret: str = ""
Expand Down Expand Up @@ -38,8 +42,8 @@ def build_execution_report(runtime):
platform="binance",
deploy_target=os.getenv("LOG_DEPLOY_TARGET", "vps"),
service_name=os.getenv("SERVICE_NAME", "binance-platform"),
strategy_profile=os.getenv("STRATEGY_PROFILE", "crypto_leader_rotation"),
strategy_domain=os.getenv("STRATEGY_DOMAIN", "crypto"),
strategy_profile=str(runtime.strategy_profile or os.getenv("STRATEGY_PROFILE", "crypto_leader_rotation")),
strategy_domain=str(runtime.strategy_domain or os.getenv("STRATEGY_DOMAIN", "crypto")),
run_id=str(runtime.run_id),
run_source="github_actions" if os.getenv("GITHUB_RUN_ID") or os.getenv("GITHUB_ACTIONS") else "runtime",
dry_run=bool(runtime.dry_run),
Expand Down Expand Up @@ -74,6 +78,10 @@ def build_execution_report(runtime):
"circuit_breaker_triggered": False,
"degraded_mode_level": None,
"upstream_pool_symbols": [],
"summary": {
"strategy_display_name": str(runtime.strategy_display_name or ""),
"strategy_display_name_localized": str(runtime.strategy_display_name_localized or ""),
},
})
return report

Expand Down
10 changes: 10 additions & 0 deletions strategy_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
PlatformStrategyPolicy,
StrategyDefinition,
build_platform_profile_matrix,
get_catalog_strategy_metadata,
get_enabled_profiles_for_platform,
resolve_platform_strategy_definition,
)
Expand Down Expand Up @@ -52,3 +53,12 @@ def resolve_strategy_definition(
strategy_catalog=STRATEGY_CATALOG,
policy=PLATFORM_POLICY,
)


def resolve_strategy_metadata(
raw_value: str | None,
*,
platform_id: str,
):
definition = resolve_strategy_definition(raw_value, platform_id=platform_id)
return get_catalog_strategy_metadata(STRATEGY_CATALOG, definition.profile)
9 changes: 8 additions & 1 deletion strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from quant_platform_kit.strategy_contracts import StrategyContext, StrategyDecision, StrategyEntrypoint

from strategy_loader import load_strategy_entrypoint_for_profile
from strategy_registry import BINANCE_PLATFORM, resolve_strategy_metadata
from trend_pool_support import get_default_live_pool_candidates as tp_get_default_live_pool_candidates


Expand Down Expand Up @@ -115,7 +116,13 @@ def evaluate(
return StrategyEvaluationResult(
decision=decision,
account_metrics=dict(account_metrics),
metadata={"strategy_profile": self.profile},
metadata={
"strategy_profile": self.profile,
"strategy_display_name": resolve_strategy_metadata(
self.profile,
platform_id=BINANCE_PLATFORM,
).display_name,
},
)


Expand Down
10 changes: 9 additions & 1 deletion tests/test_cycle_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,13 @@ def test_run_live_cycle_emits_structured_runtime_events(self):
clear=False,
):
report, _output_path = run_live_cycle(
runtime_builder=lambda: SimpleNamespace(run_id="run-001", dry_run=True),
runtime_builder=lambda: SimpleNamespace(
run_id="run-001",
dry_run=True,
strategy_profile="crypto_leader_rotation",
strategy_display_name="Crypto Leader Rotation",
strategy_display_name_localized="加密领涨轮动",
),
execute_cycle=lambda _runtime: {
"status": "ok",
"log_lines": ["line-1", "line-2"],
Expand All @@ -96,6 +102,8 @@ def test_run_live_cycle_emits_structured_runtime_events(self):
end_log = json.loads(observed["printed"][2])
self.assertEqual(start_log["event"], "strategy_cycle_started")
self.assertEqual(start_log["strategy_profile"], "crypto_leader_rotation")
self.assertEqual(start_log["strategy_display_name"], "Crypto Leader Rotation")
self.assertEqual(start_log["strategy_display_name_localized"], "加密领涨轮动")
self.assertEqual(start_log["run_id"], "run-001")
self.assertEqual(end_log["event"], "strategy_cycle_completed")
self.assertEqual(end_log["status"], "ok")
Expand Down
4 changes: 4 additions & 0 deletions tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def test_load_cycle_execution_settings_clamps_interval_and_reads_degraded_flag(s
self.assertEqual(settings.btc_status_report_interval_hours, 24)
self.assertTrue(settings.allow_new_trend_entries_on_degraded)
self.assertEqual(settings.strategy_profile, DEFAULT_STRATEGY_PROFILE)
self.assertEqual(settings.strategy_display_name, "Crypto Leader Rotation")
self.assertEqual(settings.strategy_display_name_localized, "Crypto Leader Rotation")
self.assertEqual(settings.strategy_domain, CRYPTO_DOMAIN)

def test_load_cycle_execution_settings_rejects_unknown_strategy_profile(self):
Expand Down Expand Up @@ -73,6 +75,8 @@ def test_build_live_runtime_reads_env_and_preserves_injected_hooks(self):
self.assertEqual(runtime.api_secret, "api-secret")
self.assertEqual(runtime.tg_token, "tg-token")
self.assertEqual(runtime.tg_chat_id, "chat-id")
self.assertEqual(runtime.strategy_profile, DEFAULT_STRATEGY_PROFILE)
self.assertEqual(runtime.strategy_display_name, "Crypto Leader Rotation")
self.assertIs(runtime.state_loader, state_loader)
self.assertIs(runtime.state_writer, state_writer)
self.assertIs(runtime.notifier, notifier)
Expand Down
48 changes: 48 additions & 0 deletions tests/test_status_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
append_rotation_summary,
build_btc_manual_hint,
get_periodic_report_bucket,
maybe_send_periodic_btc_status_report,
)


Expand Down Expand Up @@ -86,5 +87,52 @@ def test_append_rotation_summary_formats_expected_lines(self):
)


def test_periodic_status_report_includes_strategy_display_name(self):
state = {}
observed = []

def translate_strategy(key, **kwargs):
mapping = {
"heartbeat_title": "heartbeat",
"strategy_label": "strategy={name}",
"time_utc": "time",
"total_equity": "equity",
"trend_equity": "trend",
"btc_price": "btc",
"ahr999": "ahr",
"zscore": "z",
"zscore_threshold": "threshold",
"btc_target": "target",
"btc_gate": "gate",
"gate_on": "on",
"gate_off": "off",
"manual_hint": "hint",
"manual_hint_low_value": "low",
}
template = mapping[key]
return template.format(**kwargs) if kwargs else template

maybe_send_periodic_btc_status_report(
state,
"token",
"chat-id",
SimpleNamespace(hour=8, strftime=lambda fmt: "2026-03-29 08:00" if "%H:%M" in fmt else "20260329"),
4,
1000.0,
250.0,
0.01,
65000.0,
{"ahr999": 0.7, "zscore": 1.2, "sell_trigger": 3.0, "regime_on": True},
0.3,
"加密领涨轮动",
translate_fn=translate_strategy,
separator="---",
notifier_fn=observed.append,
)

self.assertTrue(observed)
self.assertIn("strategy=加密领涨轮动", observed[0])


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions tests/test_strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def test_strategy_runtime_evaluate_returns_decision_with_buy_sell_diagnostics(se
)

diagnostics = evaluation.decision.diagnostics
self.assertEqual(evaluation.metadata["strategy_display_name"], "Crypto Leader Rotation")
self.assertIn("planned_trend_buys", diagnostics)
self.assertIn("eligible_buy_symbols", diagnostics)
self.assertIn("sell_reasons", diagnostics)
Expand Down