""" Trading decision loop. Called by the Truth Social scraper after each new post is analyzed and saved. Iterates all active subscribers and executes trades on their behalf. """ import asyncio import logging from datetime import datetime, timezone from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.models import Post, BotTrade, Subscription from app.services.hyperliquid import HyperliquidTrader from app.services.price_store import price_store logger = logging.getLogger(__name__) # Thresholds MIN_CONFIDENCE = 70 # ai_confidence must be >= this to trade POSITION_SIZE_USD = 100 # per-trade size in USD (fixed for MVP) MAX_HOLD_SECONDS = 3600 # force-close after 1 hour async def process_post(post: Post, db: AsyncSession) -> None: """ Entry point called by the scraper after a new post is saved. Skips non-relevant or low-confidence posts. """ if not post.relevant: return if (post.ai_confidence or 0) < MIN_CONFIDENCE: logger.info("Post %d skipped: confidence %d < %d", post.id, post.ai_confidence, MIN_CONFIDENCE) return if post.sentiment == 'neutral': return asset = post.price_impact_asset or 'BTC' side = 'long' if post.sentiment == 'bullish' else 'short' logger.info("Signal: post=%d asset=%s side=%s confidence=%d", post.id, asset, side, post.ai_confidence) result = await db.execute(select(Subscription).where(Subscription.active == True)) subscribers = result.scalars().all() if not subscribers: logger.info("No active subscribers, skipping trade execution") return tasks = [_execute_for_subscriber(sub, post, asset, side, db) for sub in subscribers] await asyncio.gather(*tasks, return_exceptions=True) async def _execute_for_subscriber( sub: Subscription, post: Post, asset: str, side: str, db: AsyncSession, ) -> None: if not sub.hl_api_key: logger.warning("Subscriber %s has no HL API key, skipping", sub.wallet_address) return try: trader = HyperliquidTrader(sub.hl_api_key) # Check for existing open position to avoid doubling up open_positions = await trader.get_open_positions() already_open = any(p.get('coin') == asset for p in open_positions) if already_open: logger.info("Subscriber %s already has open %s position, skipping", sub.wallet_address, asset) return result = await trader.open_position(asset, side, POSITION_SIZE_USD) entry_price = result.get('fill_price', 0.0) trade = BotTrade( asset=asset, side=side, entry_price=entry_price, wallet_address=sub.wallet_address, trigger_post_id=post.id, opened_at=datetime.now(timezone.utc).replace(tzinfo=None), hl_order_id=result.get('order_id'), ) db.add(trade) await db.commit() await db.refresh(trade) logger.info("Opened %s %s for %s @ %.2f (trade_id=%d)", side, asset, sub.wallet_address, entry_price, trade.id) # Schedule close after MAX_HOLD_SECONDS asyncio.create_task(_close_after_hold(trade.id, sub.hl_api_key, asset, sub.wallet_address)) except Exception as e: logger.error("Trade execution failed for %s: %s", sub.wallet_address, e) async def _close_after_hold(trade_id: int, api_key: str, asset: str, wallet: str) -> None: await asyncio.sleep(MAX_HOLD_SECONDS) from app.database import AsyncSessionLocal async with AsyncSessionLocal() as db: try: result = await db.execute( select(BotTrade).where(BotTrade.id == trade_id, BotTrade.closed_at.is_(None)) ) trade = result.scalar_one_or_none() if trade is None: return # already closed trader = HyperliquidTrader(api_key) close_result = await trader.close_position(asset) exit_price = close_result.get('fill_price', 0.0) now = datetime.now(timezone.utc) hold_secs = int((now - trade.opened_at.replace(tzinfo=timezone.utc)).total_seconds()) pnl = (exit_price - trade.entry_price) * (1 if trade.side == 'long' else -1) # Approximate PnL: size_usd * pct_change pnl_usd = POSITION_SIZE_USD * (pnl / trade.entry_price) if trade.entry_price else 0.0 trade.exit_price = exit_price trade.pnl_usd = round(pnl_usd, 2) trade.hold_seconds = hold_secs trade.closed_at = now await db.commit() logger.info("Closed %s %s for %s @ %.2f PnL=%.2f", trade.side, asset, wallet, exit_price, pnl_usd) except Exception as e: logger.error("Failed to close trade %d: %s", trade_id, e)