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
33 changes: 31 additions & 2 deletions infra/binance_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,39 @@
from __future__ import annotations


def resolve_runtime_btc_snapshot(runtime, btc_price, log_buffer, *, fetch_btc_market_snapshot_fn):
def resolve_runtime_btc_snapshot(
runtime,
btc_price,
log_buffer,
*,
fetch_btc_market_snapshot_fn,
max_attempts=1,
retry_delays=(),
sleep_fn=None,
append_log_fn=None,
retry_log_message_fn=None,
):
if runtime.btc_market_snapshot is not None:
return dict(runtime.btc_market_snapshot)
return fetch_btc_market_snapshot_fn(runtime.client, btc_price, log_buffer=log_buffer)

attempts = max(1, int(max_attempts))
delays = tuple(retry_delays or ())
for attempt in range(1, attempts + 1):
snapshot = fetch_btc_market_snapshot_fn(runtime.client, btc_price, log_buffer=log_buffer)
if snapshot is not None:
return snapshot
if attempt >= attempts:
return None

delay_seconds = 0
if delays:
delay_seconds = max(0, delays[min(attempt - 1, len(delays) - 1)])
if append_log_fn is not None and retry_log_message_fn is not None:
append_log_fn(log_buffer, retry_log_message_fn(attempt + 1, attempts, delay_seconds))
if sleep_fn is not None and delay_seconds > 0:
sleep_fn(delay_seconds)

return None


def resolve_runtime_trend_indicators(runtime, trend_universe_symbols, *, fetch_daily_indicators_fn):
Expand Down
11 changes: 11 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
TREND_POOL_ACTION_HISTORY_KEY = "trend_action_history"
DEFAULT_TREND_POOL_MAX_AGE_DAYS = int(STRATEGY_RUNTIME.artifact_contract["max_age_days"])
DEFAULT_TREND_POOL_ACCEPTABLE_MODES = tuple(STRATEGY_RUNTIME.artifact_contract["acceptable_modes"])
BTC_MARKET_SNAPSHOT_RETRY_DELAYS = (5, 15)


class BalanceFetchError(RuntimeError):
Expand Down Expand Up @@ -594,6 +595,16 @@ def resolve_runtime_btc_snapshot(runtime, btc_price, log_buffer):
btc_price,
log_buffer,
fetch_btc_market_snapshot_fn=fetch_btc_market_snapshot,
max_attempts=max(1, min(5, get_env_int("BTC_MARKET_SNAPSHOT_MAX_ATTEMPTS", 3))),
retry_delays=BTC_MARKET_SNAPSHOT_RETRY_DELAYS,
sleep_fn=time.sleep,
append_log_fn=append_log,
retry_log_message_fn=lambda attempt, max_attempts, delay_seconds: t(
"btc_daily_retrying",
attempt=attempt,
max_attempts=max_attempts,
delay_seconds=delay_seconds,
),
)


Expand Down
2 changes: 2 additions & 0 deletions notify_i18n_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"btc_daily_fetch_failed": "⚠️ BTC daily fetch failed: {error}",
"btc_daily_data_empty": "⚠️ BTC daily data empty.",
"btc_data_insufficient": "⚠️ BTC data insufficient for MA200/Z-Score. len={length}, last_time={last_time}",
"btc_daily_retrying": "Retrying BTC daily fetch ({attempt}/{max_attempts}) after {delay_seconds:.0f}s.",
"asset_unavailable_for_circuit_breaker_sell": "{asset} unavailable for circuit-breaker sell",
"asset_unavailable_for_trend_sell": "{asset} unavailable for trend sell",
"usdt_unavailable_for_trend_buy": "USDT unavailable for trend buy",
Expand Down Expand Up @@ -249,6 +250,7 @@
"btc_daily_fetch_failed": "⚠️ 拉取 BTC 日线失败: {error}",
"btc_daily_data_empty": "⚠️ BTC 日线数据为空。",
"btc_data_insufficient": "⚠️ BTC 数据不足,无法计算 MA200/Z-Score。len={length}, last_time={last_time}",
"btc_daily_retrying": "正在重试拉取 BTC 日线({attempt}/{max_attempts}),等待 {delay_seconds:.0f} 秒。",
"asset_unavailable_for_circuit_breaker_sell": "{asset} 不足,无法执行熔断卖出",
"asset_unavailable_for_trend_sell": "{asset} 不足,无法执行趋势卖出",
"usdt_unavailable_for_trend_buy": "USDT 不足,无法执行趋势买入",
Expand Down
52 changes: 52 additions & 0 deletions tests/test_binance_runtime_infra.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,58 @@ def test_resolve_runtime_btc_snapshot_prefers_injected_snapshot(self):
self.assertEqual(snapshot, {"ahr999": 0.8})
self.assertIsNot(snapshot, runtime.btc_market_snapshot)

def test_resolve_runtime_btc_snapshot_retries_before_success(self):
runtime = SimpleNamespace(client=object(), btc_market_snapshot=None)
log_buffer = []
observed = {"calls": 0, "sleeps": []}

def fetch_snapshot(_client, _btc_price, log_buffer=None):
observed["calls"] += 1
if observed["calls"] < 3:
return None
return {"ahr999": 0.8}

snapshot = resolve_runtime_btc_snapshot(
runtime,
50_000.0,
log_buffer,
fetch_btc_market_snapshot_fn=fetch_snapshot,
max_attempts=3,
retry_delays=(1, 2),
sleep_fn=lambda seconds: observed["sleeps"].append(seconds),
append_log_fn=lambda buffer, message: buffer.append(message),
retry_log_message_fn=lambda attempt, max_attempts, delay_seconds: (
f"retry {attempt}/{max_attempts} after {delay_seconds}s"
),
)

self.assertEqual(snapshot, {"ahr999": 0.8})
self.assertEqual(observed["calls"], 3)
self.assertEqual(observed["sleeps"], [1, 2])
self.assertEqual(log_buffer, ["retry 2/3 after 1s", "retry 3/3 after 2s"])

def test_resolve_runtime_btc_snapshot_returns_none_after_retries(self):
runtime = SimpleNamespace(client=object(), btc_market_snapshot=None)
observed = {"calls": 0, "sleeps": []}

def fetch_missing_snapshot(*_args, **_kwargs):
observed["calls"] += 1
return None

snapshot = resolve_runtime_btc_snapshot(
runtime,
50_000.0,
[],
fetch_btc_market_snapshot_fn=fetch_missing_snapshot,
max_attempts=2,
retry_delays=(1,),
sleep_fn=lambda seconds: observed["sleeps"].append(seconds),
)

self.assertIsNone(snapshot)
self.assertEqual(observed["calls"], 2)
self.assertEqual(observed["sleeps"], [1])

def test_resolve_runtime_trend_indicators_fetches_when_not_injected(self):
runtime = SimpleNamespace(client=object(), trend_indicator_snapshots=None)
observed_symbols = []
Expand Down