first commit

This commit is contained in:
k
2026-04-20 23:05:59 +08:00
commit 9a72566753
33 changed files with 2138 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
"""
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)