feat: add daily budget, active window, trade snapshots, and price impact monitor

- New migrations for daily_budget, active_window, and bottrade snapshot
- Add trumpstruth scraper and price_impact_monitor service
- Expand bot_engine, hyperliquid, recovery, and tp_sl_monitor logic
- Update API/schemas/models for new features

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
k
2026-04-25 16:04:49 +08:00
parent a2c68e2939
commit 4ffcb442fe
20 changed files with 1110 additions and 114 deletions
+44 -16
View File
@@ -111,7 +111,7 @@ class HyperliquidTrader:
return positions
except Exception as exc:
logger.error("get_open_positions error: %s", exc)
return []
raise
async def set_leverage(self, coin: str) -> None:
"""Set isolated leverage for the given coin."""
@@ -180,14 +180,30 @@ class HyperliquidTrader:
)
statuses = result.get("response", {}).get("data", {}).get("statuses", [{}])
filled = statuses[0] if statuses else {}
fill_price = float(
(filled.get("filled") or {}).get("avgPx", mid) or mid
)
order_id = str((filled.get("resting") or {}).get("oid", ""))
status = statuses[0] if statuses else {}
logger.info("Opened %s %s @ %.2f (order_id=%s)", side, coin, fill_price, order_id)
return {"order_id": order_id, "fill_price": fill_price, "size_coins": size_coins}
# "error" key present → HL rejected the order entirely
if "error" in status:
raise ValueError(f"HL order rejected: {status['error']}")
filled_info = status.get("filled") or {}
total_sz = float(filled_info.get("totalSz", 0) or 0)
if total_sz <= 0:
# IOC returned with zero fill — no position was opened
raise ValueError(
f"IOC order for {coin} returned 0 fill "
f"(status={status}). No position opened."
)
fill_price = float(filled_info.get("avgPx", mid) or mid)
# oid lives under "filled" for IOC; "resting" is fallback for unexpected GTC
oid_src = filled_info or status.get("resting") or {}
order_id = str(oid_src.get("oid", "")) if oid_src else ""
logger.info("Opened %s %s @ %.2f size=%.5f (order_id=%s)",
side, coin, fill_price, total_sz, order_id)
return {"order_id": order_id, "fill_price": fill_price, "size_coins": total_sz}
async def close_position(self, asset: str) -> dict:
"""
@@ -201,8 +217,9 @@ class HyperliquidTrader:
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("No open %s position to close", coin)
return {"fill_price": 0.0}
logger.warning("No open %s position found on HL — already closed externally?", coin)
# Return sentinel so callers can detect "nothing to close" vs "closed @ price"
return {"fill_price": None, "already_closed": True}
szi = float(target["szi"])
is_buy = szi < 0 # closing a short → buy back
@@ -235,10 +252,21 @@ class HyperliquidTrader:
)
statuses = result.get("response", {}).get("data", {}).get("statuses", [{}])
filled = statuses[0] if statuses else {}
fill_price = float(
(filled.get("filled") or {}).get("avgPx", mid) or mid
)
status = statuses[0] if statuses else {}
filled_info = status.get("filled") or {}
total_sz = float(filled_info.get("totalSz", 0) or 0)
logger.info("Closed %s position @ %.2f", coin, fill_price)
return {"fill_price": fill_price}
if total_sz <= 0:
# IOC close got 0 fill — position may have already been closed externally.
# Re-check to be sure before giving up.
positions_after = await self.get_open_positions()
still_open = next((p for p in positions_after if p.get("coin") == coin), None)
if still_open is None:
logger.info("Close IOC got 0 fill but position is gone — treated as closed", )
return {"fill_price": mid, "already_closed": False}
logger.error("Close IOC got 0 fill and position still open for %s", coin)
raise ValueError(f"Failed to close {coin} position: IOC returned 0 fill")
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}