KOL feeds: fix dead/blocked sources, drop stale feeds (29→25)

Feed-health pass over KOL_FEEDS:
- raoulpal: stale Substack (last 2024-05) → Real Vision podcast feed
- dampedspring: paywalled (0 entries) → free "Damped Spring 101" Substack
- unchained: Cloudflare 403 → canonical Megaphone podcast feed
- lynalden: Cloudflare 202 → FeedBurner mirror
- glassnode: recovered via httpx http2=True (was 403 on HTTP/1.1)
- browser User-Agent + Accept headers on feed fetch
- removed dead feeds with no active replacement: placeholder,
  dragonfly, niccarter, eugene
- pin h2==4.3.0 (required by http2=True)

All 25 remaining feeds verified fetching real body content; newest
post per feed ≤88d. Bundles in-flight KOL-module work already in the
working tree (kol_x ingest, migration 027, tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
k
2026-06-09 22:55:16 +08:00
parent 213bb911e3
commit 54884f3e24
38 changed files with 2340 additions and 322 deletions
+150
View File
@@ -0,0 +1,150 @@
"""Tests for trade-alert broadcasts and balance pre-check logic added 2026-06-01.
Coverage targets:
1. _broadcast_trade_alert — fire-and-forget, must not raise
2. Balance pre-check maths — required_margin = (notional / leverage) * 1.1
3. Startup drain in the Telegram bot loop — offset advances past pending updates
"""
from __future__ import annotations
import asyncio
import pytest
# ── 1. _broadcast_trade_alert ─────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_broadcast_trade_alert_no_connections(monkeypatch):
"""broadcast_trade_alert must silently succeed when no WS clients are connected."""
from app.services.bot_engine import _broadcast_trade_alert
from app.ws import manager as mgr_mod
# Patch manager.broadcast to verify it's called with correct payload
calls: list[dict] = []
async def fake_broadcast(msg: dict):
calls.append(msg)
monkeypatch.setattr(mgr_mod.manager, "broadcast", fake_broadcast)
await _broadcast_trade_alert("0xabc", "execution_failed", asset="BTC", reason="test error")
assert len(calls) == 1
assert calls[0]["type"] == "trade_alert"
assert calls[0]["wallet"] == "0xabc"
assert calls[0]["event"] == "execution_failed"
assert calls[0]["asset"] == "BTC"
@pytest.mark.asyncio
async def test_broadcast_trade_alert_swallows_exceptions(monkeypatch):
"""broadcast_trade_alert must not propagate exceptions — trade flow must continue."""
from app.services.bot_engine import _broadcast_trade_alert
from app.ws import manager as mgr_mod
async def exploding_broadcast(msg: dict):
raise RuntimeError("WS layer crashed")
monkeypatch.setattr(mgr_mod.manager, "broadcast", exploding_broadcast)
# Must not raise
await _broadcast_trade_alert("0xabc", "budget_reached", asset="BTC")
# ── 2. Balance pre-check maths ────────────────────────────────────────────────
def test_required_margin_formula():
"""required_margin = (notional / leverage) * 1.1 — verify key scenarios."""
def required_margin(notional: float, leverage: int) -> float:
return round((notional / max(leverage, 1)) * 1.1, 2)
# 2× leverage, $100 position → $50 margin + 10% buffer = $55
assert required_margin(100, 2) == 55.0
# 5× leverage, $500 position → $100 margin + 10% = $110
assert required_margin(500, 5) == 110.0
# 1× leverage, $20 position → $20 + 10% = $22
assert required_margin(20, 1) == 22.0
# edge: leverage=0 treated as 1 (no divide-by-zero)
assert required_margin(100, 0) == 110.0
def test_balance_check_does_not_block_low_leverage():
"""With $100 balance and 10× leverage on a $200 notional, margin = $22 — should PASS."""
balance = 100.0
notional = 200.0
leverage = 10
required = (notional / max(leverage, 1)) * 1.1
assert balance >= required, (
f"Balance ${balance} should cover ${required:.2f} margin "
f"(${notional} notional at {leverage}×)"
)
def test_balance_check_fires_at_truly_insufficient_balance():
"""With $5 balance and 2× on $20 notional, margin = $11 — should BLOCK."""
balance = 5.0
notional = 20.0
leverage = 2
required = (notional / max(leverage, 1)) * 1.1
assert balance < required, (
f"Balance ${balance} should NOT cover ${required:.2f} margin"
)
# ── 3. Telegram startup drain ────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_telegram_startup_drain_advances_offset(monkeypatch):
"""Verify the drain calls getUpdates with timeout=0 and ACKs the last update_id."""
import httpx
drain_calls: list[dict] = []
class FakeResponse:
status_code = 200
def json(self):
if len(drain_calls) == 1: # first call: return pending updates
return {"result": [{"update_id": 100}, {"update_id": 101}]}
return {"result": []} # second call (ACK): no pending
class FakeClient:
async def __aenter__(self): return self
async def __aexit__(self, *a): pass
async def get(self, url, params=None):
drain_calls.append(params or {})
return FakeResponse()
monkeypatch.setattr(httpx, "AsyncClient", lambda **kw: FakeClient())
# Simulate only the drain portion (extract the logic)
from app.config import settings
monkeypatch.setattr(settings, "telegram_bot_token", "test-token")
import httpx as _httpx
# Re-run the drain logic inline (mirrors telegram_bot.py startup drain)
token = "test-token"
TG_API = "https://api.telegram.org/bot{token}/{method}"
async with _httpx.AsyncClient(timeout=10) as client:
r = await client.get(
TG_API.format(token=token, method="getUpdates"),
params={"timeout": 0, "limit": 100},
)
if r.status_code == 200:
pending = r.json().get("result", [])
if pending:
drain_offset = pending[-1]["update_id"] + 1
async with _httpx.AsyncClient(timeout=10) as client:
await client.get(
TG_API.format(token=token, method="getUpdates"),
params={"timeout": 0, "offset": drain_offset},
)
# First call: drain request (timeout=0, limit=100)
assert drain_calls[0].get("timeout") == 0
assert drain_calls[0].get("limit") == 100
# Second call: ACK with offset = last_update_id + 1
assert drain_calls[1].get("offset") == 102 # 101 + 1