Pre-launch hardening: KOL module, Telegram, scanners, WS resilience
Big-picture changes since b941223:
KOL pipeline (new) — Substack/podcast/blog RSS → AI ticker extraction →
on-chain wallet diff → talks-vs-trades divergence detection. Daily polls,
19 feeds, divergence emits Post + Telegram fan-out.
Telegram push (new) — walletless free tier + wallet-linked Pro upgrade,
in-bot preference commands (/trump /btc /funding /kol /conf /quiet),
signed-envelope API for dashboard. Disconnect-wallet keeps free
subscription.
BTC funding-rate reversal scanner (new) — hourly cron, 30d cumulative
funding threshold + mean-revert + 7d price confirm, emits via
/api/signals/ingest. BTC bottom-reversal scanner promoted to System 2.
WS broadcast rewrite — per-client send timeout + parallel fan-out
(asyncio.gather). Fixes "Binance WS no close frame" reconnect storms +
APScheduler 11-min job misses, both caused by one slow client stalling
the kline loop.
Error visibility — three silent-error sites (trumpstruth/truth_social
fetchers, funding_reversal scanner) now include exception type name so
httpx ConnectError-style empty-message errors stop logging blank lines.
Telegram bot loop now classifies ReadTimeout vs network vs unknown +
logger.exception for the unknown bucket.
Security hygiene — trumpsignal.db untracked from git (held subscriber
wallets + encrypted HL keys + 22 bot trades); .gitignore now blocks
*.db/.next/backups. CORS only allows FRONTEND_URL in production.
New ops scripts —
- scripts/preflight.py: env/DB/Telegram/AI auth verification gate
- scripts/backup_db.sh: cron-friendly daily DB backup (SQLite + Postgres)
- scripts/seed_kol_wallets.py: idempotent KOL on-chain wallet seeder
15 new Alembic migrations (007-021) covering convex strategy fields,
phase-1 safety, two-system frozen exits, invalidation prices, dynamic
SYS2 leverage, staged de-risk + pyramiding, peak gain tracking, risk
mode, auto-trade + grow flags, KOL module, KOL on-chain, KOL divergence,
Telegram bindings + walletless.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -301,3 +301,77 @@ class HyperliquidTrader:
|
||||
fill_price = float(filled_info.get("avgPx", mid) or mid)
|
||||
logger.info("Closed %s position @ %.2f (size=%.5f)", coin, fill_price, total_sz)
|
||||
return {"fill_price": fill_price, "already_closed": False}
|
||||
|
||||
async def reduce_position(self, asset: str, fraction: float) -> dict:
|
||||
"""Partially close `fraction` (0<f<1) of the CURRENT open position
|
||||
for `asset` with a reduce-only IOC order. Used by System-2 staged
|
||||
de-risk. Returns {"fill_price": float, "closed_fraction": float,
|
||||
"already_closed": bool}.
|
||||
|
||||
`closed_fraction` is the fraction of the position that was actually
|
||||
closed (filled_size / pre-existing size) so the caller's PnL +
|
||||
remaining bookkeeping stays exact even on partial fills.
|
||||
"""
|
||||
coin = asset.upper()
|
||||
f = max(0.0, min(1.0, float(fraction)))
|
||||
if f <= 0.0:
|
||||
return {"fill_price": None, "closed_fraction": 0.0, "already_closed": False}
|
||||
if f >= 1.0:
|
||||
r = await self.close_position(asset)
|
||||
r["closed_fraction"] = 0.0 if r.get("already_closed") else 1.0
|
||||
return r
|
||||
|
||||
positions = await self.get_open_positions()
|
||||
target = next((p for p in positions if p.get("coin") == coin), None)
|
||||
if target is None:
|
||||
logger.warning("reduce_position: no open %s on HL — already closed?", coin)
|
||||
return {"fill_price": None, "closed_fraction": 0.0, "already_closed": True}
|
||||
|
||||
szi = float(target["szi"])
|
||||
is_buy = szi < 0 # reducing a short → buy back
|
||||
pre_size = abs(szi)
|
||||
sz_dec = await self._get_sz_decimals(coin)
|
||||
size = round(pre_size * f, sz_dec)
|
||||
if size <= 0:
|
||||
# Fraction rounds to nothing at this asset's size precision —
|
||||
# treat as a no-op so the caller can advance the step without
|
||||
# an erroneous PnL slice.
|
||||
logger.warning("reduce_position: %s fraction %.3f rounds to 0 size (pre=%.6f)",
|
||||
coin, f, pre_size)
|
||||
return {"fill_price": None, "closed_fraction": 0.0, "already_closed": False}
|
||||
|
||||
mid = await self._mid_price(coin)
|
||||
slippage = 0.01
|
||||
raw_px = mid * (1 + slippage) if is_buy else mid * (1 - slippage)
|
||||
if raw_px >= 10000:
|
||||
limit_px = float(round(raw_px))
|
||||
elif raw_px >= 1000:
|
||||
limit_px = round(raw_px, 1)
|
||||
elif raw_px >= 100:
|
||||
limit_px = round(raw_px, 2)
|
||||
else:
|
||||
limit_px = round(raw_px, 3)
|
||||
|
||||
logger.info("Reducing %s by %.0f%%: size=%.6f @ limit %.2f (pre=%.6f)",
|
||||
coin, f * 100, size, limit_px, pre_size)
|
||||
|
||||
result = await self._run(
|
||||
self._exchange.order,
|
||||
coin, is_buy, size, limit_px,
|
||||
{"limit": {"tif": "Ioc"}},
|
||||
reduce_only=True,
|
||||
)
|
||||
statuses = result.get("response", {}).get("data", {}).get("statuses", [{}])
|
||||
status = statuses[0] if statuses else {}
|
||||
if "error" in status:
|
||||
raise ValueError(f"HL reduce rejected: {status['error']}")
|
||||
filled_info = status.get("filled") or {}
|
||||
total_sz = float(filled_info.get("totalSz", 0) or 0)
|
||||
if total_sz <= 0:
|
||||
raise ValueError(f"reduce_position {coin}: IOC returned 0 fill")
|
||||
fill_price = float(filled_info.get("avgPx", mid) or mid)
|
||||
closed_fraction = (total_sz / pre_size) if pre_size > 0 else 0.0
|
||||
logger.info("Reduced %s: closed %.6f / %.6f (%.1f%%) @ %.2f",
|
||||
coin, total_sz, pre_size, closed_fraction * 100, fill_price)
|
||||
return {"fill_price": fill_price, "closed_fraction": closed_fraction,
|
||||
"already_closed": False}
|
||||
|
||||
Reference in New Issue
Block a user