Files
trumpsignal-backend/tests/test_macro_fetchers_timing.py
k d6c802ef26 fix: pre-launch hardening — HYPE price feed, KOL wallet cleanup, Telegram Trump alert, rate limiting, brittle test
Batch of the pre-launch audit campaign (BUG-01…14 plus three new features):

Pricing / TP-SL protection
- Add app/services/hl_price_feed.py: supplemental HL allMids poller for
  HL-native assets (HYPE, PURR) not listed on Binance. Pumps price_store +
  tp_sl_monitor.on_price_tick so bot trades on these assets keep full
  stop-loss / take-profit / trailing protection instead of max-hold only.
- Wire feed into main.py lifespan (startup task + graceful shutdown cancel).

Telegram
- Add format_trump_mention + PATH B in _dispatch: crypto-relevant Trump
  posts with no directional signal (relevant=True, signal=hold) now alert
  the public channel only (no per-subscriber noise).
- Rate limiter (slowapi) on the API; assorted bot/digest fixes.

KOL on-chain
- seed_kol_wallets.py: KOL_FEEDS coverage cross-check; reversibly deactivate
  orphaned wallets (handle not in KOL_FEEDS → can never produce divergence)
  so the scanner stops burning cycles on them.

Tests / misc
- Fix brittle test_macro_ahr999_uses_same_formula_as_scanner: mock now uses
  realistic ms timestamps so the in-progress-day drop fires, matching the
  fetcher's bar count (was 0.3179 vs 0.3178 off-by-one).
- Refresh stale notify_signal comment in truth_social.py.

Frontend reduce-action type fix lives in the sibling repo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 11:57:19 +08:00

98 lines
3.2 KiB
Python

from datetime import datetime, timezone
from app.services.macro.fetchers import (
_drop_in_progress_daily_klines,
_latest_closed_daily_point,
_parse_farside_latest_total,
)
from app.services.bottom_indicators import ahr999 as scanner_ahr999
from app.services.macro import fetchers
def test_drop_in_progress_daily_klines_removes_today_open_bar():
now = datetime(2026, 5, 25, 8, 0, tzinfo=timezone.utc)
rows = [
[1779494400000, 0, 0, 0, "76715.20"],
[1779580800000, 0, 0, 0, "77030.30"],
[1779667200000, 0, 0, 0, "77404.80"], # 2026-05-25 00:00 UTC, still open
]
filtered = _drop_in_progress_daily_klines(rows, now=now)
assert [row[0] for row in filtered] == [1779494400000, 1779580800000]
def test_latest_closed_daily_point_skips_today_point():
now = datetime(2026, 5, 25, 8, 0, tzinfo=timezone.utc)
rows = [
{"timestamp": 1779494400000, "sumOpenInterestValue": "1"},
{"timestamp": 1779580800000, "sumOpenInterestValue": "2"},
{"timestamp": 1779667200000, "sumOpenInterestValue": "3"},
]
latest = _latest_closed_daily_point(rows, now=now)
assert latest == rows[1]
def test_parse_farside_latest_total_uses_newest_date_not_first_row():
html = """
<table>
<tbody>
<tr><td>11 Jan 2024</td><td>0.0</td><td>655.3</td></tr>
<tr><td>24 May 2026</td><td>0.0</td><td>(12.5)</td></tr>
<tr><td>25 May 2026</td><td>0.0</td><td>321.0</td></tr>
</tbody>
</table>
"""
parsed = _parse_farside_latest_total(html)
assert parsed["value"] == 321_000_000.0
assert parsed["raw"]["date"] == "25 May 2026"
def test_macro_ahr999_uses_same_formula_as_scanner(monkeypatch):
# Build 300 daily bars with realistic OPEN timestamps so the fetcher's
# in-progress-day drop (_drop_in_progress_daily_klines) actually fires on
# the last bar (today's 00:00 UTC). With fake integer timestamps the drop
# is a no-op, so the fetcher would compute AHR999 over one MORE bar than
# the scanner below — producing a spurious 4th-decimal mismatch (the
# fetcher and scanner share the exact same ahr999() function, so identical
# inputs must yield identical output).
now = datetime.now(timezone.utc)
midnight_ms = int(
now.replace(hour=0, minute=0, second=0, microsecond=0).timestamp() * 1000
)
day_ms = 86_400_000
# Row i opens at (299 - i) days before today's midnight; the last row
# (i=299) opens at today's midnight → still in progress → dropped.
closes = [
[midnight_ms - (299 - i) * day_ms, 0, 0, 0, str(50_000 + i)]
for i in range(300)
]
class _Resp:
def raise_for_status(self):
return None
def json(self):
return closes
class _Client:
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return None
async def get(self, *_args, **_kwargs):
return _Resp()
monkeypatch.setattr(fetchers.httpx, "AsyncClient", lambda *a, **kw: _Client())
result = __import__("asyncio").run(fetchers.fetch_ahr999())
expected = scanner_ahr999([float(r[4]) for r in closes[:-1]])
assert result["value"] == round(expected or 0, 4)