diff --git a/alembic/versions/006_add_target_asset.py b/alembic/versions/006_add_target_asset.py new file mode 100644 index 0000000..916fb83 --- /dev/null +++ b/alembic/versions/006_add_target_asset.py @@ -0,0 +1,34 @@ +"""Add target_asset / category / expected_move_pct to posts (v5 routing) + +Revision ID: 006 +Revises: 005 +Create Date: 2026-05-08 00:00:00.000000 + +The v5 AI prompt outputs not just signal direction but also which specific +perp to trade (could be BTC/ETH/SOL/TRUMP/anything on Hyperliquid). The +bot reads `target_asset` to route the trade. `price_impact_asset` stays +bound to BTC/ETH for the existing price_impact_monitor. +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "006" +down_revision: Union[str, None] = "005" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + with op.batch_alter_table("posts") as batch_op: + batch_op.add_column(sa.Column("target_asset", sa.String(16), nullable=True)) + batch_op.add_column(sa.Column("category", sa.String(24), nullable=True)) + batch_op.add_column(sa.Column("expected_move_pct", sa.Float(), nullable=True)) + + +def downgrade() -> None: + with op.batch_alter_table("posts") as batch_op: + batch_op.drop_column("expected_move_pct") + batch_op.drop_column("category") + batch_op.drop_column("target_asset") diff --git a/app/models.py b/app/models.py index 03cd18e..560d22a 100644 --- a/app/models.py +++ b/app/models.py @@ -53,6 +53,13 @@ class Post(Base): 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") diff --git a/app/scrapers/truth_social.py b/app/scrapers/truth_social.py index a8c4cc5..5c712a2 100644 --- a/app/scrapers/truth_social.py +++ b/app/scrapers/truth_social.py @@ -102,11 +102,16 @@ async def _process_entry(entry: dict, db: AsyncSession) -> Optional[Post]: prefilter_reason=analysis.get("prefilter_reason"), analysis_version=analysis.get("analysis_version"), relevant=analysis["relevant"], + # `asset` (BTC/ETH only) feeds the existing price_impact tracker. price_impact_asset=asset if analysis["relevant"] else None, price_impact_m5=None, # filled by price_impact_monitor after 5 m price_impact_m15=None, # filled by price_impact_monitor after 15 m price_impact_m1h=None, # filled by price_impact_monitor after 1 h price_at_post=price_at_post, + # v5 routing: AI decides the actual perp to trade. May be SOL/TRUMP/etc. + target_asset=analysis.get("target_asset"), + category=analysis.get("category"), + expected_move_pct=analysis.get("expected_move_pct"), ) db.add(post) await db.flush() diff --git a/app/services/bot_engine.py b/app/services/bot_engine.py index bdde71c..3137a6c 100644 --- a/app/services/bot_engine.py +++ b/app/services/bot_engine.py @@ -77,8 +77,17 @@ async def process_post(post: Post, db: AsyncSession) -> None: logger.info("Post %d skipped: signal=%s is not actionable", post.id, post.signal) return - asset = post.price_impact_asset or 'BTC' + # v5: route the trade to the AI-decided perp. target_asset can be any + # Hyperliquid perp ticker (BTC/ETH/SOL/TRUMP/...) — analysis.py already + # validated against HL_PERPS and resolved chain fallbacks before this + # field was set, so by the time we get here it's safe to trade directly. + # Fallbacks for older / null rows: legacy price_impact_asset, then BTC. + asset = post.target_asset or post.price_impact_asset or 'BTC' side = 'long' if post.signal == 'buy' else 'short' + logger.info( + "Routing post %d → trade %s/%s (category=%s, expected_move=%.2f%%)", + post.id, asset, side, post.category, post.expected_move_pct or 0, + ) logger.info("Signal: post=%d asset=%s side=%s confidence=%d", post.id, asset, side, post.ai_confidence)