from typing import Optional from pydantic import BaseModel class PriceImpact(BaseModel): asset: str # None = window still open (live rolling peak) or no price data yet. # Float = sealed peak move (%) in the signal direction once window closed. m5: Optional[float] = None m15: Optional[float] = None m1h: Optional[float] = None price_at_post: float # None = outcome window not yet reached; True/False = signal direction matched correct_m5: Optional[bool] = None correct_m15: Optional[bool] = None correct_m1h: Optional[bool] = None class TrumpPost(BaseModel): id: int text: str source: str published_at: str # ISO sentiment: str signal: Optional[str] = None # buy | sell | short | hold ai_confidence: int ai_reasoning: Optional[str] = None prefilter_reason: Optional[str] = None # rt_only | url_only | empty | parse_error | api_error | null analysis_version: Optional[str] = None relevant: bool price_impact: Optional[PriceImpact] = None # v5 asset routing — what the bot will actually trade if signal != hold. # target_asset = any HL perp ticker (BTC/ETH/SOL/TRUMP/...). category # buckets the post's catalyst type. expected_move_pct = AI's own 1h-move # estimate on target_asset. All three are null on pre-v5 posts. target_asset: Optional[str] = None category: Optional[str] = None expected_move_pct: Optional[float] = None invalidation_price: Optional[float] = None model_config = {"from_attributes": True} class Candle(BaseModel): time: int open: float high: float low: float close: float volume: float class BotTrade(BaseModel): id: int asset: str side: str entry_price: float exit_price: float pnl_usd: float hold_seconds: int trigger_post_id: int opened_at: str closed_at: str # Source tag of the originating signal (e.g. 'truth', 'breakout', 'my_strategy'). # Joined from posts.source on read; not stored on BotTrade itself. # 'unknown' when the trigger post has been deleted or trigger_post_id is null. trigger_source: Optional[str] = None # True iff this was a paper trade (hl_order_id == 'paper'). Lets the UI # tag the row so paper-mode P&L isn't mixed with real money in summaries. is_paper: bool = False model_config = {"from_attributes": True} class BotPerformance(BaseModel): period_days: int total_trades: int win_rate: float net_pnl_usd: float avg_hold_seconds: float max_drawdown_pct: float class SignedEnvelope(BaseModel): """Every write request carries this envelope. Client builds a canonical message (see app.services.signed_request.build_message) and signs it via EIP-191 personal_sign. Server re-builds the message from {action, wallet, timestamp, sha256(body)} and recovers the signer. """ wallet: str timestamp: int # milliseconds since epoch signature: str class SubscribeRequest(SignedEnvelope): # Optional paper-mode flag. When true the bot writes simulated trades to # the DB but never hits Hyperliquid — safe path for new users to try the # system without risking funds. Defaults to false (live) for backwards # compatibility with the existing signed-message format (body=None). paper_mode: Optional[bool] = False class SubscribeResponse(BaseModel): status: str wallet: str paper_mode: bool = False class SetApiKeyRequest(SignedEnvelope): api_key: str # Hyperliquid API wallet private key (0x...) class SetApiKeyResponse(BaseModel): status: str masked_key: str # last 6 chars only, e.g. "...a1b2c3" verified: bool = False class UserSettings(BaseModel): leverage: int position_size_usd: float take_profit_pct: Optional[float] = None stop_loss_pct: Optional[float] = None min_confidence: int daily_budget_usd: Optional[float] = None # System-2 (bottom reversal) leverage — independent of `leverage` (Trump). # None → platform default (SYS2_DEFAULT_LEVERAGE). The protective stop is # auto-scaled to this so the position is never exchange-liquidated. sys2_leverage: Optional[int] = None # System-2 risk mode: "standard" (default) or "aggressive" (separately # funded high-risk/high-explosiveness sleeve). None → unchanged/standard. # "standard" | "aggressive" | "" (empty string = reset to default "standard") sys2_mode: Optional[str] = None # ISO-8601 UTC strings; both None = always on (Subscription.active still gates it). active_from: Optional[str] = None active_until: Optional[str] = None # Module on/off toggles — default False so new users start with bot idle. trump_enabled: Optional[bool] = None macro_enabled: Optional[bool] = None class SetSettingsRequest(SignedEnvelope): settings: UserSettings class UserResponse(BaseModel): wallet_address: str active: bool subscribed_at: Optional[str] = None hl_api_key_set: bool hl_api_key_masked: Optional[str] = None paper_mode: bool = False trades: list[BotTrade] settings: UserSettings # Convex-strategy: ISO-UTC timestamp until which the bot is manually armed. # When null or in the past, the regular active_from/active_until schedule applies. manual_window_until: Optional[str] = None