#!/usr/bin/env python3
"""
Turbine Single Market Automated YES/NO Market Maker - ASYNC

Mirrors SMTrader (auto1.py) logic but uses TurbineClient SDK
for 15-minute BTC/ETH/SOL prediction markets on Turbine.

Key differences from Kalshi SMTrader:
  - Prices in 1e6 units (0-1,000,000) not cents (1-99)
  - Sizes in 1e6 units (1,000,000 = 1 share)
  - Orders identified by order_hash not order_id
  - Market auto-rotates every 15 minutes via QuickMarket
  - EIP-712 wallet signing for orders + Ed25519 bearer auth
  - Cancel-and-replace (no amend endpoint)
"""

import os
import sys
import time
import math
import json
import asyncio
import logging
import contextlib
import io
from typing import Optional, Dict
from datetime import datetime, timezone
from collections import deque

import requests as _requests

from app.modules.terminal.auto.turbine_client import TurbineClient, Outcome, Side, QuickMarket
from app.modules.terminal.auto.turbine_client.exceptions import TurbineApiError

GAMMA_HOST = "https://gamma-api.polymarket.com"

# Suppress noisy SDK logs
logging.getLogger("turbine_client.discovery").setLevel(logging.CRITICAL)
logging.getLogger("turbine_client").setLevel(logging.WARNING)

# Constants
CHAIN_ID = 137  # Polygon
TURBINE_HOST = "https://api.turbinefi.com"
PRICE_TICK = 10_000  # 1 cent in 1e6 units
END_OF_MARKET_STOP_SECONDS = 60
MARKET_POLL_SECONDS = 5
MAX_APPROVAL = (2 ** 256 - 1) // 2


class TurbineSMTrader:
    """Single market YES/NO trader for Turbine - mirrors SMTrader interface."""

    SIDES = ["yes", "no"]
    OUTCOMES = {"yes": Outcome.YES, "no": Outcome.NO}

    def __init__(self, client: TurbineClient, asset: str = "BTC", contract_usdc: float = 1.0):
        self.client = client
        self.asset = asset
        self.contract_usdc = contract_usdc

        # Market state
        self.market_id: str = ""
        self.settlement_address: str = ""
        self.contract_address: str = ""
        self.end_time: int = 0
        self.strike_price: int = 0

        # Orderbook
        self.yes_bid: Optional[int] = None
        self.yes_bid_size: int = 0
        self.yes_second_bid: Optional[int] = None
        self.yes_ask: Optional[int] = None
        self.yes_ask_size: int = 0
        self.no_bid: Optional[int] = None
        self.no_bid_size: int = 0
        self.no_second_bid: Optional[int] = None
        self.no_ask: Optional[int] = None
        self.no_ask_size: int = 0

        # Order tracking
        self.order_hashes: Dict[str, Optional[str]] = {"yes": None, "no": None}
        self.order_sides: Dict[str, Side] = {"yes": Side.BUY, "no": Side.BUY}
        self.last_prices: Dict[str, Optional[int]] = {"yes": None, "no": None}
        self.current_increment: Dict[str, int] = {"yes": 0, "no": 0}
        self.cycle_start_resting: Dict[str, int] = {"yes": 0, "no": 0}
        self.cached_resting: Dict[str, int] = {"yes": 0, "no": 0}
        self.cached_position: Dict[str, int] = {"yes": 0, "no": 0}
        self.fill_prices: Dict[str, Optional[int]] = {"yes": None, "no": None}

        # Control flags (same as SMTrader)
        self.running = False
        self.active = False
        self.stopping = False
        self.paused = False
        self.waiting_for_manual_resume = False
        self.is_rebalancing = False
        self.market_expiring = False
        self.contract_increment = 3

        # Single fire
        self.single_fire_mode = False
        self.single_fire_cycles_completed = 0

        # Fair value
        self.fair_value_enabled = False
        self.fair_value_history = deque(maxlen=10)
        self.current_fair_value = None

        # Jump mode
        self.jump_active: Dict[str, bool] = {}
        self.jump_target: Dict[str, Optional[int]] = {}

        # One-side-first
        self.one_side_first_mode = False
        self.active_side: Optional[str] = None

        # Redis + instance tracking
        self.instance_id: Optional[int] = None
        self.redis_client = None

        # Session tracking
        self.session_start_time: Optional[str] = None
        self.cycles_completed: int = 0

        # USDC approval tracking
        self.approved_settlements: set = set()

        # Polymarket price feed mode
        self.price_feed: Optional[str] = None  # "poly_mid" or None
        self.poly_mid_spread: int = 100_000  # spread in 1e6 units (default 10c)
        self.poly_yes_mid: Optional[float] = None  # 0.0-1.0 from Polymarket
        self.poly_token_id: Optional[str] = None  # YES token_id for midpoint
        self.poly_strike_max_diff: float = 10.0  # max $ diff between strikes

    def _get_markets(self):
        return [self.market_id]

    # ============================================================
    # POLYMARKET PRICE FEED
    # ============================================================

    def _find_poly_market(self) -> bool:
        """Find the matching Polymarket 15m up/down market for our asset.
        Computes the current 15-minute window start timestamp and looks up
        the Polymarket event by slug: {asset}-updown-15m-{timestamp}.
        Returns True if a matching market was found.
        """
        if self.price_feed != "poly_mid":
            return False

        # Compute current 15-minute window start (markets start on :00, :15, :30, :45)
        window_start = (int(time.time()) // 900) * 900
        slug = f"{self.asset.lower()}-updown-15m-{window_start}"

        try:
            resp = _requests.get(
                f"{GAMMA_HOST}/events",
                params={"slug": slug},
                timeout=10,
            )
            resp.raise_for_status()
            events = resp.json()
        except Exception as e:
            print(f"Poly price feed: search failed: {e}")
            return False

        if not events:
            print(f"Poly price feed: no event for slug '{slug}'")
            self.poly_token_id = None
            return False

        event = events[0] if isinstance(events, list) else events
        markets = event.get("markets", [])
        if not markets:
            print(f"Poly price feed: event has no markets")
            self.poly_token_id = None
            return False

        market = markets[0]
        token_ids_raw = market.get("clobTokenIds", "[]")
        token_ids = json.loads(token_ids_raw) if isinstance(token_ids_raw, str) else token_ids_raw
        if not token_ids:
            print(f"Poly price feed: no clobTokenIds")
            self.poly_token_id = None
            return False

        self.poly_token_id = token_ids[0]  # "Up" token
        title = event.get("title", slug)[:60]
        print(f"Poly price feed: matched '{title}'")
        return True

    def _fetch_poly_midpoint(self) -> Optional[float]:
        """Fetch current YES midpoint from Polymarket for our matched market.
        Returns 0.0-1.0 or None on failure.
        """
        if not self.poly_token_id:
            return None
        try:
            resp = _requests.get(
                f"https://clob.polymarket.com/midpoint",
                params={"token_id": self.poly_token_id},
                timeout=5,
            )
            resp.raise_for_status()
            data = resp.json()
            mid = float(data.get("mid", 0))
            if 0 < mid < 1:
                self._poly_last_fetch = time.time()
                return mid
            print(f"⚠️ Poly midpoint out of range: {mid}")
        except Exception as e:
            print(f"⚠️ Poly midpoint fetch failed: {e}")
        # Staleness guard: if last successful fetch was >60s ago, clear cached mid
        if hasattr(self, '_poly_last_fetch') and time.time() - self._poly_last_fetch > 60:
            self.poly_yes_mid = None
        return None

    def _get_poly_target_prices(self) -> Dict[str, Optional[int]]:
        """Calculate YES and NO target prices from Polymarket midpoint.
        Returns prices in 1e6 units, or None if poly feed unavailable.
        """
        mid = self._fetch_poly_midpoint()
        if mid is None:
            return {"yes": None, "no": None}

        self.poly_yes_mid = mid
        half_spread = self.poly_mid_spread // 2  # in 1e6 units

        yes_price = int(mid * 1_000_000) - half_spread
        no_price = int((1.0 - mid) * 1_000_000) - half_spread

        # Clamp to valid range
        yes_price = max(10_000, min(990_000, yes_price))
        no_price = max(10_000, min(990_000, no_price))

        return {"yes": yes_price, "no": no_price}

    def _shares_from_usdc(self, usdc: float, price: int) -> int:
        if price <= 0:
            return 0
        return math.ceil((usdc * 1_000_000 * 1_000_000) / price)

    def seconds_remaining(self) -> int:
        return max(0, self.end_time - int(time.time()))

    # ============================================================
    # USDC APPROVAL
    # ============================================================

    def ensure_usdc_approved(self) -> None:
        addr = self.settlement_address
        if not addr or addr in self.approved_settlements:
            return
        try:
            allowance = self.client.get_usdc_allowance(spender=addr)
            if allowance >= MAX_APPROVAL:
                print("✓ USDC max approval already in place")
                self.approved_settlements.add(addr)
                return
        except Exception:
            pass
        try:
            result = self.client.approve_usdc_for_settlement(addr)
            tx_hash = result.get("tx_hash", result.get("txHash", "unknown"))
            print(f"✓ USDC approval TX: {tx_hash}")
            self.approved_settlements.add(addr)
        except Exception as e:
            print(f"⚠️ USDC approval error: {e}")

    # ============================================================
    # MARKET DISCOVERY
    # ============================================================

    def discover_market(self) -> bool:
        """Find the active quick market for this asset."""
        try:
            resp = self.client._http.get(f"/api/v1/quick-markets/{self.asset}")
            data = resp.get("quickMarket") if resp else None
            if not data:
                print(f"❌ No active {self.asset} market found")
                return False

            qm = QuickMarket.from_dict(data)
            self.market_id = qm.market_id
            self.end_time = qm.end_time
            self.strike_price = qm.start_price or 0
            self.contract_address = qm.contract_address

            # Fetch settlement address
            try:
                with contextlib.redirect_stdout(io.StringIO()), \
                     contextlib.redirect_stderr(io.StringIO()):
                    markets = self.client.get_markets()
                for m in (markets or []):
                    if m.id == self.market_id:
                        self.settlement_address = m.settlement_address
                        break
            except Exception as e:
                print(f"⚠️ Settlement address fetch failed: {e}")

            self.ensure_usdc_approved()
            strike_usd = self.strike_price / 1e6 if self.strike_price else 0
            print(f"✓ Market: {self.market_id[:20]}... | Strike ${strike_usd:,.2f}")

            # Find matching Polymarket market for price feed
            if self.price_feed == "poly_mid":
                self._find_poly_market()

            return True
        except Exception as e:
            print(f"❌ Market discovery error: {e}")
            return False

    async def check_market_transition(self) -> bool:
        """Check if market has rotated to a new 15m window. Returns True if transitioned."""
        try:
            resp = self.client._http.get(f"/api/v1/quick-markets/{self.asset}")
            data = resp.get("quickMarket") if resp else None
            if not data:
                return False

            qm = QuickMarket.from_dict(data)
            if qm.market_id != self.market_id:
                print(f"\n🔄 New market: {qm.market_id[:16]}...")
                await self.cancel_all_orders_async()

                self.market_id = qm.market_id
                self.end_time = qm.end_time
                self.strike_price = qm.start_price or 0
                self.contract_address = qm.contract_address
                self.market_expiring = False

                # Reset order tracking
                self.order_hashes = {"yes": None, "no": None}
                self.order_sides = {"yes": Side.BUY, "no": Side.BUY}
                self.last_prices = {"yes": None, "no": None}
                self.cached_resting = {"yes": 0, "no": 0}
                self.current_increment = {"yes": 0, "no": 0}
                self.cycle_start_resting = {"yes": 0, "no": 0}
                self.fill_prices = {"yes": None, "no": None}

                # Fetch settlement for new market
                old_settlement = self.settlement_address
                try:
                    with contextlib.redirect_stdout(io.StringIO()), \
                         contextlib.redirect_stderr(io.StringIO()):
                        markets = self.client.get_markets()
                    found = False
                    for m in (markets or []):
                        if m.id == qm.market_id:
                            self.settlement_address = m.settlement_address
                            found = True
                            break
                    if not found:
                        print(f"⚠️ Settlement not found for new market — keeping previous")
                except Exception as e:
                    print(f"⚠️ Settlement fetch failed on transition: {e}")

                if self.settlement_address:
                    self.ensure_usdc_approved()

                strike_usd = (qm.start_price or 0) / 1e6
                print(f"✓ Now trading: {qm.market_id[:16]}... | Strike ${strike_usd:,.2f}")

                # Re-match Polymarket market for new strike
                if self.price_feed == "poly_mid":
                    self._find_poly_market()

                return True
            else:
                self.end_time = qm.end_time  # keep end_time fresh
        except Exception as e:
            print(f"⚠️ Market check error: {e}")
        return False

    # ============================================================
    # MARKET DATA
    # ============================================================

    async def refresh_market_data_async(self):
        """Fetch orderbooks, open orders, and positions."""
        # YES orderbook
        try:
            yes_ob = self.client.get_orderbook(self.market_id, outcome=Outcome.YES)
            if yes_ob.bids:
                self.yes_bid = yes_ob.bids[0].price
                self.yes_bid_size = yes_ob.bids[0].size
                self.yes_second_bid = yes_ob.bids[1].price if len(yes_ob.bids) > 1 else None
            else:
                self.yes_bid = None
                self.yes_bid_size = 0
                self.yes_second_bid = None
            if yes_ob.asks:
                self.yes_ask = yes_ob.asks[0].price
                self.yes_ask_size = yes_ob.asks[0].size
            else:
                self.yes_ask = None
                self.yes_ask_size = 0
        except Exception as e:
            print(f"⚠️ YES orderbook fetch failed: {e}")

        # NO orderbook
        try:
            no_ob = self.client.get_orderbook(self.market_id, outcome=Outcome.NO)
            if no_ob.bids:
                self.no_bid = no_ob.bids[0].price
                self.no_bid_size = no_ob.bids[0].size
                self.no_second_bid = no_ob.bids[1].price if len(no_ob.bids) > 1 else None
            else:
                self.no_bid = None
                self.no_bid_size = 0
                self.no_second_bid = None
            if no_ob.asks:
                self.no_ask = no_ob.asks[0].price
                self.no_ask_size = no_ob.asks[0].size
            else:
                self.no_ask = None
                self.no_ask_size = 0
        except Exception as e:
            print(f"⚠️ NO orderbook fetch failed: {e}")

        # Open orders
        try:
            open_orders = self.client.get_orders(
                trader=self.client.address,
                market_id=self.market_id,
                status="open",
            )
            api_hashes = {o.order_hash: o for o in (open_orders or [])}
            for side in self.SIDES:
                oh = self.order_hashes[side]
                if oh is not None:
                    if oh in api_hashes:
                        self.cached_resting[side] = api_hashes[oh].remaining_size
                    else:
                        self.order_hashes[side] = None
                        self.order_sides[side] = Side.BUY
                        self.last_prices[side] = None
                        self.cached_resting[side] = 0
                else:
                    self.cached_resting[side] = 0
        except Exception as e:
            print(f"⚠️ Open orders fetch failed: {e}")

        # Positions
        try:
            with contextlib.redirect_stdout(io.StringIO()), \
                 contextlib.redirect_stderr(io.StringIO()):
                positions = self.client.get_user_positions(self.client.address)
            self.cached_position["yes"] = 0
            self.cached_position["no"] = 0
            for pos in (positions or []):
                if getattr(pos, "market_id", None) == self.market_id:
                    self.cached_position["yes"] = getattr(pos, "yes_shares", 0) or 0
                    self.cached_position["no"] = getattr(pos, "no_shares", 0) or 0
                    break
        except Exception as e:
            print(f"⚠️ Positions fetch failed: {e}")

        # Fair value
        if self.fair_value_enabled:
            if self.yes_bid is not None and self.no_bid is not None:
                # YES fair value midpoint: average of YES bid and implied YES ask (1M - NO bid)
                midpoint = (self.yes_bid + (1_000_000 - self.no_bid)) / 2
                self.fair_value_history.append(midpoint)
                if len(self.fair_value_history) > 0:
                    self.current_fair_value = sum(self.fair_value_history) / len(self.fair_value_history)

    # ============================================================
    # ORDER OPERATIONS
    # ============================================================

    async def _place_order(self, side: str, price: int, shares: int) -> Optional[str]:
        """Place a limit buy, return order_hash or None."""
        outcome = self.OUTCOMES[side]
        try:
            order = self.client.create_limit_buy(
                market_id=self.market_id,
                outcome=outcome,
                price=price,
                size=shares,
                expiration=int(time.time()) + 300,
                settlement_address=self.settlement_address,
            )
            self.client.post_order(order)
            return order.order_hash
        except Exception as e:
            print(f"❌ {side.upper()}: Order failed: {e}")
            return None

    async def _cancel_order(self, order_hash: str, side: Side = Side.BUY) -> bool:
        try:
            self.client.cancel_order(order_hash, market_id=self.market_id, side=side)
            return True
        except Exception:
            return False

    async def cancel_all_orders_async(self):
        """Cancel open orders for our current market only."""
        try:
            open_orders = self.client.get_orders(
                trader=self.client.address,
                market_id=self.market_id,
                status="open",
            )
            for order in (open_orders or []):
                try:
                    self.client.cancel_order(
                        order.order_hash,
                        market_id=order.market_id,
                        side=Side(order.side),
                    )
                except Exception:
                    pass
        except Exception as e:
            print(f"❌ Cancel error: {e}")

        for side in self.SIDES:
            self.order_hashes[side] = None
            self.order_sides[side] = Side.BUY
            self.last_prices[side] = None
            self.cached_resting[side] = 0

    # ============================================================
    # BID/PRICE LOGIC (adapted from tk.py, mirrors mm_core.py)
    # ============================================================

    def get_bid_info(self, side: str):
        if side == "yes":
            return self.yes_bid, self.yes_bid_size, self.yes_second_bid
        return self.no_bid, self.no_bid_size, self.no_second_bid

    def get_market_spread(self) -> Optional[int]:
        """Spread in 1e6 units."""
        if self.yes_bid is not None and self.no_bid is not None:
            return 1_000_000 - self.yes_bid - self.no_bid
        return None

    def check_target_price(self, side: str, bid, bid_size, second_bid,
                           current_price, our_resting) -> Optional[int]:
        """Determine target price in 1e6 units."""
        # POLY_MID MODE: price from Polymarket fair value
        if self.price_feed == "poly_mid":
            targets = self._get_poly_target_prices()
            target = targets.get(side)
            if target is not None:
                # Join-only: never bid above the current best bid
                if bid is not None and target > bid:
                    return bid
                return target
            # Fallback to join mode if poly feed fails
            if bid is None:
                return None
            return bid

        if bid is None:
            return None

        if bid_size > our_resting:
            others_best = bid
        elif second_bid is not None:
            others_best = second_bid
        else:
            others_best = None

        # JUMP MODE
        if self.jump_active.get(self.market_id, False):
            if self.jump_target.get(self.market_id) is None and others_best is not None:
                self.jump_target[self.market_id] = others_best + PRICE_TICK
            target = self.jump_target.get(self.market_id)
            if target and others_best and others_best >= target:
                self.jump_active[self.market_id] = False
                self.jump_target[self.market_id] = None
            elif target and target <= 990_000:
                return target

        # JOIN MODE
        if current_price is not None and bid > current_price:
            return bid
        if bid_size > our_resting:
            return bid
        if second_bid is not None:
            return second_bid
        return bid

    # ============================================================
    # TRADING CYCLE (mirrors auto1.py SMTrader)
    # ============================================================

    async def initialize_orders_async(self) -> bool:
        """Place initial orders on both sides."""
        success = True
        sides = [self.active_side] if self.one_side_first_mode else self.SIDES

        # Get poly_mid prices if in price feed mode
        poly_prices = {}
        if self.price_feed == "poly_mid":
            poly_prices = self._get_poly_target_prices()
            if poly_prices.get("yes") and poly_prices.get("no"):
                print(f"Poly mid: YES={self.poly_yes_mid:.3f} -> bid YES@${poly_prices['yes']/1e6:.3f} NO@${poly_prices['no']/1e6:.3f}")

        for side in sides:
            if self.cached_resting[side] > 0:
                continue

            bid, bid_size, _ = self.get_bid_info(side)

            if self.price_feed == "poly_mid" and poly_prices.get(side):
                # Use Polymarket-derived price
                place_price = poly_prices[side]
                # Join-only: cap at current best bid
                if bid is not None and place_price > bid:
                    place_price = bid
            elif bid is not None:
                # Default: place 0.5% above target for queue priority
                place_price = min(bid + 5_000, 990_000)
            else:
                success = False
                continue

            shares = self._shares_from_usdc(self.contract_usdc * self.contract_increment, place_price)
            if shares <= 0:
                success = False
                continue

            oh = await self._place_order(side, place_price, shares)
            if oh:
                self.order_hashes[side] = oh
                self.last_prices[side] = place_price
                self.cycle_start_resting[side] = shares
                self.cached_resting[side] = shares
                print(f"{side.upper()}: Placed @ ${place_price/1e6:.3f}")
                await asyncio.sleep(0.2)
            else:
                success = False

        return success

    def check_fills(self):
        """Detect fills by comparing resting to cycle start."""
        sides = [self.active_side] if self.one_side_first_mode else self.SIDES

        for side in sides:
            resting = self.cached_resting[side]
            start = self.cycle_start_resting[side]
            if start == 0:
                continue

            filled = start - resting
            if filled > self.current_increment[side]:
                target_fill = self._shares_from_usdc(
                    self.contract_usdc * self.contract_increment,
                    self.last_prices[side] or 500_000
                )
                if self.current_increment[side] < target_fill and filled >= target_fill:
                    self.fill_prices[side] = self.last_prices[side]
                self.current_increment[side] = filled

    async def both_filled_async(self) -> bool:
        """Check if both sides have filled their target amount."""
        if self.one_side_first_mode:
            target = self._shares_from_usdc(
                self.contract_usdc * self.contract_increment,
                self.last_prices[self.active_side] or 500_000
            )
            return self.current_increment[self.active_side] >= target

        if self.is_rebalancing:
            if any(self.order_hashes[s] and self.cached_resting[s] > 0 for s in self.SIDES):
                return False
            yes_pos = self.cached_position["yes"]
            no_pos = self.cached_position["no"]
            if yes_pos == no_pos:
                self.is_rebalancing = False
                return True
            await self.rebalance_async(yes_pos, no_pos)
            return False

        # Check if both sides filled
        for side in self.SIDES:
            target = self._shares_from_usdc(
                self.contract_usdc * self.contract_increment,
                self.last_prices[side] or 500_000
            )
            if self.current_increment[side] < target:
                return False

        yes_pos = self.cached_position["yes"]
        no_pos = self.cached_position["no"]
        if yes_pos == no_pos:
            return True

        await self.rebalance_async(yes_pos, no_pos)
        return False

    async def rebalance_async(self, yes_pos: int, no_pos: int):
        """Rebalance positions."""
        print(f"\n⚠️ Position mismatch: YES={yes_pos/1e6:.2f}, NO={no_pos/1e6:.2f}")
        await self.cancel_all_orders_async()

        lagging_side = "yes" if yes_pos < no_pos else "no"
        diff = abs(yes_pos - no_pos)

        self.current_increment = {"yes": 0, "no": 0}
        self.order_hashes = {"yes": None, "no": None}
        self.last_prices = {"yes": None, "no": None}
        self.cycle_start_resting = {"yes": 0, "no": 0}
        self.fill_prices = {"yes": None, "no": None}

        bid, _, _ = self.get_bid_info(lagging_side)
        if bid:
            place_price = min(bid + 5_000, 990_000)
            oh = await self._place_order(lagging_side, place_price, diff)
            if oh:
                self.order_hashes[lagging_side] = oh
                self.last_prices[lagging_side] = place_price
                self.cycle_start_resting[lagging_side] = diff
                self.is_rebalancing = True
                print(f"✓ Rebalancing: Placed {diff/1e6:.2f} {lagging_side.upper()} @ ${place_price/1e6:.3f}")

    async def start_new_cycle_async(self):
        """Start a new trading cycle."""
        if not self.active:
            return

        self.cycles_completed += 1

        if self.single_fire_mode:
            self.single_fire_cycles_completed += 1
            if self.single_fire_cycles_completed >= 1:
                print("✓ Single fire complete - pausing")
                self.paused = True
                self.single_fire_mode = False
                self.single_fire_cycles_completed = 0
                self.waiting_for_manual_resume = True
                return

        self.current_increment = {"yes": 0, "no": 0}
        self.order_hashes = {"yes": None, "no": None}
        self.last_prices = {"yes": None, "no": None}
        self.fill_prices = {"yes": None, "no": None}
        self.cycle_start_resting = {"yes": 0, "no": 0}

        if await self.initialize_orders_async():
            print("✓ New cycle initialized")
        else:
            print("❌ Failed to start new cycle")

    async def update_orders_async(self):
        """Update orders - cancel and replace if price moved."""
        sides = [self.active_side] if self.one_side_first_mode else self.SIDES

        for side in sides:
            if not self.cached_resting[side]:
                continue

            bid, bid_size, second_bid = self.get_bid_info(side)
            target_price = self.check_target_price(
                side, bid, bid_size, second_bid,
                self.last_prices[side], self.cached_resting[side]
            )

            if target_price and self.last_prices[side]:
                # Poly_mid mode: use exact target (already join-capped)
                # Default mode: add 0.5% queue priority bump
                if self.price_feed == "poly_mid":
                    place_price = target_price
                else:
                    place_price = min(target_price + 5_000, 990_000)
                if place_price != self.last_prices[side]:
                    # Cancel old order — only replace if cancel succeeds
                    oh = self.order_hashes[side]
                    if oh:
                        cancelled = await self._cancel_order(oh, side=self.order_sides[side])
                        if not cancelled:
                            continue  # Skip to avoid orphan/duplicate orders
                        self.order_hashes[side] = None
                        await asyncio.sleep(0.1)

                    # Place new order
                    shares = self._shares_from_usdc(
                        self.contract_usdc * self.contract_increment,
                        place_price
                    )
                    if shares > 0:
                        new_oh = await self._place_order(side, place_price, shares)
                        if new_oh:
                            self.order_hashes[side] = new_oh
                            direction = "↑" if place_price > self.last_prices[side] else "↓"
                            print(f"\n{direction} {side.upper()}: ${self.last_prices[side]/1e6:.3f} → ${place_price/1e6:.3f}")
                            self.last_prices[side] = place_price
                            self.cycle_start_resting[side] = self.current_increment[side] + shares
                            self.cached_resting[side] = shares
                        else:
                            self.order_hashes[side] = None
                            self.last_prices[side] = None

    # ============================================================
    # REDIS COMMANDS (same interface as mm_core.py)
    # ============================================================

    async def process_redis_commands_async(self):
        if not self.redis_client or not self.instance_id:
            return

        command_key = f"trading:instance:{self.instance_id}:command"
        while True:
            cmd_data = self.redis_client.lpop(command_key)
            if not cmd_data:
                break

            try:
                cmd = json.loads(cmd_data)
                action = cmd.get("action")

                if action == "toggle_jump":
                    market_idx = cmd.get("market_index", 0)
                    mid = self.market_id
                    self.jump_active[mid] = not self.jump_active.get(mid, False)
                    if not self.jump_active[mid]:
                        self.jump_target[mid] = None
                    print(f"\n{'🔼' if self.jump_active[mid] else '🔽'} Jump {'ON' if self.jump_active[mid] else 'OFF'}")

                elif action == "toggle_pause":
                    if self.paused or self.waiting_for_manual_resume:
                        self.paused = False
                        self.waiting_for_manual_resume = False
                    else:
                        self.paused = True

                elif action == "single_fire":
                    if self.paused or self.waiting_for_manual_resume:
                        self.single_fire_mode = True
                        self.single_fire_cycles_completed = 0
                        self.paused = False

                elif action == "toggle_fair_value":
                    self.fair_value_enabled = not self.fair_value_enabled
                    if not self.fair_value_enabled:
                        self.fair_value_history.clear()
                        self.current_fair_value = None

                elif action == "cancel_orders":
                    await self.cancel_all_orders_async()

                elif action == "stop":
                    self.stopping = True

                elif action == "force_stop":
                    await self.cancel_all_orders_async()
                    self.active = False
                    self.stopping = False

            except Exception as e:
                print(f"Error processing command: {e}")

    # ============================================================
    # STATUS
    # ============================================================

    def format_status_data(self, instance_id: int) -> dict:
        """Format status for WebSocket publishing."""
        return {
            "id": instance_id,
            "status": "running",
            "platform": "turbine",
            "asset": self.asset,
            "market_id": self.market_id[:20] + "..." if len(self.market_id) > 20 else self.market_id,
            "strike": self.strike_price / 1e6 if self.strike_price else 0,
            "time_remaining": self.seconds_remaining(),
            "position": (self.cached_position.get("yes", 0) + self.cached_position.get("no", 0)),
            "pnl": "+$0.00",
            "current_increment": {
                "yes": {"filled": self.current_increment.get("yes", 0), "total": self._shares_from_usdc(self.contract_usdc * self.contract_increment, 500_000)},
                "no": {"filled": self.current_increment.get("no", 0), "total": self._shares_from_usdc(self.contract_usdc * self.contract_increment, 500_000)}
            },
        }

    def print_status(self):
        def p(v):
            return f"${v/1e6:.3f}" if v is not None else "N/A"

        secs = self.seconds_remaining()
        m, s = divmod(secs, 60)
        mode = "PAUSED" if self.paused else ("EXPIRING" if self.market_expiring else ("POLY" if self.price_feed == "poly_mid" else "JOIN"))

        poly_info = ""
        if self.price_feed == "poly_mid" and self.poly_yes_mid is not None:
            poly_info = f" poly:{self.poly_yes_mid:.3f}"

        print(
            f"\r[{mode}] YES: bid{p(self.yes_bid)} ask{p(self.yes_ask)} rest:{self.cached_resting['yes']/1e6:.2f}"
            f" || NO: bid{p(self.no_bid)} ask{p(self.no_ask)} rest:{self.cached_resting['no']/1e6:.2f}"
            f" | {m}:{s:02d}{poly_info}",
            end=""
        )
        sys.stdout.flush()

    # ============================================================
    # CLOSE
    # ============================================================

    async def close_session(self):
        """Clean up."""
        try:
            self.client.close()
        except Exception:
            pass

    async def generate_session_report(self, instance_id, markets, script_type):
        """Placeholder for session reporting - Turbine doesn't use Base reports yet."""
        pass

    # ============================================================
    # MAIN TRADING LOOP
    # ============================================================

    async def run_trading_instance(self, instance_id: int, markets: list):
        """Main async trading loop for webapp."""
        self.session_start_time = datetime.now(timezone.utc).isoformat()
        last_status = time.time()
        last_market_check = time.time()

        while self.running and self.active:
            await self.process_redis_commands_async()

            if not self.active:
                break

            # Check for market rotation every 5 seconds
            if time.time() - last_market_check >= MARKET_POLL_SECONDS:
                transitioned = await self.check_market_transition()
                last_market_check = time.time()
                if transitioned and not self.paused:
                    await self.initialize_orders_async()

            # Check market expiry
            secs = self.seconds_remaining()
            if secs < END_OF_MARKET_STOP_SECONDS and not self.market_expiring:
                self.market_expiring = True
                if any(self.order_hashes[s] for s in self.SIDES):
                    print(f"\n⏰ Market expiring in {secs}s — pulling orders")
                    await self.cancel_all_orders_async()

            await self.refresh_market_data_async()
            self.check_fills()

            if not self.active:
                break

            if self.market_expiring:
                pass  # Wait for market transition
            elif self.stopping:
                if await self.both_filled_async():
                    await self.cancel_all_orders_async()
                    self.active = False
                    break
            elif self.paused:
                await self.update_orders_async()
            elif self.waiting_for_manual_resume:
                if await self.initialize_orders_async():
                    pass
                self.waiting_for_manual_resume = False
            elif await self.both_filled_async():
                if not self.active:
                    break
                await self.start_new_cycle_async()
            else:
                await self.update_orders_async()

            if time.time() - last_status >= 1:
                self.print_status()
                last_status = time.time()

            await asyncio.sleep(0.5)

        await self.close_session()
        print("\n✓ Trading stopped")
