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:
+14
-6
@@ -26,15 +26,23 @@ def _direction_correct(signal: Optional[str], pct: Optional[float]) -> Optional[
|
||||
def _post_to_schema(post: Post) -> TrumpPost:
|
||||
price_impact: Optional[PriceImpact] = None
|
||||
if post.price_impact_asset and post.price_at_post is not None:
|
||||
# Overlay live rolling peaks for windows that haven't closed yet
|
||||
from app.services.price_impact_monitor import get_live_impact
|
||||
live = get_live_impact(post.id) or {}
|
||||
|
||||
m5 = live.get("price_impact_m5", post.price_impact_m5)
|
||||
m15 = live.get("price_impact_m15", post.price_impact_m15)
|
||||
m1h = live.get("price_impact_m1h", post.price_impact_m1h)
|
||||
|
||||
price_impact = PriceImpact(
|
||||
asset=post.price_impact_asset,
|
||||
m5=post.price_impact_m5 or 0.0,
|
||||
m15=post.price_impact_m15 or 0.0,
|
||||
m1h=post.price_impact_m1h or 0.0,
|
||||
m5=m5,
|
||||
m15=m15,
|
||||
m1h=m1h,
|
||||
price_at_post=post.price_at_post,
|
||||
correct_m5=_direction_correct(post.signal, post.price_impact_m5),
|
||||
correct_m15=_direction_correct(post.signal, post.price_impact_m15),
|
||||
correct_m1h=_direction_correct(post.signal, post.price_impact_m1h),
|
||||
correct_m5=_direction_correct(post.signal, m5),
|
||||
correct_m15=_direction_correct(post.signal, m15),
|
||||
correct_m1h=_direction_correct(post.signal, m1h),
|
||||
)
|
||||
return TrumpPost(
|
||||
id=post.id,
|
||||
|
||||
+50
-7
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -150,6 +150,9 @@ async def get_user(
|
||||
take_profit_pct=sub.take_profit_pct,
|
||||
stop_loss_pct=sub.stop_loss_pct,
|
||||
min_confidence=sub.min_confidence,
|
||||
daily_budget_usd=sub.daily_budget_usd,
|
||||
active_from=iso_utc(sub.active_from),
|
||||
active_until=iso_utc(sub.active_until),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -157,10 +160,20 @@ async def get_user(
|
||||
@router.put("/user/{wallet}/settings", response_model=UserSettings)
|
||||
async def set_user_settings(
|
||||
wallet: str,
|
||||
body: SetSettingsRequest,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
wallet = wallet.lower().strip()
|
||||
# Read raw JSON FIRST — the frontend signs the payload exactly as it's
|
||||
# serialized in JSON (e.g. `5` stays `5`, not `5.0`). If we let Pydantic
|
||||
# coerce ints→floats first, the server-side hash won't match the client's.
|
||||
raw = await request.json()
|
||||
raw_settings = raw.get("settings")
|
||||
if not isinstance(raw_settings, dict):
|
||||
raise HTTPException(422, "Missing settings block")
|
||||
|
||||
# Now validate with Pydantic
|
||||
body = SetSettingsRequest(**raw)
|
||||
if wallet != body.wallet.lower().strip():
|
||||
raise HTTPException(400, "Wallet mismatch")
|
||||
|
||||
@@ -169,19 +182,46 @@ async def set_user_settings(
|
||||
raise HTTPException(422, "leverage must be 1–50")
|
||||
if not (5 <= s.position_size_usd <= 10000):
|
||||
raise HTTPException(422, "position_size_usd must be $5–$10,000")
|
||||
if s.take_profit_pct is not None and not (0.1 <= s.take_profit_pct <= 50):
|
||||
raise HTTPException(422, "take_profit_pct must be 0.1–50")
|
||||
if s.stop_loss_pct is not None and not (0.1 <= s.stop_loss_pct <= 50):
|
||||
raise HTTPException(422, "stop_loss_pct must be 0.1–50")
|
||||
# Take-profit, stop-loss and daily budget are now MANDATORY — the bot won't
|
||||
# run without them, so we reject null here and force the client to supply values.
|
||||
if s.take_profit_pct is None or not (0.1 <= s.take_profit_pct <= 50):
|
||||
raise HTTPException(422, "take_profit_pct is required (0.1–50)")
|
||||
if s.stop_loss_pct is None or not (0.1 <= s.stop_loss_pct <= 50):
|
||||
raise HTTPException(422, "stop_loss_pct is required (0.1–50)")
|
||||
if not (0 <= s.min_confidence <= 100):
|
||||
raise HTTPException(422, "min_confidence must be 0–100")
|
||||
if s.daily_budget_usd is None or not (0 < s.daily_budget_usd <= 100000):
|
||||
raise HTTPException(422, "daily_budget_usd is required (>0, ≤100,000)")
|
||||
|
||||
# Parse schedule (ISO strings → naive-UTC datetimes). Either both or neither.
|
||||
from datetime import datetime as _dt, timezone as _tz
|
||||
|
||||
def _parse_iso_utc(v):
|
||||
if v is None:
|
||||
return None
|
||||
try:
|
||||
# Accept "...Z" or "+00:00" forms
|
||||
dt = _dt.fromisoformat(v.replace("Z", "+00:00"))
|
||||
if dt.tzinfo is not None:
|
||||
dt = dt.astimezone(_tz.utc).replace(tzinfo=None)
|
||||
return dt
|
||||
except Exception:
|
||||
raise HTTPException(422, f"invalid ISO datetime: {v!r}")
|
||||
|
||||
af = _parse_iso_utc(s.active_from)
|
||||
au = _parse_iso_utc(s.active_until)
|
||||
if (af is None) != (au is None):
|
||||
raise HTTPException(422, "active_from and active_until must be set together or both empty")
|
||||
if af is not None and au is not None and au <= af:
|
||||
raise HTTPException(422, "active_until must be after active_from")
|
||||
|
||||
# Hash the RAW dict (matches what frontend signed)
|
||||
verify_signed_request(
|
||||
action=ACTION_SET_SETTINGS,
|
||||
wallet=wallet,
|
||||
timestamp_ms=body.timestamp,
|
||||
signature=body.signature,
|
||||
body=s.model_dump(),
|
||||
body=raw_settings,
|
||||
)
|
||||
|
||||
result = await db.execute(select(Subscription).where(Subscription.wallet_address == wallet))
|
||||
@@ -194,6 +234,9 @@ async def set_user_settings(
|
||||
sub.take_profit_pct = s.take_profit_pct
|
||||
sub.stop_loss_pct = s.stop_loss_pct
|
||||
sub.min_confidence = s.min_confidence
|
||||
sub.daily_budget_usd = s.daily_budget_usd
|
||||
sub.active_from = af
|
||||
sub.active_until = au
|
||||
await db.commit()
|
||||
logger.info("Settings updated for %s: %s", wallet, s.model_dump())
|
||||
return s
|
||||
|
||||
Reference in New Issue
Block a user