9872a4cc52
Closes the loop on the asset-routing prompt change. Previously the v5 prompt emitted target_asset (e.g. SOL, TRUMP) but bot_engine still read price_impact_asset and only ever traded BTC/ETH. Now the trade actually fires on whatever perp the AI picked. Schema (alembic 006): posts.target_asset (str) — HL perp ticker, any of the universe posts.category (str) — 6-class enum (direct_named, etc.) posts.expected_move_pct (float) — AI's 1h move estimate Wiring: truth_social.py persists the three fields when creating Post rows. bot_engine.py routing: asset = target_asset || price_impact_asset || BTC Old rows (target_asset=NULL) fall back to legacy BTC/ETH path — no retroactive scoring needed; new rows route correctly from now on. Hyperliquid trader doesn't need changes — `coin` is already a parameter, and analysis.py validated against HL_PERPS before storing target_asset so by the time bot_engine reads the field, it's guaranteed tradeable. Deployment: alembic upgrade head # adds the 3 columns Restart api container Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
130 lines
6.8 KiB
Python
130 lines
6.8 KiB
Python
from datetime import datetime, timezone
|
|
from typing import List, Optional
|
|
|
|
from sqlalchemy import (
|
|
BigInteger,
|
|
Boolean,
|
|
DateTime,
|
|
Float,
|
|
ForeignKey,
|
|
Integer,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.database import Base
|
|
|
|
|
|
def utcnow() -> datetime:
|
|
"""Naive UTC datetime. All datetimes in this codebase are stored as naive-UTC."""
|
|
return datetime.now(timezone.utc).replace(tzinfo=None)
|
|
|
|
|
|
def iso_utc(dt: Optional[datetime]) -> Optional[str]:
|
|
"""Serialize a naive-UTC datetime to an explicit ISO-8601 UTC string (suffix 'Z').
|
|
If dt already carries tzinfo, convert to UTC first. Returns None if dt is None."""
|
|
if dt is None:
|
|
return None
|
|
if dt.tzinfo is not None:
|
|
dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
|
|
# Drop microseconds past millisecond for compactness; always end in Z
|
|
return dt.isoformat(timespec="milliseconds") + "Z"
|
|
|
|
|
|
class Post(Base):
|
|
__tablename__ = "posts"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
|
external_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
|
|
text: Mapped[str] = mapped_column(Text, nullable=False)
|
|
source: Mapped[str] = mapped_column(String(32), nullable=False, default="truth")
|
|
published_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
|
sentiment: Mapped[str] = mapped_column(String(16), nullable=False, default="neutral")
|
|
ai_confidence: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
relevant: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
price_impact_asset: Mapped[Optional[str]] = mapped_column(String(8), nullable=True)
|
|
price_impact_m5: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
price_impact_m15: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
price_impact_m1h: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
price_at_post: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
signal: Mapped[Optional[str]] = mapped_column(String(8), nullable=True)
|
|
ai_reasoning: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
prefilter_reason: Mapped[Optional[str]] = mapped_column(String(32), nullable=True)
|
|
analysis_version: Mapped[Optional[str]] = mapped_column(String(16), nullable=True)
|
|
# v5: AI-decided trade target. Can be any HL perp (BTC/ETH/SOL/TRUMP/...),
|
|
# not just BTC/ETH. The bot routes the actual trade here.
|
|
# `price_impact_asset` stays bound to BTC/ETH for the existing
|
|
# price_impact_monitor; `target_asset` is what we actually trade.
|
|
target_asset: Mapped[Optional[str]] = mapped_column(String(16), nullable=True)
|
|
category: Mapped[Optional[str]] = mapped_column(String(24), nullable=True)
|
|
expected_move_pct: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=utcnow)
|
|
|
|
trades: Mapped[List["BotTrade"]] = relationship("BotTrade", back_populates="trigger_post")
|
|
|
|
|
|
class Candle(Base):
|
|
__tablename__ = "candles"
|
|
__table_args__ = (UniqueConstraint("asset", "timeframe", "time", name="uq_candle_asset_tf_time"),)
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
|
asset: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
|
|
timeframe: Mapped[str] = mapped_column(String(4), nullable=False)
|
|
time: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
|
open: Mapped[float] = mapped_column(Float, nullable=False)
|
|
high: Mapped[float] = mapped_column(Float, nullable=False)
|
|
low: Mapped[float] = mapped_column(Float, nullable=False)
|
|
close: Mapped[float] = mapped_column(Float, nullable=False)
|
|
volume: Mapped[float] = mapped_column(Float, nullable=False)
|
|
|
|
|
|
class BotTrade(Base):
|
|
__tablename__ = "bot_trades"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
|
asset: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
side: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
entry_price: Mapped[float] = mapped_column(Float, nullable=False)
|
|
exit_price: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
pnl_usd: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
hold_seconds: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
trigger_post_id: Mapped[Optional[int]] = mapped_column(
|
|
Integer, ForeignKey("posts.id"), nullable=True, index=True
|
|
)
|
|
wallet_address: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
opened_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=utcnow)
|
|
closed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
|
hl_order_id: Mapped[Optional[str]] = mapped_column(String(128), nullable=True)
|
|
# Snapshot of user settings AT TIME OF ENTRY. Required so changes to
|
|
# Subscription.position_size_usd / leverage after-the-fact don't corrupt
|
|
# historical PnL or daily-budget accounting.
|
|
size_usd: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
leverage: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
|
|
trigger_post: Mapped[Optional["Post"]] = relationship("Post", back_populates="trades")
|
|
|
|
|
|
class Subscription(Base):
|
|
__tablename__ = "subscriptions"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
|
wallet_address: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
|
|
hl_api_key: Mapped[Optional[str]] = mapped_column(String(256), nullable=True)
|
|
active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
subscribed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=utcnow)
|
|
|
|
# Per-user trading config (null → use platform defaults)
|
|
leverage: Mapped[int] = mapped_column(Integer, nullable=False, default=3)
|
|
position_size_usd: Mapped[float] = mapped_column(Float, nullable=False, default=20.0)
|
|
take_profit_pct: Mapped[Optional[float]] = mapped_column(Float, nullable=True) # e.g. 2.0 = close at +2%
|
|
stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float, nullable=True) # e.g. 1.5 = close at -1.5%
|
|
min_confidence: Mapped[int] = mapped_column(Integer, nullable=False, default=80)
|
|
daily_budget_usd: Mapped[Optional[float]] = mapped_column(Float, nullable=True) # e.g. 15.0 = max $15 of new positions per UTC day
|
|
# Optional active window. Both None = always on (when Subscription.active=True).
|
|
# Both stored as naive-UTC datetimes.
|
|
active_from: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
|
active_until: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|