# RAPTOR (v2 supervised demo)

Raptor is a **single-wallet, single-recipe** whale **copy-follow** strategy: it
finds currently-winning quality traders, takes their strongest-conviction
position when smart money agrees, and rides it with a patient-but-bounded DSL
exit.

Ported from `senpi-skills/raptor` (producer v4.0.1, config v3.1) to the runtime
v2 supervised external-scanner contract. The standalone producer daemon is gone;
the runtime supervises `scanners/scan.py` and calls `scan()` every
`interval_seconds` (180s).

> **DOC-vs-CODE warning.** The `senpi-skills/raptor` SKILL/README docs are
> **stale** — they describe a $2M trader gate and 0.40 concentration. The
> **code** (config v3.1 + producer constants) is authoritative and is what this
> port uses: trader delta gate **$500k**, position gate **$100k**, concentration
> **0.35**, SM **pct ≥ 2 / traders ≥ 10**, whale entry-discipline **5%**,
> `minScore` **6**. Every threshold below was resolved to the code.

## The strategy (`scanners/scan.py`)

Quality-first copy-follow pipeline, run once per tick:

1. **Quality hot traders** — `discovery_get_top_traders(WEEKLY,
   sort PROFIT_AND_LOSS_UNREALIZED, consistency ELITE/RELIABLE,
   open_position_filter, limit 20)`; keep traders whose **unrealized delta-pnl
   ≥ $500k**, sorted desc. Scan the top **10** for positions.
2. **Strongest position** — per trader, `leaderboard_get_trader_positions`; pick
   the position with the largest **|delta_pnl| (≥ $100k)**.
3. **Concentration** — `best |delta_pnl| / total |delta_pnl| ≥ 0.35` (one
   conviction bet, not a diversified book).
4. **Smart-money alignment** — `leaderboard_get_markets`; the asset must show
   **SM pct ≥ 2.0**, **traders ≥ 10**, and direction matching the whale's.
5. **Whale entry-discipline** — `market_get_prices`; **skip if the price already
   ran > 5%** from the whale's entry in the whale's direction (don't buy the top).
6. **Additive score (max ~16)** — TCS + trader-$ tier + ROI + concentration +
   SM strength + 4h/1h price confirm + 15m contribution + a better-than-whale
   entry-edge bonus. (All tiers ported verbatim — see `scoring.py`.)
7. **Emit the single best candidate**, and only if `score ≥ 6`.

Sizing rides on each signal's `data{}` so the `OPEN_POSITION` rule action sizes
identically to the source:

- **marginPct** = percent of withdrawable (0–100): **25.0** (**35.0** if
  `score ≥ 10`). Dual-DEX equity is collapsed via **`max()` not `sum()`** —
  one cross-margined wallet, two sub-DEX views.
- **leverage** = conviction tier (**≥10 → 10x, ≥8 → 8x, ≥6 → 7x**), clamped to
  the per-asset Hyperliquid venue max via `strategy_get_asset_trading_limits`.

**XYZ is banned** (config `smAlignment.xyzBanned`): xyz-dex SM markets and `xyz:`
positions are dropped.

MCP tools used (all **read-only**): `discovery_get_top_traders`,
`leaderboard_get_trader_positions`, `leaderboard_get_markets`,
`market_get_prices`, `strategy_get_asset_trading_limits`,
`strategy_get_clearinghouse_state`.

State (the per-`(trader, asset)` **4h event dedup** map) lives in `ctx.state`
(the source kept it in `seen-events.json`).

## Why raptor emits at most one signal per tick

Turbine/spider emit **all** gated candidates and let the runtime apply the slot
ceiling. **Raptor is deliberately different: it emits ≤ 1 signal per tick** — the
single highest-score whale-aligned setup. This is the **strategy**, not an
oversight: raptor's thesis is "follow the single strongest conviction signal
right now," and the source producer pushed exactly one signal per tick after
sorting candidates by score. `slots: 2` still lets a prior position stay open
while a later tick emits the next-best; the runtime owns whether the second slot
fills across ticks.

## Layout

```
raptor/
├── README.md
├── recipe.yaml            # the single recipe (path: ./scanners, entrypoint: scan.py)
└── scanners/
    ├── scan.py            # scan(inputs, ctx) entrypoint (whale copy-follow pipeline)
    └── scoring.py         # pure parsing/gating/scoring functions (unit-tested)
```

## Environment variables

| Variable           | Required | Purpose |
|--------------------|----------|---------|
| `RAPTOR_WALLET`    | yes      | Raptor's strategy wallet |
| `SENPI_API_KEY`    | yes      | MCP auth for `ctx.senpi_mcp` (injected into the scaffold child) |
| `SENPI_MCP_URL`    | yes      | MCP server URL |
| `TELEGRAM_CHAT_ID` | optional | Notifications |

## Install / run

```bash
RAPTOR_WALLET=0x... \
SENPI_API_KEY=... SENPI_MCP_URL=https://mcp.prod.senpi.ai/mcp \
  openclaw senpi run examples/strategies/raptor/recipe.yaml
```

## Fidelity notes

Source behaviors that changed (or did not map 1:1) under the v2 contract.
Nothing load-bearing was silently dropped.

### Dropped: the daemon loop + ingest POST

The source ran a `producer_daemon(interval_seconds=180)` that POSTed signals to
the ingest endpoint via `push_signal`. Under v2 the runtime **supervises** this
module and calls `scan()` once per `interval_seconds`; `scan()` is **single-pass
and synchronous** and **returns a `list[dict]`**. The scaffold owns delivery,
the wire envelope, and `signal_id` dedup. The source's normalized [0,1] wire
score (`min(score / 16, 1.0)`) is **not** recomputed in `scan()` — the raw
additive score rides on `data{}` and the runtime owns wire scoring. (The
`16.0` divisor is preserved in `scoring.py` for parity.)

### Moved to `ctx.state`: the 4h event dedup

The per-`(trader, asset)` **4h** event dedup that the source kept in
`seen-events.json` now lives in `ctx.state`: each tick reads the latest
`{key: ts}` map, **prunes** it by the 4h window, applies it, **stamps** the
chosen candidate's `(trader, asset)`, and appends the updated map for the next
tick. The dedup key is verbatim — first 10 chars of the lowercased trader
address + the asset symbol.

### Action gate: `decision_mode: llm` → `rule`

The source `OPEN_POSITION` action used `decision_mode: llm` with a copy-verbatim
pass-through prompt and a pinned confidence — a rule wearing an LLM costume. This
recipe uses `decision_mode: rule` directly.

### Thresholds resolved to CODE (not stale docs)

The `senpi-skills/raptor` SKILL/README claimed a **$2M** trader gate and **0.40**
concentration. The **code** (config v3.1 + producer constants) uses **$500k** and
**0.35** — those are the values ported. Every other gate/scoring tier
(`minPositionPnl` $100k, SM pct 2 / traders 10, entry-discipline 5%, `minScore`
6, trader tiers $1.5M/$3M, leverage 7/8/10x, margin 0.25/0.35) maps cleanly and
is carried verbatim.

### DSL preset maps cleanly

The source DSL preset maps 1:1 to the v2 schema (no dropped tiers): hard_timeout
360m, weak_peak 60m@2.5, dead_weight 40m, phase1 max_loss 25 / retrace 10, and a
5-tier phase2 (8/25 · 15/50 · 25/65 · 40/80 · 60/88).

### Guard rails: minutes/hours → seconds

The v2 schema uses integer seconds everywhere. `cooldown_minutes 30` →
`cooldown_seconds 1800`; `per_asset_cooldown_minutes 120` →
`per_asset_cooldown_seconds 7200`; `data_retention_hours 72` →
`data_retention_seconds 259200`.

### Only read-only MCP calls

`scan.py` never opens, closes, or cancels — it produces signals. A test asserts
no `create_position` / `close_position` / `cancel_order` tool is ever called.
