d6c802ef26
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>
98 lines
3.2 KiB
Python
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)
|