# REMORA FREEZE v1.1 — mechanical clarification
## What the Grok implementation actually did (the 155-trade run)

Do not retune. Do not “fix” lookahead. Reproduce this behavior, then label any cleaner fork as v1.2.

Reproduced just now from the frozen 3600-bar caches + the same engine:

- trades 155 (88 wins / 67 losses)
- WR 0.567741935483871
- PnL +810.5373830602038 USDT
- PF 1.2601012959180373
- max DD 0.04271911716402652
- avg R 0.2813496333507463
- hunters 155 / wounded 0
- exits: target 86, stop 66, time 3, host_dead 0

Ledger: `/remora-ledger-v1.json` (also `data/remora/ledger-v1.json`).

---

## 1. Historical window

All six symbols share the same 3600 hourly opens:

| | unix ms | ISO UTC |
|---|---:|---|
| first bar open | 1774152000000 | 2026-03-22T04:00:00.000Z |
| last bar open | 1787108400000 | 2026-08-19T03:00:00.000Z |
| last bar close (open + 1h) | 1787112000000 | 2026-08-19T04:00:00.000Z |
| warmup | 50 bars | first evaluable open 2026-03-24T06:00:00.000Z |
| first closed trade exit | 1774364400000 | 2026-03-24T15:00:00.000Z |
| last closed trade exit | 1786788000000 | 2026-08-15T10:00:00.000Z |

History request: last 3600 1H bars via pagination, then `slice(-3600)`.
`confirm` is **not** read. The last bar at fetch time may be the forming 1H candle.

---

## 2. Instrument IDs (exact)

OKX SWAP:

- `BTC-USDT-SWAP`
- `ETH-USDT-SWAP`
- `SOL-USDT-SWAP`
- `XRP-USDT-SWAP`
- `DOGE-USDT-SWAP`
- `LINK-USDT-SWAP`

---

## 3. Candle source and volume field

Backtest history:

```
GET https://www.okx.com/api/v5/market/history-candles
  ?instId={ID}&bar=1H&limit=100&after={minTsSeen}
```

Paginate backward with `after = min(ts in chunk)` until ≥ 3600 rows, dedupe by `ts`, sort ascending, keep last 3600.

Live radar (not the 155-trade sample):

```
GET https://www.okx.com/api/v5/market/candles?instId={ID}&bar=1H&limit=…
```

OKX row layout used by the parser:

```
[0] ts
[1] o
[2] h
[3] l
[4] c
[5] vol            // contracts
[6] volCcy         // coin
[7] volCcyQuote    // USDT quote
[8] confirm        // IGNORED
```

Volume actually used:

```
v = Number(row[7] || row[6] || row[5])
```

On the frozen cache this is **volCcyQuote (USDT)**. BTC first bar `v ≈ 1.5818284701055e8`. If `row[7]` is missing/0, it would fall back. Do not use contract `vol` if you want to match.

---

## 4. Timezone / alignment

- `ts` = **candle open**, unix milliseconds.
- `bar=1H` = UTC hour alignment (`…T04:00:00.000Z`, `…T05:00:00.000Z`, …).
- No exchange-local offset. No 4H/daily session cut.

---

## 5. `impulse`

Yes.

```
impulse = print.high - print.low
        = range of the print candle
```

Stored as `print.impulse`. All hunter fibs use this number. Not ATR. Not body.

---

## 6. Hunter trend filter

Evaluated **on the print bar P only**, once, when the print is accepted as hunter:

```
SMA36[P] = mean(close[P-35] … close[P])   // includes print close
long hunter  allowed iff close[P] > SMA36[P]
short hunter allowed iff close[P] < SMA36[P]
```

It is **not** re-checked on the fill bar.

The fill bar’s close is never used to authorize the latch.
However: if the fill occurs on bar P+1 (see §7–8), the algorithm has already read P+1’s **entire** OHLC (including close) for wounded classification before it allows a P+1 fill. That is a different leak (classification), not the SMA filter.

---

## 7. Wounded timing — what the code actually did

Print detected at bar **P** (P must be fully closed).

Classification uses **bar P+1 close**:

```
wounded iff
  impulse >= 2.6 * ATR[P]
  AND rejectWick / impulse >= 0.42
  AND (long print ? next.c < print.o : next.c > print.o)
```

Wounded execution in the current engine:

```
entry = slip(bar[P+1].open, fadeSide, entry=true)
signal.barIndex = P+1
```

That is **lookahead**. You cannot know P+1’s close at P+1’s open.

**v1 produced 0 wounded trades.** This bug does not move the reported +810. Keep it if you want a bitwise match; do not “fix” it and still claim v1.

Hunter watch window as coded (spec v1 said “8 bars”; the code is 9):

```
pending.expires = P + 1 + 8 = P+9
fill allowed on i = P+1, P+2, …, P+9   // 9 hourly bars
expired when i > P+9
```

61 of the 155 hunter fills occurred on **P+1** (classification bar). Those fills are also path-dependent: close of P+1 was already known when the low/high of P+1 was used as a fill.

---

## 8. Intrabar execution (the 155-trade assumptions)

Two stages. Do not merge them.

### 8a. Signal scan (`scanBars`)

On bar i, **first** try to fill any pending hunter against `bars[i]` OHLC (touch of the 36% level + retrace in [0.28, 0.54]). **Then** detect a new print at i using `bars[i+1].c` for kind.

One fill per bar index (`used` set). One fill per print.

### 8b. Simulator (`backtestSymbol`) on each bar i

1. For every **already-open** position:
   - stop touched? (`long: l <= stop`, `short: h >= stop`) → exit raw = **stop**
   - else target touched? (`long: h >= target`, `short: l <= target`) → exit raw = **target**
   - if **both** touched on that bar: **stop wins** (pessimistic)
   - else if `i - entryBarIndex >= holdLimit`: exit raw = **close** (`time`)
   - else if host-dead: exit raw = **close**
2. **Then** open signals whose `barIndex === i`.

Consequence: **the fill bar is never tested for stop, target, time, or host-dead.**

If a 1H candle tags the belly **and** the stop, v1 still fills and the position survives to the next bar. That is **optimistic on the entry candle**.

No trade in the 155-ledger has `entryTs === exitTs` (0 / 155), which matches this rule.

First legal exit bar is **entryBar + 1**.
84 / 155 trades have `barsHeld === 1` (resolved on that next bar).

Stop / target prices stored on the signal are **unslipped** theoretical levels. Touch is against those. Slip is applied after the touch (§11).

---

## 9. Host-dead

Known only at bar close. Evaluated after stop/target and after the time-stop check.

```
range = h - l
ATR[i] = SMA(TR, 14) ending at i   // includes this bar’s TR
trigger if range >= 1.55 * ATR[i]
  AND long  ? close < min(entry, open)
  AND short ? close > max(entry, open)
exit raw = close, then slip
```

0 trades in v1. Keep the branch.

---

## 10. Time-stop

```
holdLimit = hunter 12, wounded 5
fire when i - signal.barIndex >= holdLimit
```

Evaluated only if stop/target did **not** hit on that bar.
Exit raw = that bar’s **close**, then slip.
`barsHeld = max(1, exitIndex - entryIndex)`.

3 time-stops in v1.

---

## 11. Slippage

`SLIPPAGE = 0.00012` (1.2 bps) of the **raw** price, always against the trader.

```
slip = raw * 0.00012

entry long  = raw + slip
entry short = raw - slip
exit  long  = raw - slip     // all exit types: stop, target, time, host_dead, open
exit  short = raw + slip
```

Hunter raw entry = 36% fib.
Wounded raw entry = classification bar open (lookahead, unused in v1 PnL).
Stop/target raw = theoretical levels.
Time / host-dead raw = bar close.

---

## 12. Fees

`FEE_PER_SIDE = 0.0004` (4 bps) on **slipped** notional, both sides.

```
entryFee = qty * slippedEntry * 0.0004
exitFee  = qty * slippedExit  * 0.0004
pnl      = qty * (side==long ? exit-entry : entry-exit) - entryFee - exitFee
```

No funding. No maker/taker mix. No liquidation fee.

---

## 13. Position sizing

Uses **slipped entry** and **unslipped stop**:

```
dist = abs(slippedEntry - theoreticalStop)
qty  = (equity * 0.004) / dist
```

`equity` is that **symbol’s** cash after closed trades only (no MTM) at the moment the fill is accepted.

`pnlR = slippedDelta / abs(slippedEntry - theoreticalStop)`
so R uses the same dist as sizing, not an unslipped-to-unslipped risk.

One position per symbol. `MAX_OPEN = 3` never binds in the per-symbol runs.

---

## 14. Combined ALL

Not a 3-slot portfolio. Not $60k.

1. Backtest each symbol **independently** from `STARTING_EQUITY = 10000`.
2. Collect closed trades (`exitReason !== "open"`).
3. Sort by `exitT` ascending, then `symbol` lexicographic (BTC < DOGE < ETH < LINK < SOL < XRP) — the published ALL curve used `flatMap` in UNIVERSE order (BTC, ETH, SOL, XRP, DOGE, LINK) with a **stable** `sort` on `exitT` only. Same-timestamp ties therefore keep that universe order, not A–Z. For a match on same-ms exits, apply UNIVERSE order.
4. `eq = 10000; for trade in sorted: eq += trade.pnl`
5. ALL max DD = peak-to-trough on that **exit-only staircase**. No intra-trade MTM, no overlapping-host mark.

ALL PnL +810.54 is the **sum of six independent 10k books**.
ALL `pnlPct = pnl / 10000 = +8.105%` — this is **not** return on $60k. On six accounts it is +810 / 60000 ≈ **+1.35%**.

Per-symbol max DD uses that symbol’s MTM curve (open positions marked at close). ALL max DD does not.

---

## 15. Frozen ledger

File: `public/remora-ledger-v1.json`

Each row is one closed trade:

```
t, barIndex, symbol, label, side, kind,
entry, stop, target, sharkT, score, note,
exit, exitT, exitReason, qty, pnl, pnlR, fees, barsHeld
```

- `t` / `barIndex` = fill bar open / index
- `sharkT` = print bar open
- `entry` / `exit` already slipped
- `stop` / `target` not slipped
- `pnl` already net of both fees

Diagnostics on that ledger:

- 61 / 155 fills have `t === sharkT + 3600000` (classification-bar fills)
- 84 / 155 `barsHeld === 1`
- 0 wounded
- 0 host_dead

If your independent run is not inside ~ rounding noise of +810.54 / 155 / PF 1.2601 on **this** window, you did not implement v1.

---

## Known v1 defects (do not silently repair)

1. Wounded entry at P+1 open after seeing P+1 close (lookahead). Unused in PnL.
2. Hunter fill allowed on P+1 after seeing P+1 close (61 trades).
3. Entry candle never sees stop/target (optimistic).
4. ALL % return uses one 10k denominator on six books.
5. Watch window is 9 bars, not the “8” in spec v1.
6. `confirm` ignored; last history bar may be forming.

A cleaner executable fork is a new freeze id. It is not v1.
