# Adversarial Trading in Prediction Markets

*Finding, profiling, and trading against predictable bots. With working code.*

---

Most bots in prediction markets share a problem: they were designed to be consistent. Fixed order sizes, regular intervals, mechanical reactions to price. That consistency is what makes them work. It's also what makes them readable.

If you can classify a bot's strategy, measure how predictable it is, and understand its behavioral patterns, you can build a strategy that profits from its rigidity. This article covers the full pipeline: scanning markets for automated participants, profiling their weaknesses, building counter-strategies, and protecting yourself from the same analysis.

Everything runs on [Horizon SDK](https://api.mathematicalcompany.com). Python interface, Rust execution core. These are real function calls, not pseudocode.

---

## 1. Finding the Bots

Before trading against anyone, you need to know who's in the market and how they behave.

`scan_bots()` pulls on-chain trade history for a Polymarket market and classifies each wallet by strategy type:

```python
import horizon as hz

bots = hz.scan_bots("0xcondition_id_here", trade_limit=1000)

for bot in bots:
    if bot.is_bot:
        print(f"{bot.wallet[:10]}... → {bot.strategy_type} "
              f"(confidence={bot.confidence:.0%})")
        for e in bot.evidence:
            print(f"    {e}")
```

Sample output from a real Polymarket market:

```
0xe7ab56a1aa... → momentum (confidence=100%)
    Strong directional bias (1.00)
    High frequency (166.7 trades/hr)
0x9ef582748b... → momentum (confidence=100%)
    Strong directional bias (1.00)
    High frequency (63.2 trades/hr)
0xf6c7dd2bf6... → human (confidence=100%)
    Irregular timing (CV=3.34)
    Varied sizing (CV=1.79)
    Low frequency (1.7 trades/hr)
```

The classifier looks at five dimensions:

| Signal | Measures | Bot threshold |
|--------|----------|---------------|
| Timing regularity | CV of inter-trade intervals | CV < 0.3 |
| Size consistency | CV of trade sizes | CV < 0.3 |
| Directional bias | Buy ratio (buys / total) | \|ratio - 0.5\| > 0.25 |
| Trade frequency | Trades per minute | > 1/min |
| Price stepping | Regularity of price levels | Uniform grid spacing |

Each bot type has a different set of vulnerabilities. Market makers get picked off by informed flow. Grid bots get front-run at known price levels. Momentum bots get faded when they overshoot. The classification tells you which playbook to use.

### Scoring wallets

Not every bot is good at what it does. `score_wallet()` grades a wallet's historical performance:

```python
score = hz.score_wallet("0x7a3b2c91...")
print(f"Composite: {score.composite_score:+.2f}")
print(f"Win rate:  {score.win_rate:.0%}")
print(f"Sharpe:    {score.sharpe:.2f}")
print(f"Total PnL: ${score.total_pnl:,.0f}")
```

Composite score runs from -1 to +1:

| Score | Read | Action |
|-------|------|--------|
| > 0.5 | Sharp | Avoid or copy |
| -0.2 to 0.2 | Average | Neutral |
| < -0.5 | Consistently wrong | Fade |

A wallet at -0.6 over 200 trades isn't having bad luck. That's a systematic negative edge. And a systematic negative edge on one side is a systematic positive edge on the other.

---

## 2. Profiling Vulnerabilities

Knowing a wallet is a bot tells you *what* it is. Profiling tells you *how* to trade against it.

`analyze_wallet()` tests six behavioral dimensions and flags which ones are exploitable:

```python
report = hz.analyze_wallet("0x7a3b2c91...")
print(f"Vulnerability Score: {report.vulnerability_score:.2f}")

for pattern in report.patterns:
    tag = "EXPLOITABLE" if pattern.exploitable else "safe"
    print(f"  {pattern.name}: {pattern.value:.2f} [{tag}]")
    if pattern.exploitable:
        print(f"      Counter: {pattern.counter_strategy}")
```

The six patterns:

**Timing regularity.** The bot trades on a schedule (every 45 +/- 3 seconds). CV of inter-trade intervals below 0.3 means you can predict when the next order lands. Counter: position yourself 2-3 seconds before the expected trade.

**Sizing predictability.** Fixed or near-fixed order sizes (always 10 contracts, every time). CV of sizes below 0.3. Counter: size your trades to exactly absorb their flow.

**Directional bias.** The bot overwhelmingly buys or sells (82% buy trades). Counter: fade the dominant direction. Their flow becomes your exit liquidity.

**Reaction speed.** Consistent latency when responding to price moves (median 30s after a 2% move). Counter: move before their reaction window closes. If you know they'll chase in 30 seconds, you have 30 seconds of edge.

**Concentration.** The bot focuses on a few markets (Herfindahl index > 0.3). Counter: target the markets where they have the most capital committed.

**Stop-loss clustering.** Exits cluster around predictable price levels (70% of exits at -5% from entry). Counter: if price approaches the cluster, the cascade is coming. Position for it.

---

## 3. Building Counter-Strategies

With bots identified and profiled, you can build pipelines that act on their weaknesses.

### The Bot Fader

Find wallets with negative composite scores. Take the other side of their trades automatically.

```python
import horizon as hz

bots = hz.scan_bots("0xcondition...")
targets = []

for bot in bots:
    if bot.is_bot:
        score = hz.score_wallet(bot.wallet)
        if score.composite_score < -0.3:
            targets.append(bot.wallet)
            print(f"Fade target: {bot.wallet[:10]}... "
                  f"(score={score.composite_score:+.2f}, "
                  f"type={bot.strategy_type})")

for wallet in targets:
    hz.reverse_copy(
        wallet=wallet,
        size_scale=0.5,
        dry_run=True,  # log without submitting, remove for live
    )
```

`reverse_copy()` scores the wallet first. If the composite score is below the threshold (default -0.3), it starts an inverse copy-trade loop via `copy_trades(inverse=True)`. It monitors the target wallet on-chain and submits opposing orders. When they buy, you sell. `size_scale=0.5` means you trade at half their size, which keeps risk manageable while still capturing the edge.

### Toxic Flow Market Making

Standard market making bleeds money when informed traders show up and pick off your stale quotes. The fix is knowing when informed flow enters the book, and stepping out of the way.

This pipeline uses three microstructure signals:
- **VPIN** (Volume-synchronized Probability of Informed Trading): measures how much of recent volume looks informed
- **Order Flow Imbalance + Kyle's lambda**: measures directional pressure and price impact per unit flow
- **CUSUM**: detects structural breaks in price series

```python
import horizon as hz
from horizon.context import FeedData

vpin = hz.toxic_flow(feed_name="book", window=50, threshold=0.7)

micro = hz.microstructure(
    feed_name="book",
    ofi_window=20,
    lambda_window=50,
)

cusum = hz.change_detector(
    feed_name="book",
    threshold=3.0,
    drift=0.5,
)


def adversarial_quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
    """Wide against informed flow, tight against noise."""
    vpin_val = ctx.params.get("vpin", 0.5)
    ofi = ctx.params.get("ofi", 0.0)
    kyles_lambda = ctx.params.get("kyles_lambda", 0.0)
    cusum_alert = ctx.params.get("cusum_alert", False)

    # Structural break detected, pull everything
    if cusum_alert:
        return []

    if vpin_val > 0.7:
        # High toxicity: informed traders in the book
        # Go wide, go small
        spread = 0.08
        size = 3.0
    elif vpin_val < 0.3:
        # Low toxicity: mostly bots and retail
        # Quote tight, collect spread
        spread = 0.015
        size = 15.0
    else:
        spread = 0.04
        size = 8.0

    # Skew quotes in the direction of order flow pressure
    skew = ofi * kyles_lambda * 0.01
    skewed_fair = fair - skew

    return hz.quotes(skewed_fair, spread, size=size)


hz.run(
    name="adversarial_mm",
    markets=["election-winner"],
    feeds={"book": hz.PolymarketBook("election-winner")},
    pipeline=[vpin, micro, cusum, adversarial_quoter],
    risk=hz.Risk(max_position=100, max_drawdown_pct=5),
    interval=1.0,
    mode="paper",
)
```

The idea is simple: you don't need to outrun informed traders. You need to not be quoting when they arrive. VPIN is the early warning system. When it's low, you're collecting spread from uninformed flow. When it spikes, your quotes disappear.

### Grid Bot Front-Running

Grid bots place limit orders at regular price intervals, say every $0.02 from $0.40 to $0.60. You can see these orders on-chain or in the orderbook. The play: rest your orders one tick inside theirs.

```python
import horizon as hz
from horizon.context import FeedData


def detect_grid_levels(ctx: hz.Context) -> dict:
    """Spot regular price spacing in the orderbook."""
    book = ctx.feeds.get("book", FeedData())

    spread = book.ask - book.bid if book.ask > book.bid else 0.02
    mid = (book.bid + book.ask) / 2 if book.bid > 0 else 0.50

    return {
        "mid": mid,
        "spread": spread,
        "grid_detected": spread < 0.03,
        "bid_level": book.bid,
        "ask_level": book.ask,
    }


def front_run_quoter(ctx: hz.Context, grid: dict) -> list[hz.Quote]:
    """Step inside detected grid levels by one tick."""
    if not grid["grid_detected"]:
        return []

    tick = 0.001
    return [hz.Quote(
        bid=grid["bid_level"] + tick,
        ask=grid["ask_level"] - tick,
        size=5.0,
    )]


hz.run(
    name="grid_frontrunner",
    markets=["target-market"],
    feeds={"book": hz.PolymarketBook("target-market")},
    pipeline=[detect_grid_levels, front_run_quoter],
    risk=hz.Risk(max_position=50, max_drawdown_pct=3),
    interval=0.5,
    mode="paper",
)
```

Your order sits at the front of the queue. When a market order comes in, you fill first. The grid bot behind you gets nothing. This is standard practice in traditional equities and works well against bots that don't adapt their levels dynamically.

### Multi-Signal Oracle

Sometimes the best adversarial edge comes from better information rather than targeting a specific bot.

Horizon's Oracle module combines six independent signal sources into an ensemble probability estimate, then finds markets where the ensemble disagrees with the current price:

```python
import horizon as hz

edges = hz.scan_edges(min_edge_bps=300, max_markets=50)

for e in edges:
    print(f"{e.market_title}")
    print(f"  Market: {e.market_price:.2f}  Forecast: {e.forecast_prob:.2f}")
    print(f"  Edge: {e.edge_bps:.0f} bps ({e.direction})")
    print(f"  Confidence: {e.confidence:.2f}")
```

The six signals that feed the ensemble:

1. **Smart Money Flow**: holder analysis weighted by wallet score. High-scoring wallets accumulating a side is signal.
2. **Microstructure**: Kyle's lambda + VPIN. Measures whether informed flow is building a position.
3. **Momentum**: EMA crossover on the probability time series. Short-term trend vs. long-term.
4. **Volume Profile**: buy vs. sell volume imbalance over a rolling window.
5. **Holder Concentration**: Herfindahl index of top holders. High concentration = high conviction.
6. **Temporal Patterns**: hour-of-day activity. Some markets have consistent daily rhythms.

You can also run it continuously inside a pipeline:

```python
hz.run(
    name="oracle-edge-trader",
    markets=["market-a", "market-b", "market-c"],
    pipeline=[
        hz.oracle(forecast_interval_cycles=20),
        quoter,  # reads ctx.params["oracle_edge_bps"]
    ],
    interval=5.0,
    mode="paper",
)
```

---

## 4. Stealth: Don't Be the Pattern

Everything above can be turned against you. If your wallet shows regular timing, fixed sizes, or consistent direction, someone will run `analyze_wallet()` on you and find the same patterns.

Horizon's stealth execution module randomizes your footprint.

### Estimating impact first

Before placing a large order, check how much it will move the market:

```python
impact = hz.estimate_impact(engine, market_id="election-winner", size=500.0)
print(f"Expected slippage: {impact.estimated_slippage_bps:.1f} bps")
print(f"Kyle's lambda:     {impact.kyle_lambda:.6f}")
print(f"Effective spread:  {impact.effective_spread_bps:.1f} bps")
print(f"Recommended algo:  {impact.recommended_strategy}")
```

### Three execution algorithms

**Adaptive TWAP** splits your order across time slices. If fills come easily, it slows down to avoid signaling demand. If fills are scarce, it speeds up. Timing jitter of +/-20% and size jitter of +/-30% break any pattern a scanner could detect.

**Iceberg Plus** shows a small, randomized fraction of your total order. Visible size varies between 0.5x and 1.5x the base amount. Random delays between refills. The book sees a normal small order, not a 500-contract position being built.

**Sniper** waits for optimal conditions (tight spread, favorable LOB imbalance) and fires the full order at once. Minimal time in market, minimal information leakage.

```python
plan = hz.stealth_execute(
    engine,
    market_id="election-winner",
    side=hz.Side.Yes,
    order_side=hz.OrderSide.Buy,
    size=500.0,
    price=0.55,
    strategy="auto",
    config=hz.StealthConfig(
        randomize=True,
        impact_limit_bps=50.0,
        patience_secs=120.0,
    ),
)
print(f"Strategy: {plan.strategy}")
print(f"Child orders: {len(plan.child_orders)}")
print(f"Estimated cost: {plan.estimated_cost_bps:.1f} bps")
print(f"Duration: {plan.estimated_duration_secs:.0f}s")
```

So the full loop looks like: you use adversarial analysis to read other bots, and stealth execution to make sure nobody can read you.

---

## 5. Risk Management

Adversarial strategies go wrong. The bot you're fading starts winning. A regime shift makes your VPIN thresholds stale. A correlated event hits multiple positions at once.

### Sentinel: graduated drawdown response

Sentinel runs alongside your strategy and escalates its response as losses mount:

```python
hz.run(
    name="adversarial-strategy",
    markets=["market-a", "market-b"],
    pipeline=[
        hz.sentinel(config=hz.SentinelConfig(
            auto_deleverage=True,
            drawdown_thresholds=[
                (-0.05, "alert"),    # 5% down: log warning
                (-0.10, "reduce"),   # 10% down: cancel 50% of orders
                (-0.20, "pause"),    # 20% down: cancel everything
                (-0.30, "exit"),     # 30% down: flatten all positions
            ],
        )),
        vpin,
        micro,
        adversarial_quoter,
    ],
    risk=hz.Risk(max_position=100, max_drawdown_pct=5),
    interval=1.0,
)
```

At -5%, you get a warning. At -10%, it starts cutting exposure. At -20%, all orders are pulled. At -30%, it exits every position. No manual intervention, no "I'll just hold through this" moments.

Sentinel also watches for correlation spikes across positions (Ledoit-Wolf shrinkage), classifies the current volatility regime (low/normal/high/crisis), and auto-adjusts risk budgets based on conditions.

### The 8-point risk pipeline

Independent of Sentinel, every single order passes through Horizon's risk pipeline before reaching the exchange:

1. **Kill switch** check
2. **Price sanity**: limit price in [0.01, 0.99]
3. **Size sanity**: positive, below max, not NaN
4. **Position limit**: per-market exposure cap
5. **Portfolio notional**: total exposure cap, accounting for netting pairs
6. **Daily drawdown**: max loss for the day
7. **Duplicate detection**: same order within dedup window gets rejected
8. **Rate limit**: token bucket with sustained rate and burst capacity

All eight checks run in Rust. If any fails, the order never leaves your machine.

```python
risk = hz.Risk(
    max_position=100,
    max_notional=5000,
    max_drawdown_pct=5,
    max_order_size=50,
    rate_limit=50,
    rate_burst=300,
)
```

Your strategy can be as aggressive as you want. The risk layer is the constraint that keeps it from blowing up.

---

## 6. Backtesting

Horizon's paper exchange is a tick-based matching engine. It handles limit/market orders, GTC/FOK/FAK time-in-force, maker/taker fee classification, and configurable partial fills.

```python
/com
```

A few details worth noting:

- **GTC** orders rest in the book until filled or canceled
- **FOK** fills entirely or cancels entirely, no partials
- **FAK/IOC** fills what's available immediately, cancels the rest
- Maker/taker is classified by whether your order was resting (maker, lower fee) or crossing (taker, higher fee)
- `partial_fill_ratio=0.7` means only 70% of a matchable order fills per tick, which simulates realistic queue priority

You can run any of the strategies above in paper mode first. Same risk pipeline, same pipeline composition, same everything. Just `mode="paper"` instead of `mode="live"`.

---

## The Full Workflow

Here's how the pieces connect:

```
SCAN            hz.scan_bots()           Find automated participants
SCORE           hz.score_wallet()        Grade their performance
PROFILE         hz.analyze_wallet()      Map exploitable patterns
FORECAST        hz.scan_edges()          Find mispriced markets
EXECUTE         hz.run() + pipeline      Deploy counter-strategy
HIDE            hz.stealth_execute()     Randomize your own footprint
DEFEND          hz.sentinel()            Graduated drawdown response
BACKTEST        paper exchange           Validate before going live
```

Each step is a single function call backed by a compiled Rust core. The scanning and profiling that would normally require weeks of data infrastructure work collapses into a few lines of Python.

Prediction markets are still young enough that most automated participants run simple, rigid strategies. Their patterns are visible in the on-chain data. The tools to analyze and trade against those patterns are getting better faster than the bots are adapting.

Good time to be paying attention.
