market-maker

Create a market maker bot for Turbine's BTC 15-minute prediction markets. Use when building trading bots for Turbine.

16 stars

Best use case

market-maker is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Create a market maker bot for Turbine's BTC 15-minute prediction markets. Use when building trading bots for Turbine.

Teams using market-maker should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/market-maker/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/tools/market-maker/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/market-maker/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How market-maker Compares

Feature / Agentmarket-makerStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Create a market maker bot for Turbine's BTC 15-minute prediction markets. Use when building trading bots for Turbine.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Turbine Market Maker Bot Generator

You are helping a programmer create a market maker bot for Turbine's Bitcoin 15-minute prediction markets.

## Step 0: Environment Context Detection

**CRITICAL**: Before writing ANY Python code, you MUST detect the user's environment to ensure correct syntax and compatibility.

Run these commands to gather environment context:

```bash
# Get Python version
python3 --version

# Check if in virtualenv
echo "VIRTUAL_ENV: $VIRTUAL_ENV"

# Get platform info
uname -s

# Check if pyproject.toml exists for project Python requirements
cat pyproject.toml 2>/dev/null | grep -E "(requires-python|python_version)" || echo "No pyproject.toml found"
```

**Environment Rules:**
- If Python version is 3.9+: Use modern syntax (type hints with `list[str]` instead of `List[str]`, `dict[str, int]` instead of `Dict[str, int]`, `X | None` instead of `Optional[X]`)
- If Python version is 3.8 or below: Use `from typing import List, Dict, Optional` and older syntax
- Always match the project's `requires-python` if specified in pyproject.toml
- Use `async`/`await` syntax (supported in all Python 3.9+ environments)
- For dataclasses, use `@dataclass` decorator (available in Python 3.7+)

Store the detected Python version mentally and use it for ALL generated code in this session.

## Step 1: Environment Setup Check

First, check if the user has the required setup:

1. Check if `turbine_client` is importable by looking at the project structure
2. Check if `.env` file exists with the required credentials
3. If `.env` doesn't exist, guide them through creating it

## Step 2: Private Key Setup

Check if .env file exists with TURBINE_PRIVATE_KEY set. If not:

1. Use AskUserQuestion to ask for their Ethereum wallet private key
2. Explain security best practices:
   - Use a dedicated trading wallet with limited funds
   - Never share your private key
   - Get it from MetaMask: Settings > Security & Privacy > Export Private Key
3. Once they provide it, CREATE the .env file directly using the Write tool

Do NOT just tell them to create the file - actually create it for them!

## Step 3: API Key Auto-Registration

The bot should automatically register for API credentials on first run AND save them to the .env file automatically. Use this pattern:

```python
import os
import re
from pathlib import Path
from turbine_client import TurbineClient

def get_or_create_api_credentials(env_path: Path = None):
    """Get existing credentials or register new ones and save to .env."""
    if env_path is None:
        env_path = Path(__file__).parent / ".env"

    api_key_id = os.environ.get("TURBINE_API_KEY_ID")
    api_private_key = os.environ.get("TURBINE_API_PRIVATE_KEY")

    if api_key_id and api_private_key:
        print("Using existing API credentials")
        return api_key_id, api_private_key

    # Register new credentials
    private_key = os.environ.get("TURBINE_PRIVATE_KEY")
    if not private_key:
        raise ValueError("TURBINE_PRIVATE_KEY not set in environment")

    print("Registering new API credentials...")
    credentials = TurbineClient.request_api_credentials(
        host="https://api.turbinefi.com",
        private_key=private_key,
    )

    api_key_id = credentials["api_key_id"]
    api_private_key = credentials["api_private_key"]

    # Auto-save credentials to .env file
    _save_credentials_to_env(env_path, api_key_id, api_private_key)

    # Update current environment so bot can use them immediately
    os.environ["TURBINE_API_KEY_ID"] = api_key_id
    os.environ["TURBINE_API_PRIVATE_KEY"] = api_private_key

    print(f"API credentials registered and saved to {env_path}")
    return api_key_id, api_private_key


def _save_credentials_to_env(env_path: Path, api_key_id: str, api_private_key: str):
    """Save API credentials to .env file."""
    env_path = Path(env_path)

    if env_path.exists():
        content = env_path.read_text()
        # Update or append TURBINE_API_KEY_ID
        if "TURBINE_API_KEY_ID=" in content:
            content = re.sub(r'^TURBINE_API_KEY_ID=.*$', f'TURBINE_API_KEY_ID={api_key_id}', content, flags=re.MULTILINE)
        else:
            content = content.rstrip() + f"\nTURBINE_API_KEY_ID={api_key_id}"
        # Update or append TURBINE_API_PRIVATE_KEY
        if "TURBINE_API_PRIVATE_KEY=" in content:
            content = re.sub(r'^TURBINE_API_PRIVATE_KEY=.*$', f'TURBINE_API_PRIVATE_KEY={api_private_key}', content, flags=re.MULTILINE)
        else:
            content = content.rstrip() + f"\nTURBINE_API_PRIVATE_KEY={api_private_key}"
        env_path.write_text(content + "\n")
    else:
        # Create new .env file
        content = f"""# Turbine Trading Bot Configuration
TURBINE_PRIVATE_KEY={os.environ.get('TURBINE_PRIVATE_KEY', '0x...')}
TURBINE_API_KEY_ID={api_key_id}
TURBINE_API_PRIVATE_KEY={api_private_key}
"""
        env_path.write_text(content)
```

## Step 4: Algorithm Selection

Present the user with these trading algorithm options for prediction markets:

**Option 1: Price Action Trader (Recommended)**
- Uses real-time BTC price from Pyth Network (same oracle Turbine uses)
- Compares current price to the market's strike price
- If BTC is above strike price → buy YES (bet it stays above)
- If BTC is below strike price → buy NO (bet it stays below)
- Adjusts confidence based on how far price is from strike
- Best for: Beginners, following price momentum
- Risk: Medium - follows current price action

**Option 2: Simple Spread Market Maker**
- Places bid and ask orders around the mid-price with a fixed spread
- Best for: Learning the basics, stable markets
- Risk: Medium - can accumulate inventory in trending markets

**Option 3: Inventory-Aware Market Maker**
- Adjusts quotes based on current position to reduce inventory risk
- Skews prices to encourage trades that reduce position
- Best for: Balanced exposure, risk management
- Risk: Lower - actively manages inventory

**Option 4: Momentum-Following Trader**
- Detects price direction from recent trades
- Buys when momentum is up, sells when momentum is down
- Best for: Trending markets, breakouts
- Risk: Higher - can be wrong on reversals

**Option 5: Mean Reversion Trader**
- Fades large moves expecting price to revert
- Buys after dips, sells after spikes
- Best for: Range-bound markets, overreactions
- Risk: Higher - can fight strong trends

**Option 6: Probability-Weighted Trader**
- Uses distance from 50% as a signal
- Bets on extremes reverting toward uncertainty
- Best for: Markets with overconfident pricing
- Risk: Medium - based on market efficiency assumptions

## Step 5: Generate the Bot Code

Based on the user's algorithm choice, generate a complete bot file. The bot should:

1. Load credentials from environment variables
2. Auto-register API keys if needed
3. Connect to the BTC 15-minute quick market
4. Implement the chosen algorithm
5. Include proper error handling
6. Cancel orders on shutdown
7. **Automatically detect new BTC markets and switch liquidity/trades to them**
8. Handle market expiration gracefully with seamless transitions
9. **Sign USDC permits for gasless order execution** (no separate approval transaction needed)
10. **Track traded markets and automatically claim winnings when they resolve**

Use this template structure for all bots:

```python
"""
Turbine Market Maker Bot - {ALGORITHM_NAME}
Generated for Turbine

Algorithm: {ALGORITHM_DESCRIPTION}
"""

import asyncio
import os
import re
import time
from pathlib import Path
from dotenv import load_dotenv
import httpx  # For Price Action Trader - fetching BTC price from Pyth Network

from turbine_client import TurbineClient, TurbineWSClient, Outcome, Side
from turbine_client.exceptions import TurbineApiError, WebSocketError

# Load environment variables
load_dotenv()

# ============================================================
# CONFIGURATION - Adjust these parameters for your strategy
# ============================================================
ORDER_SIZE = 1_000_000  # 1 share (6 decimals)
MAX_POSITION = 5_000_000  # Maximum position size (5 shares)
QUOTE_REFRESH_SECONDS = 30  # How often to refresh quotes
# Algorithm-specific parameters added here...

def get_or_create_api_credentials(env_path: Path = None):
    """Get existing credentials or register new ones and save to .env."""
    if env_path is None:
        env_path = Path(__file__).parent / ".env"

    api_key_id = os.environ.get("TURBINE_API_KEY_ID")
    api_private_key = os.environ.get("TURBINE_API_PRIVATE_KEY")

    if api_key_id and api_private_key:
        print("Using existing API credentials")
        return api_key_id, api_private_key

    private_key = os.environ.get("TURBINE_PRIVATE_KEY")
    if not private_key:
        raise ValueError("Set TURBINE_PRIVATE_KEY in your .env file")

    print("Registering new API credentials...")
    credentials = TurbineClient.request_api_credentials(
        host="https://api.turbinefi.com",
        private_key=private_key,
    )

    api_key_id = credentials["api_key_id"]
    api_private_key = credentials["api_private_key"]

    # Auto-save to .env
    _save_credentials_to_env(env_path, api_key_id, api_private_key)
    os.environ["TURBINE_API_KEY_ID"] = api_key_id
    os.environ["TURBINE_API_PRIVATE_KEY"] = api_private_key

    print(f"API credentials saved to {env_path}")
    return api_key_id, api_private_key


def _save_credentials_to_env(env_path: Path, api_key_id: str, api_private_key: str):
    """Save API credentials to .env file."""
    env_path = Path(env_path)

    if env_path.exists():
        content = env_path.read_text()
        # Update or append each credential
        if "TURBINE_API_KEY_ID=" in content:
            content = re.sub(r'^TURBINE_API_KEY_ID=.*$', f'TURBINE_API_KEY_ID={api_key_id}', content, flags=re.MULTILINE)
        else:
            content = content.rstrip() + f"\nTURBINE_API_KEY_ID={api_key_id}"
        if "TURBINE_API_PRIVATE_KEY=" in content:
            content = re.sub(r'^TURBINE_API_PRIVATE_KEY=.*$', f'TURBINE_API_PRIVATE_KEY={api_private_key}', content, flags=re.MULTILINE)
        else:
            content = content.rstrip() + f"\nTURBINE_API_PRIVATE_KEY={api_private_key}"
        env_path.write_text(content + "\n")
    else:
        content = f"# Turbine Bot Config\nTURBINE_PRIVATE_KEY={os.environ.get('TURBINE_PRIVATE_KEY', '')}\nTURBINE_API_KEY_ID={api_key_id}\nTURBINE_API_PRIVATE_KEY={api_private_key}\n"
        env_path.write_text(content)


class MarketMakerBot:
    """Market maker bot implementation with automatic market switching and winnings claiming."""

    def __init__(self, client: TurbineClient):
        self.client = client
        self.market_id: str | None = None
        self.settlement_address: str | None = None  # For USDC permits
        self.contract_address: str | None = None  # For claiming winnings
        self.strike_price: int = 0  # BTC price when market created (8 decimals) - used by Price Action Trader
        self.current_position = 0
        self.active_orders: dict[str, str] = {}  # order_hash -> side
        self.running = True
        # Track markets we've traded in for claiming winnings
        self.traded_markets: dict[str, str] = {}  # market_id -> contract_address
        # Algorithm state...

    async def get_active_market(self) -> tuple[str, int, int]:
        """
        Get the currently active BTC quick market.
        Returns (market_id, end_time, start_price) tuple.
        """
        quick_market = self.client.get_quick_market("BTC")
        return quick_market.market_id, quick_market.end_time, quick_market.start_price

    async def cancel_all_orders(self, market_id: str) -> None:
        """Cancel all active orders on a market before switching."""
        if not self.active_orders:
            return

        print(f"Cancelling {len(self.active_orders)} orders on market {market_id[:8]}...")
        for order_id in list(self.active_orders.keys()):
            try:
                self.client.cancel_order(market_id=market_id, order_id=order_id)
                del self.active_orders[order_id]
            except TurbineApiError as e:
                print(f"Failed to cancel order {order_id}: {e}")

    async def switch_to_new_market(self, new_market_id: str, start_price: int = 0) -> None:
        """
        Switch liquidity and trading to a new market.
        Called when a new BTC 15-minute market becomes active.

        Args:
            new_market_id: The new market ID to switch to.
            start_price: The BTC price when market was created (8 decimals).
                         Used by Price Action Trader to compare against current price.
        """
        old_market_id = self.market_id

        # Track old market for claiming winnings later
        if old_market_id and self.contract_address:
            self.traded_markets[old_market_id] = self.contract_address
            print(f"Tracking market {old_market_id[:8]}... for winnings claim")

        if old_market_id:
            print(f"\n{'='*50}")
            print(f"MARKET TRANSITION DETECTED")
            print(f"Old market: {old_market_id[:8]}...")
            print(f"New market: {new_market_id[:8]}...")
            print(f"{'='*50}\n")

            # Cancel all orders on the old market
            await self.cancel_all_orders(old_market_id)

        # Update to new market
        self.market_id = new_market_id
        self.strike_price = start_price  # Store for Price Action Trader
        self.active_orders = {}

        # Fetch settlement and contract addresses from markets list
        try:
            markets = self.client.get_markets()
            for market in markets:
                if market.id == new_market_id:
                    self.settlement_address = market.settlement_address
                    self.contract_address = market.contract_address
                    print(f"Settlement: {self.settlement_address[:16]}...")
                    print(f"Contract: {self.contract_address[:16]}...")
                    break
        except Exception as e:
            print(f"Warning: Could not fetch market addresses: {e}")

        strike_usd = start_price / 1e8 if start_price else 0
        print(f"Now trading on market: {new_market_id[:8]}...")
        if strike_usd > 0:
            print(f"Strike price: ${strike_usd:,.2f}")

    async def monitor_market_transitions(self) -> None:
        """
        Background task that polls for new markets and triggers transitions.
        Runs continuously while the bot is active.
        """
        POLL_INTERVAL = 5  # Check every 5 seconds

        while self.running:
            try:
                new_market_id, end_time, start_price = await self.get_active_market()

                # Check if market has changed
                if new_market_id != self.market_id:
                    await self.switch_to_new_market(new_market_id, start_price)

                # Log time remaining periodically
                time_remaining = end_time - int(time.time())
                if time_remaining <= 60 and time_remaining > 0:
                    print(f"Market expires in {time_remaining}s - preparing for transition...")

            except Exception as e:
                print(f"Market monitor error: {e}")

            await asyncio.sleep(POLL_INTERVAL)

    # ... Algorithm implementation ...


async def main():
    # Get credentials
    private_key = os.environ.get("TURBINE_PRIVATE_KEY")
    if not private_key:
        print("Error: Set TURBINE_PRIVATE_KEY in your .env file")
        return

    api_key_id, api_private_key = get_or_create_api_credentials()

    # Create client
    client = TurbineClient(
        host="https://api.turbinefi.com",
        chain_id=137,  # Polygon mainnet
        private_key=private_key,
        api_key_id=api_key_id,
        api_private_key=api_private_key,
    )

    print(f"Bot wallet address: {client.address}")

    # Get the initial active BTC 15-minute market
    quick_market = client.get_quick_market("BTC")
    print(f"Initial market: BTC @ ${quick_market.start_price / 1e8:,.2f}")
    print(f"Market expires at: {quick_market.end_time}")

    # Note gasless features
    print("Orders will include USDC permit signatures for gasless trading")
    print("Automatic winnings claim enabled for resolved markets")
    print()

    # Run the bot with automatic market switching and winnings claiming
    bot = MarketMakerBot(client)

    try:
        # Initialize with the current market (pass start_price for Price Action Trader)
        await bot.switch_to_new_market(quick_market.market_id, quick_market.start_price)

        # Run the main trading loop (starts background tasks internally)
        await bot.run("https://api.turbinefi.com")
    except KeyboardInterrupt:
        print("\nShutting down...")
    finally:
        bot.running = False
        # Cancel any remaining orders before exit
        if bot.market_id:
            await bot.cancel_all_orders(bot.market_id)
        client.close()
        print("Bot stopped cleanly.")


if __name__ == "__main__":
    asyncio.run(main())
```

## Step 6: Create the .env File and Install Dependencies

IMPORTANT: Actually create the .env file for the user using the Write tool. Do NOT just tell them to copy a template.

Ask the user for their Ethereum private key using AskUserQuestion, then:

1. Create the `.env` file directly with their private key:
```
# Turbine Trading Bot Configuration
TURBINE_PRIVATE_KEY=0x...user's_actual_key...
TURBINE_API_KEY_ID=
TURBINE_API_PRIVATE_KEY=
```

2. Install dependencies by running:
```bash
pip install -e . python-dotenv httpx
```

Note: `httpx` is used by the Price Action Trader to fetch real-time BTC prices from Pyth Network.

## Step 7: Explain How to Run

Tell the user:
```
Your bot is ready! To run it:

  python {bot_filename}.py

The bot will:
- Automatically register API credentials on first run (saved to .env)
- Connect to the current BTC 15-minute market
- Start trading based on your chosen algorithm
- Sign USDC permits for gasless order execution (no approval TX needed)
- Automatically switch to new markets when they start
- Track traded markets and claim winnings when they resolve

To stop the bot, press Ctrl+C.
```

## Core Bot Run Method

Every generated bot must include this `run()` method that handles WebSocket streaming with automatic market switching and winnings claiming:

```python
async def run(self, host: str) -> None:
    """
    Main trading loop with WebSocket streaming, automatic market switching, and winnings claiming.
    """
    ws = TurbineWSClient(host)

    # Start background tasks
    monitor_task = asyncio.create_task(self.monitor_market_transitions())
    claim_task = asyncio.create_task(self.claim_resolved_markets())

    while self.running:
        try:
            # Ensure we have a current market
            if not self.market_id:
                market_id, _ = await self.get_active_market()
                await self.switch_to_new_market(market_id)

            current_market = self.market_id

            async with ws.connect() as stream:
                # Subscribe to the current market
                await stream.subscribe(current_market)
                print(f"Subscribed to market {current_market[:8]}...")

                # Place initial quotes (with USDC permits)
                await self.place_quotes()

                async for message in stream:
                    # Check if market has changed (set by monitor task)
                    if self.market_id != current_market:
                        print("Market changed, reconnecting to new market...")
                        break  # Exit inner loop to reconnect

                    if message.type == "orderbook":
                        await self.on_orderbook_update(message.orderbook)
                    elif message.type == "trade":
                        await self.on_trade(message.trade)
                    elif message.type == "order_cancelled":
                        self.on_order_cancelled(message.data)

        except WebSocketError as e:
            print(f"WebSocket error: {e}, reconnecting...")
            await asyncio.sleep(1)
        except Exception as e:
            print(f"Unexpected error: {e}")
            await asyncio.sleep(5)

    # Cleanup background tasks
    monitor_task.cancel()
    claim_task.cancel()
```

## Algorithm Implementation Details

When generating bots, use these implementations:

### Price Action Trader (Recommended)

This algorithm fetches the current BTC price from **Pyth Network** (the same oracle Turbine uses) and compares it to the market's strike price to make trading decisions.

```python
import httpx

# Pyth Network Hermes API - same price source Turbine uses
PYTH_HERMES_URL = "https://hermes.pyth.network/v2/updates/price/latest"
PYTH_BTC_FEED_ID = "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"

# Configuration
PRICE_THRESHOLD_BPS = 50  # 0.5% threshold before taking action
MIN_CONFIDENCE = 0.6  # Minimum confidence to place a trade
MAX_CONFIDENCE = 0.9  # Cap confidence at 90%
PRICE_POLL_SECONDS = 10  # How often to check price

class PriceActionBot:
    def __init__(self, client: TurbineClient):
        self.client = client
        self.market_id: str | None = None
        self.strike_price: int = 0  # BTC price when market created (8 decimals)
        self.current_position = 0
        self.active_orders: dict[str, str] = {}
        self.running = True
        self.traded_markets: dict[str, str] = {}
        self.settlement_address: str | None = None
        self.contract_address: str | None = None

    def get_current_btc_price(self) -> float:
        """Fetch current BTC price from Pyth Network (same source as Turbine)."""
        try:
            response = httpx.get(
                PYTH_HERMES_URL,
                params={"ids[]": PYTH_BTC_FEED_ID},
                timeout=5.0,
            )
            response.raise_for_status()
            data = response.json()

            if not data.get("parsed"):
                print("No price data from Pyth")
                return 0.0

            price_data = data["parsed"][0]["price"]
            price_int = int(price_data["price"])
            expo = price_data["expo"]  # Usually -8 for BTC

            # Convert Pyth price to USD: price * 10^expo
            return price_int * (10 ** expo)

        except Exception as e:
            print(f"Failed to fetch BTC price from Pyth: {e}")
            return 0.0

    def calculate_signal(self) -> tuple[str, float]:
        """
        Calculate trading signal based on current price vs strike price.

        Returns:
            (action, confidence) where action is "BUY_YES", "BUY_NO", or "HOLD"
        """
        current_price = self.get_current_btc_price()
        if current_price <= 0:
            return "HOLD", 0.0

        # Convert strike price from 8 decimals to USD
        strike_usd = self.strike_price / 1e8

        # Calculate percentage difference
        price_diff_pct = ((current_price - strike_usd) / strike_usd) * 100

        # Threshold check (0.5% = 50 bps)
        threshold_pct = PRICE_THRESHOLD_BPS / 100

        if abs(price_diff_pct) < threshold_pct:
            # Price too close to strike, hold
            return "HOLD", 0.0

        # Calculate confidence based on distance from strike
        # Further from strike = higher confidence (capped)
        raw_confidence = min(abs(price_diff_pct) / 2, MAX_CONFIDENCE)
        confidence = max(raw_confidence, MIN_CONFIDENCE) if abs(price_diff_pct) >= threshold_pct else 0.0

        if price_diff_pct > 0:
            # BTC is above strike → bet YES (will end above)
            print(f"BTC ${current_price:,.2f} is {price_diff_pct:+.2f}% above strike ${strike_usd:,.2f}")
            return "BUY_YES", confidence
        else:
            # BTC is below strike → bet NO (will end below)
            print(f"BTC ${current_price:,.2f} is {price_diff_pct:+.2f}% below strike ${strike_usd:,.2f}")
            return "BUY_NO", confidence

    async def execute_signal(self, action: str, confidence: float) -> None:
        """Execute the trading signal."""
        if action == "HOLD" or confidence < MIN_CONFIDENCE:
            return

        # Check position limits
        if abs(self.current_position) >= MAX_POSITION:
            print("Position limit reached")
            return

        # Get orderbook to determine price
        orderbook = self.client.get_orderbook(self.market_id)

        if action == "BUY_YES":
            # Buy YES outcome
            if not orderbook.asks:
                return
            # Pay slightly above best ask to ensure fill
            price = min(orderbook.asks[0].price + 5000, 999000)
            outcome = Outcome.YES
        else:
            # Buy NO outcome
            if not orderbook.asks:
                return
            price = min(orderbook.asks[0].price + 5000, 999000)
            outcome = Outcome.NO

        try:
            order = self.client.create_limit_buy(
                market_id=self.market_id,
                outcome=outcome,
                price=price,
                size=ORDER_SIZE,
                expiration=int(time.time()) + 300,
                settlement_address=self.settlement_address,
            )

            # Sign USDC permit for gasless execution
            buyer_cost = (ORDER_SIZE * price) // 1_000_000
            permit_amount = (buyer_cost * 120) // 100  # 20% margin
            permit = self.client.sign_usdc_permit(
                value=permit_amount,
                settlement_address=self.settlement_address,
            )
            order.permit_signature = permit

            result = self.client.post_order(order)
            outcome_str = "YES" if outcome == Outcome.YES else "NO"
            print(f"Placed {outcome_str} order @ {price / 10000:.1f}% (confidence: {confidence:.0%})")

            # Track position
            self.current_position += ORDER_SIZE if outcome == Outcome.YES else -ORDER_SIZE
            self.active_orders[order.order_hash] = action

        except TurbineApiError as e:
            print(f"Order failed: {e}")

    async def price_action_loop(self) -> None:
        """Main loop that monitors price and executes trades."""
        while self.running and self.market_id:
            try:
                action, confidence = self.calculate_signal()
                if action != "HOLD":
                    await self.execute_signal(action, confidence)
                await asyncio.sleep(PRICE_POLL_SECONDS)
            except Exception as e:
                print(f"Price action error: {e}")
                await asyncio.sleep(PRICE_POLL_SECONDS)
```

**Key points for Price Action Trader:**
- Uses Pyth Network Hermes API (same oracle Turbine uses) to get real-time BTC price
- Compares current price to strike price (stored in `quick_market.start_price`)
- If BTC > strike by threshold → buy YES
- If BTC < strike by threshold → buy NO
- Confidence scales with distance from strike (capped at 90%)
- Polls price every 10 seconds by default

### Simple Spread Market Maker
```python
SPREAD_BPS = 200  # 2% total spread (1% each side)

def calculate_quotes(self, mid_price):
    """Calculate bid/ask around mid price."""
    half_spread = (mid_price * SPREAD_BPS) // 20000
    bid = max(1, mid_price - half_spread)
    ask = min(999999, mid_price + half_spread)
    return bid, ask
```

### Inventory-Aware Market Maker
```python
SPREAD_BPS = 200
SKEW_FACTOR = 50  # BPS skew per share of inventory

def calculate_quotes(self, mid_price):
    """Skew quotes based on inventory."""
    half_spread = (mid_price * SPREAD_BPS) // 20000

    # Skew to reduce inventory
    inventory_shares = self.current_position / 1_000_000
    skew = int(inventory_shares * SKEW_FACTOR)

    bid = max(1, mid_price - half_spread - skew)
    ask = min(999999, mid_price + half_spread - skew)
    return bid, ask
```

### Momentum Following
```python
MOMENTUM_WINDOW = 10  # Number of trades to consider
MOMENTUM_THRESHOLD = 0.6  # 60% same direction = trend

def detect_momentum(self, recent_trades):
    """Detect market momentum from recent trades."""
    if len(recent_trades) < MOMENTUM_WINDOW:
        return None

    buys = sum(1 for t in recent_trades[-MOMENTUM_WINDOW:] if t["side"] == "BUY")
    buy_ratio = buys / MOMENTUM_WINDOW

    if buy_ratio > MOMENTUM_THRESHOLD:
        return "UP"
    elif buy_ratio < (1 - MOMENTUM_THRESHOLD):
        return "DOWN"
    return None
```

### Mean Reversion
```python
REVERSION_THRESHOLD = 50000  # 5% move triggers fade
LOOKBACK_TRADES = 20

def should_fade(self, current_price, recent_trades):
    """Check if price moved enough to fade."""
    if len(recent_trades) < LOOKBACK_TRADES:
        return None

    avg_price = sum(t["price"] for t in recent_trades) / len(recent_trades)
    deviation = current_price - avg_price

    if deviation > REVERSION_THRESHOLD:
        return "SELL"  # Fade the up move
    elif deviation < -REVERSION_THRESHOLD:
        return "BUY"  # Fade the down move
    return None
```

### Probability-Weighted
```python
EDGE_THRESHOLD = 200000  # 20% from 50% = extreme

def find_edge(self, best_bid, best_ask):
    """Look for mispriced extremes."""
    mid = (best_bid + best_ask) // 2
    distance_from_fair = abs(mid - 500000)

    if distance_from_fair > EDGE_THRESHOLD:
        if mid > 500000:
            return "SELL"  # Market too bullish
        else:
            return "BUY"  # Market too bearish
    return None
```

## Automatic Market Transition

**All generated bots automatically handle market transitions.** When a BTC 15-minute market expires:

1. **Detection**: The bot polls every 5 seconds for new markets
2. **Order Cleanup**: All active orders on the expiring market are cancelled
3. **Seamless Switch**: The bot automatically connects to the new market
4. **Continued Trading**: Trading resumes on the new market without manual intervention

**How it works:**
- A background task (`monitor_market_transitions`) runs continuously
- It compares the current market ID with the active market from the API
- When a new market is detected, `switch_to_new_market()` handles the transition
- Positions carry over (they're wallet-based), but orders must be re-placed

**Warning before expiration:**
- When less than 60 seconds remain, the bot logs a warning
- Orders are cancelled proactively to avoid stuck orders on expired markets

## USDC Permit Signatures (Gasless Trading)

**Every order must include a USDC permit signature** for gasless execution. Without this, orders will fail with "ERC20: transfer amount exceeds allowance".

The `TurbineClient` provides `sign_usdc_permit()` to create EIP-2612 permit signatures:

```python
async def place_quotes(self) -> None:
    """Place bid and ask orders with USDC permit signatures."""
    bid_price, ask_price = self.calculate_quotes()

    # Place bid (buy YES)
    bid_order = self.client.create_limit_buy(
        market_id=self.market_id,
        outcome=Outcome.YES,
        price=bid_price,
        size=ORDER_SIZE,
        expiration=int(time.time()) + QUOTE_REFRESH_SECONDS + 60,
        settlement_address=self.settlement_address,
    )

    # Calculate permit amount for BUY orders:
    # (size * price / 1e6) + 1% fee + 10% safety margin
    buyer_cost = (ORDER_SIZE * bid_price) // 1_000_000
    total_fee = ORDER_SIZE // 100  # 1% fee
    permit_amount = ((buyer_cost + total_fee) * 110) // 100

    # Sign and attach USDC permit
    permit = self.client.sign_usdc_permit(
        value=permit_amount,
        settlement_address=self.settlement_address,
    )
    bid_order.permit_signature = permit

    result = self.client.post_order(bid_order)

    # Place ask (sell YES)
    ask_order = self.client.create_limit_sell(
        market_id=self.market_id,
        outcome=Outcome.YES,
        price=ask_price,
        size=ORDER_SIZE,
        expiration=int(time.time()) + QUOTE_REFRESH_SECONDS + 60,
        settlement_address=self.settlement_address,
    )

    # Calculate permit amount for SELL orders: size + 10% margin
    permit_amount = (ORDER_SIZE * 110) // 100

    permit = self.client.sign_usdc_permit(
        value=permit_amount,
        settlement_address=self.settlement_address,
    )
    ask_order.permit_signature = permit

    result = self.client.post_order(ask_order)
```

**Key points:**
- BUY orders need permit for: `(size * price / 1e6) + fee`
- SELL orders need permit for: `size` (for JIT token splitting)
- Always add a 10% safety margin to permit amounts
- Permits are signed per-order with the settlement address as spender

## Automatic Winnings Claiming

**Bots must track markets they've traded in and automatically claim winnings when markets resolve.**

### Implementation Pattern

Add these fields to your bot class:

```python
class MarketMakerBot:
    def __init__(self, client: TurbineClient):
        self.client = client
        self.market_id: str | None = None
        self.settlement_address: str | None = None
        self.contract_address: str | None = None  # Current market contract
        self.current_position = 0
        self.active_orders: dict[str, str] = {}
        self.running = True
        # Track markets we've traded in for claiming winnings
        # market_id -> contract_address
        self.traded_markets: dict[str, str] = {}
```

### Track Markets When Switching

When switching to a new market, save the old market for later claiming:

```python
async def switch_to_new_market(self, new_market_id: str, start_price: int = 0) -> None:
    """Switch liquidity to a new market.

    Args:
        new_market_id: The new market ID.
        start_price: BTC strike price (8 decimals) - used by Price Action Trader.
    """
    old_market_id = self.market_id

    # Track old market for claiming winnings later
    if old_market_id and self.contract_address:
        self.traded_markets[old_market_id] = self.contract_address
        print(f"Tracking market {old_market_id[:16]}... for winnings claim")

    if old_market_id:
        await self.cancel_all_orders()

    self.market_id = new_market_id
    self.strike_price = start_price  # Store for Price Action Trader
    self.active_orders = {}

    # Fetch settlement and contract addresses
    markets = self.client.get_markets()
    for market in markets:
        if market.id == new_market_id:
            self.settlement_address = market.settlement_address
            self.contract_address = market.contract_address
            break

    if start_price:
        print(f"Strike price: ${start_price / 1e8:,.2f}")
```

### Background Task for Claiming

Add a background task that checks for resolved markets and claims winnings:

```python
async def claim_resolved_markets(self) -> None:
    """Background task to claim winnings from resolved markets."""
    while self.running:
        try:
            if not self.traded_markets:
                await asyncio.sleep(30)
                continue

            markets_to_remove = []
            for market_id, contract_address in list(self.traded_markets.items()):
                try:
                    # Check if market is resolved
                    markets = self.client.get_markets()
                    market_resolved = False
                    for market in markets:
                        if market.id == market_id and market.resolved:
                            market_resolved = True
                            break

                    if market_resolved:
                        print(f"\nMarket {market_id[:16]}... has resolved!")
                        print(f"Attempting to claim winnings...")
                        try:
                            result = self.client.claim_winnings(contract_address)
                            tx_hash = result.get("txHash", result.get("tx_hash", "unknown"))
                            print(f"Winnings claimed! TX: {tx_hash}")
                            markets_to_remove.append(market_id)
                        except TurbineApiError as e:
                            if "no winnings" in str(e).lower() or "no position" in str(e).lower():
                                print(f"No winnings to claim for {market_id[:16]}...")
                                markets_to_remove.append(market_id)
                            else:
                                print(f"Failed to claim winnings: {e}")
                except Exception as e:
                    print(f"Error checking market {market_id[:16]}...: {e}")

            # Remove claimed markets from tracking
            for market_id in markets_to_remove:
                self.traded_markets.pop(market_id, None)

        except Exception as e:
            print(f"Claim monitor error: {e}")

        await asyncio.sleep(30)  # Check every 30 seconds
```

### Start the Claim Task

In the `run()` method, start the claim task alongside other background tasks:

```python
async def run(self, host: str) -> None:
    """Main trading loop with automatic market switching and winnings claiming."""
    ws = TurbineWSClient(host=host)

    # Start background tasks
    monitor_task = asyncio.create_task(self.monitor_market_transitions())
    claim_task = asyncio.create_task(self.claim_resolved_markets())

    try:
        # ... main trading loop ...
    finally:
        monitor_task.cancel()
        claim_task.cancel()
```

**Key points:**
- `claim_winnings(contract_address)` uses gasless EIP-712 permits
- The API handles all on-chain redemption via a relayer
- Markets are removed from tracking after successful claim or if no position exists
- Check every 30 seconds to catch resolutions promptly

## Important Notes for Users

- **Risk Warning**: Trading involves risk. Start with small sizes.
- **Testnet First**: Consider testing on Base Sepolia (chain_id=84532) first.
- **Monitor Positions**: Always monitor your bot and have stop-loss logic.
- **Market Expiration**: BTC 15-minute markets expire quickly. Bots handle this automatically!
- **Gas/Fees**: Trading on Polygon has minimal gas costs but watch for fees.
- **Continuous Operation**: Bots are designed to run 24/7, switching between markets automatically.

## Quick Reference

**Price Scaling**: Prices are 0-1,000,000 representing 0-100%
- 500000 = 50% probability
- 250000 = 25% probability

**Size Scaling**: Sizes use 6 decimals
- 1_000_000 = 1 share
- 500_000 = 0.5 shares

**Outcome Values**:
- Outcome.YES (0) = BTC ends ABOVE strike price
- Outcome.NO (1) = BTC ends BELOW strike price

**Strike Price (for Price Action Trader)**:
- Available via `quick_market.start_price` (8 decimals)
- Example: 9500000000000 = $95,000.00
- Current BTC price fetched from Pyth Network (same oracle Turbine uses):
  - URL: `https://hermes.pyth.network/v2/updates/price/latest?ids[]=0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43`
  - BTC Feed ID: `0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43`
- If current > strike → buy YES, if current < strike → buy NO

Related Skills

remarkety-automation

16
from diegosouzapw/awesome-omni-skill

Automate Remarkety tasks via Rube MCP (Composio). Always search tools first for current schemas.

plugin-marketplace

16
from diegosouzapw/awesome-omni-skill

Plugin marketplace — add, remove, list, and search skills from GitHub repositories

marketplace-mcp

16
from diegosouzapw/awesome-omni-skill

Build and deploy MCP servers to FastMCP Cloud marketplace. Use this skill when creating Python MCP servers with FastMCP 2 for deployment to fastmcp.cloud. Provides patterns, examples, and deployment guidance.

dev-swarm-stage-market-research

16
from diegosouzapw/awesome-omni-skill

Conduct comprehensive market research and competitive analysis to validate the problem, understand the market landscape, and identify opportunities. Use when starting stage 01 (market-research) or when user asks about competitors or market analysis.

coinmarketcap-automation

16
from diegosouzapw/awesome-omni-skill

Automate Coinmarketcap tasks via Rube MCP (Composio). Always search tools first for current schemas.

coinmarketcal-automation

16
from diegosouzapw/awesome-omni-skill

Automate Coinmarketcal tasks via Rube MCP (Composio). Always search tools first for current schemas.

skill-marketplace

16
from diegosouzapw/awesome-omni-skill

自動從 Skills Marketplace (skillsmp.com) 搜尋、安裝並使用適合當前任務的技能。當面對複雜任務或需要專業工具時自動觸發。

plan-maker

16
from diegosouzapw/awesome-omni-skill

Create implementation plans with testable acceptance criteria, validation strategies, integration touchpoints, and risk analysis before coding begins.

manifold-markets-skill

16
from diegosouzapw/awesome-omni-skill

Manifold Markets prediction market API guide. Use when: (1) Fetching/searching markets or market data, (2) Placing bets, limit orders, or multi-bets, (3) Selling shares or canceling orders, (4) Analyzing positions, portfolios, or profit, (5) Building trading bots, (6) WebSocket real-time updates, (7) Bulk data via Supabase, (8) Creating/editing/resolving markets, (9) Comments, reactions, follows, managrams, or DMs, (10) Querying transactions or bet history, (11) AMM math/simulation.

ats-resume-maker

16
from diegosouzapw/awesome-omni-skill

Create ATS-optimized resumes with selective bold highlighting, two-page maximum, and export to DOCX/PDF. Use when: (1) Creating resumes from scratch or provided data, (2) Converting resume content to ATS-friendly format, (3) Generating DOCX/PDF resume files, (4) Validating resumes for ATS compliance

marketplace-liquidity

16
from diegosouzapw/awesome-omni-skill

Help users build and manage marketplace liquidity. Use when someone is working on a marketplace business, struggling with supply/demand balance, trying to improve match rates, or asking how to reach critical mass in a two-sided market.

prediction-markets-gina

16
from diegosouzapw/awesome-omni-skill

Search, Trade, and Automate any strategy on Polymarket with your own agent.