---
title: "Copy-Trading"
description: "Automatically copy trades from Polymarket whale wallets with size scaling, position limits, and inverse (fade) mode."
icon: "copy"
---

<Note>
  **Pro Feature.** Requires a Pro or Ultra subscription. [Get started at api.mathematicalcompany.com](https://api.mathematicalcompany.com)
</Note>

# Copy-Trading

Horizon's copy-trading module monitors Polymarket wallets and automatically mirrors their trades through the Engine. It supports size scaling, position limits per market, deduplication, and an inverse mode for fading whale activity.

<Note>
  Copy-trading uses Polymarket's public Data API (`hz.get_wallet_trades()`) to poll for new trades, then submits mirrored orders through the Horizon Engine with full risk pipeline protection. No access to the source wallet's private key is needed.
</Note>

### When to Use What

| Goal | Function | Page |
|------|----------|------|
| Mirror a known wallet | `copy_trades()` / `copy_trader()` | This page |
| Auto-fade a bad wallet | `copy_trades(inverse=True)` | This page |
| Score first, then auto-execute | `hunt()` | [Wallet Profiler](/wallet-profiler) |
| Discover + auto-copy whales | `galaxy_hunt()` | [Whale Galaxy](/whale-galaxy) |
| Score or analyze a wallet first | `score_wallet()` / `analyze_wallet()` | [Wallet Intelligence](/wallet-intelligence) |

## Overview

<CardGroup cols={2}>
  <Card title="Standalone Mode" icon="play">
    `hz.copy_trades()` runs a blocking poll loop - the simplest way to start.
  </Card>
  <Card title="Pipeline Mode" icon="diagram-project">
    `hz.copy_trader()` returns a pipeline function for use inside `hz.run()`.
  </Card>
  <Card title="Inverse (Fade) Mode" icon="arrows-rotate">
    Flip buy/sell to fade the whale's trades instead of copying them.
  </Card>
  <Card title="Dedup + Watermarks" icon="filter">
    FIFO bounded dedup (10k capacity) with per-wallet timestamp watermarks prevents replaying history.
  </Card>
</CardGroup>

---

## Standalone Mode: hz.copy_trades

The simplest entry point. Creates an Engine, seeds dedup state, and runs a blocking poll loop.

```python
import horizon as hz

hz.copy_trades(
    wallet="0x1234abcd...",
    size_scale=0.5,           # Copy at 50% of whale's size
    max_position=1000.0,      # Max $1000 exposure per market
    poll_interval=30.0,       # Check every 30 seconds
    exchange="paper",         # "paper" or "polymarket"
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `wallet` | `str or list[str]` | required | Wallet address(es) to copy |
| `wallets` | `list[str]` | `None` | Alternative to `wallet` for multiple addresses |
| `size_scale` | `float` | `1.0` | Multiplier for trade size (0.5 = half size) |
| `max_position` | `float` | `1000.0` | Max USDC exposure per market |
| `min_trade_usdc` | `float` | `1.0` | Ignore trades smaller than this |
| `poll_interval` | `float` | `30.0` | Seconds between polls |
| `exchange` | `str` | `"paper"` | Exchange backend (`"paper"` or `"polymarket"`) |
| `inverse` | `bool` | `False` | Fade the whale (flip buy/sell) |
| `max_slippage` | `float` | `0.02` | Max price deviation from source |
| `dry_run` | `bool` | `False` | Log trades without submitting orders |
| `risk` | `Risk or RiskConfig` | `None` | Risk configuration for the engine |
| `api_key` | `str` | `None` | Horizon API key |

**Behavior:**
1. Creates an Engine with the specified exchange backend
2. Seeds dedup state from the last 100 trades per wallet (prevents replaying history)
3. Polls `get_wallet_trades()` each cycle for new trades
4. Filters by min size, valid price, and timestamp watermark
5. Applies size scaling and position limit clamping
6. Submits `OrderRequest` through the Engine (with full risk pipeline)
7. Handles SIGINT/SIGTERM for graceful shutdown

---

## Pipeline Mode: hz.copy_trader

For use inside `hz.run()` alongside other pipeline functions.

```python
import horizon as hz

hz.run(
    name="whale_copier",
    markets=["some-market"],
    pipeline=[
        hz.copy_trader(
            wallet="0x1234abcd...",
            size_scale=0.5,
            max_position=1000.0,
        ),
    ],
    interval=30.0,
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `wallet` | `str or list[str]` | required | Wallet address(es) to copy |
| `wallets` | `list[str]` | `None` | Alternative to `wallet` |
| `size_scale` | `float` | `1.0` | Size multiplier |
| `max_position` | `float` | `1000.0` | Max USDC per market |
| `min_trade_usdc` | `float` | `1.0` | Min trade size |
| `inverse` | `bool` | `False` | Fade mode |
| `max_slippage` | `float` | `0.02` | Max slippage |
| `dry_run` | `bool` | `False` | Dry run mode |

The pipeline function:
- Uses `ctx.params["engine"]` for direct order submission (same pattern as `arb_scanner`)
- Seeds dedup on first call only
- Stores results in `ctx.params["copy_trade_results"]`
- Returns `None` (bypasses the quote mechanism - orders are submitted directly)

---

## Side Mapping

Copy-trading maps source trades to orders as follows:

| Source Side | Source Outcome | → Side | → OrderSide |
|-------------|---------------|--------|-------------|
| BUY | Yes | Yes | Buy |
| BUY | No | No | Buy |
| SELL | Yes | Yes | Sell |
| SELL | No | No | Sell |

**Inverse mode** flips `OrderSide` only (Buy ↔ Sell). The `Side` (Yes/No) stays the same. This means inverse mode buys when the whale sells, and sells when the whale buys - fading their directional conviction.

---

## Size Calculation

Orders are sized as follows:

```
scaled_usdc = trade.usdc_size × size_scale
token_size  = scaled_usdc / trade.price
```

For buy orders, the `scaled_usdc` is clamped to the remaining position limit:

```
allowed = max(0, max_position - current_exposure)
scaled_usdc = min(scaled_usdc, allowed)
```

The submitted price includes slippage protection:
- **Buy:** `min(0.99, source_price + max_slippage)`
- **Sell:** `max(0.01, source_price - max_slippage)`

---

## Dedup Strategy

Copy-trading uses a FIFO bounded dedup set (same pattern as Polymarket/Kalshi fill dedup in the Rust core):

- **Primary key:** `tx_hash` (falls back to `wallet:condition_id:timestamp:size` composite)
- **Capacity:** 10,000 entries with FIFO eviction
- **Seeding:** On startup, the last 100 trades per wallet are marked as seen
- **Watermark:** Per-wallet timestamp - only trades newer than the last seen are processed

---

## Data Types

### CopyTraderConfig

```python
config = hz.CopyTraderConfig(
    wallets=["0x1234..."],
    size_scale=0.5,
    max_position_per_market=1000.0,
    min_trade_usdc=1.0,
    poll_interval=30.0,
    inverse=False,
    max_slippage=0.02,
    dry_run=False,
    dedup_capacity=10_000,
)
```

### CopyTradeResult

Each processed trade produces a result record:

```python
result.source_wallet     # "0x1234..." source wallet
result.source_tx_hash    # Transaction hash (or None)
result.market_slug       # Market slug
result.condition_id      # Condition ID
result.side              # "yes" or "no"
result.order_side        # "buy" or "sell"
result.size              # Token size submitted
result.price             # Price submitted (with slippage)
result.order_id          # Engine order ID (None if dry_run)
result.error             # Error message (None on success)
```

---

## Examples

### Copy a Single Whale (Paper Mode)

```python
import horizon as hz

hz.copy_trades(
    wallet="0xd91ef12a78d5296661816eec300753405cc1e577",
    size_scale=0.5,
    max_position=500.0,
    poll_interval=30.0,
)
```

### Fade a Whale (Inverse Mode)

```python
import horizon as hz

hz.copy_trades(
    wallet="0xd91ef12a78d5296661816eec300753405cc1e577",
    size_scale=0.25,
    inverse=True,       # Sell when they buy, buy when they sell
    dry_run=True,       # Test first
)
```

### Copy Multiple Wallets

```python
import horizon as hz

hz.copy_trades(
    wallets=[
        "0xd91ef12a78d5296661816eec300753405cc1e577",
        "0xabcdef1234567890abcdef1234567890abcdef12",
    ],
    size_scale=0.25,
    max_position=500.0,
)
```

### Pipeline Mode with Market Making

```python
import horizon as hz

hz.run(
    name="copy_and_mm",
    markets=["election-winner"],
    feeds={"book": hz.PolymarketBook("election-winner")},
    pipeline=[
        hz.copy_trader(wallet="0x1234...", size_scale=0.5),
        hz.market_maker(feed_name="book", gamma=0.5, size=5.0),
    ],
    interval=30.0,
)
```

### Live Trading with Risk Controls

```python
import horizon as hz

hz.copy_trades(
    wallet="0x1234...",
    size_scale=0.5,
    max_position=1000.0,
    exchange="polymarket",
    risk=hz.Risk(
        max_position_size=200.0,
        max_total_exposure=2000.0,
        max_drawdown=0.10,
    ),
)
```

### Access Results in Pipeline

```python
import horizon as hz

def log_copies(ctx):
    results = ctx.params.get("copy_trade_results", [])
    for r in results:
        if r.error:
            print(f"Failed: {r.error}")
        else:
            print(f"Copied: {r.order_side} {r.side} {r.size} @ {r.price}")
    return None

hz.run(
    name="copy_with_logging",
    markets=["election-winner"],
    pipeline=[
        hz.copy_trader(wallet="0x1234...", size_scale=0.5),
        log_copies,
    ],
    interval=30.0,
)
```

<Warning>
  Copy-trading submits real orders when `exchange="polymarket"`. Always test with `dry_run=True` or `exchange="paper"` first. Position limits and the risk pipeline provide safety guardrails, but you are responsible for monitoring your exposure. The copy-trader polls the public Data API, which may have a delay of a few seconds - you will not get the exact same fill price as the source wallet.
</Warning>
