Metadata-Version: 2.5
Name: alphaboard-zorro-sidecar
Version: 0.1.1
Summary: Watch Zorro output files and push trades + heartbeats to an AlphaBoard server without changing any lite-C strategy code.
Author-email: Adam King <adm.king@gmail.com>
License: Apache-2.0
Keywords: algotrading,alphaboard,monitoring,trading,zorro
Requires-Python: >=3.10
Requires-Dist: alphaboard>=0.1.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: watchdog>=4.0.0
Description-Content-Type: text/markdown

# alphaboard-zorro-sidecar

Watches Zorro's trade-log CSVs and pushes new fills to an AlphaBoard server. Runs on the same Windows VPS as Zorro. **Zero changes** to your lite-C strategy code for closed-trade ingestion; **one `#include` + one call** per strategy if you also want per-bar equity (OPM-692, see below).

## Install

```bash
pip install alphaboard-zorro-sidecar
```

## Pick the right Zorro file(s)

Zorro writes trades to two different CSV families depending on what mode produced them:

| Zorro mode | File pattern | Lives in | One file per |
|---|---|---|---|
| `[Test]` / `[Train]` (when `LOGFILE` is set) | `<Strategy>_trd.csv` | `Log\` | strategy |
| `[Trade]` real account | `trades.csv` | `Log\` | host (shared) |
| `[Trade]` demo account | `demotrades.csv` | `Log\` | host (shared) |

The column shape is the same per Zorro's export docs, but live `[Trade]` mode shares ONE file across every strategy on the host — the per-row `Name` column distinguishes them. Both shapes are supported below.

## Configure

Create `config.yaml`. Each instance watches one CSV file, and the file's shape decides which of the static fields it needs (OPM-1390):

| `csv_format` | Where a row's strategy comes from | `strategy:` | `broker:` / `account:` |
|---|---|---|---|
| `log`, `finished_trades` — one file = one strategy | the instance | **required** | **required** |
| `finished_trades` + `name_column: Name` — live `trades.csv` | the row's `Name`, routed by `strategy_map` | optional: the fallback for a blank `Name` (such rows are skipped without it) | required unless every `strategy_map` rule sets both |
| `equity` | the row (`alphaboard_equity.c` writes it) | unused — warned at load | required unless every `strategy_map` rule sets both |
| `positions` | the row (`alphaboard_positions.c` writes it) | unused — warned at load | only for rows a rule does not route or with an empty `source`; required unless every rule sets both |

A `strategy_map` rule that sets neither `broker` nor `account` inherits the instance's. An instance with such a bare rule and no `broker:` / `account:` of its own refuses to load, and the error names the rule. Two operating modes — mix freely:

### (1) One file = one strategy — legacy

For per-strategy `_trd.csv` files (or any custom CSV your strategy writes to its own file):

```yaml
api_url: http://localhost:8000
api_key: sk_...

instances:
  - name: perp_momentum
    strategy: perp_momentum_eth
    broker: hyperliquid
    account: "0x1234abcd5678ef90"   # the real wallet / account id the connector polls
    csv_format: trd
    trade_log_path: C:\Zorro\Log\perp_momentum_eth_trd.csv
```

### (2) One file = many strategies — live `[Trade]` mode

For `Log\trades.csv` / `Log\demotrades.csv`, set `name_column: Name`. The parser reads the `Name` column per row and uses it as the AlphaBoard strategy — overriding the static `strategy:` (which becomes a fallback for rows where Name is empty). The sidecar lazily creates one `AlphaBoard` client per distinct strategy seen, and heartbeats each separately.

```yaml
api_url: http://localhost:8000
api_key: sk_...

instances:
  - name: live_real
    strategy: zorro_real        # optional: fallback for rows missing a Name
    broker: ibkr
    account: U1234567           # your real IB account id
    csv_format: trd
    name_column: Name
    trade_log_path: C:\Users\me\OneDrive\Zorro\Log\trades.csv

  - name: live_demo             # no `strategy:` — a row with a blank Name is skipped (logged once)
    broker: ibkr
    account: demo
    csv_format: trd
    name_column: Name
    trade_log_path: C:\Users\me\OneDrive\Zorro\Log\demotrades.csv

heartbeat_interval_seconds: 60
```

> **Heads up on `trades.csv` headers.** It's a *perpetual* file — Zorro writes the header once and appends forever. If the header was written by an older Zorro version it may have fewer columns than the rows the current Zorro is now appending. Modern header is:
>
> ```text
> Name,ID,Type,Asset,Lots,Open,Close,Entry,Exit,ExitType,Roll,Commission,MAE,MFE,Profit
> ```
>
> If yours differs from what current rows actually contain, the column-name lookups in the parser will silently mis-attribute fields. Rewrite the first line once (history is preserved) — example PowerShell:
>
> ```powershell
> $p = "C:\Users\me\OneDrive\Zorro\Log\trades.csv"
> Copy-Item $p "$p.bak"
> $hdr = "Name,ID,Type,Asset,Lots,Open,Close,Entry,Exit,ExitType,Roll,Commission,MAE,MFE,Profit"
> @($hdr) + (Get-Content $p | Select-Object -Skip 1) | Set-Content $p -Encoding UTF8
> ```

### Contract sizes — Zorro `Lots` are contracts, not units (OPM-1304)

Zorro's `Lots` column counts **contracts**. How many units a contract is worth
is the asset's *contract size*, which lives in column 10 of the asset list
Zorro priced the trade from — spelled `LotAmount` in the generic/broker
templates and `Multiplier` in the IB and FIX ones (same field, same position):

```text
units = Lots x LotAmount
```

Equities and most futures have a contract size of exactly `1`, which is why
storing lots as units looks fine right up until a crypto perp or an FX lot
lands on the dashboard. Real numbers from a live Hyperliquid asset list:
`BTC` 0.000001, `ETH` 0.0001, `LINK` 0.1. A 4-lot fill on an asset sized
0.0001 is 0.0004 units, not 4 — a **10,000x** overstatement of stored notional,
and every position, exposure and per-unit figure derived from it.

#### A contract size belongs to the *asset list*, not to the asset name

This is the part that is easy to get wrong, and it was found on a real
15-month journal rather than in a test. One Zorro install prices `AUD/USD` at
**25,000** units per lot under `AssetsIB.csv` and at **1** unit per lot under
its Oanda-era list — and both eras write into the same `trades.csv`. Apply one
install-wide map to all of it and a 20,822-lot Oanda trade becomes
**520,550,000** units.

So the recommended place for asset lists is on the `strategy_map` rule, where
each strategy is priced from the list Zorro actually priced it with:

```yaml
api_url: http://localhost:8000
api_key: sk_...

instances:
  - name: live_real
    csv_format: finished_trades
    name_column: Name
    trade_log_path: C:\Users\me\OneDrive\Zorro\Log\trades.csv

    strategy_map:
      "Pairs_LIVE*":
        strategy: Pairs_LIVE
        broker: ibkr
        asset_lists: [C:\Users\me\OneDrive\Zorro\History\AssetsPairs.csv]
      "Perp_HL*":
        strategy: Perp_HL_Native
        broker: hyperliquid
        asset_lists: [C:\Users\me\OneDrive\Zorro\History\AssetsPerp.csv]
        contract_sizes: { MNQ: 2 }   # pins win over the list
      "Rot12:*":
        strategy: Rot12              # no list -> quantity stays raw Lots
        broker: historic
```

Resolution mirrors `strategy_map` itself: **first matching rule wins**, and a
rule that declares neither key *inherits* the instance's sizes. That is not the
same as declaring an empty block — inheriting means "whatever the instance
says", including nothing. Scoping only the strategies that need it is enough.

For a single-broker install where every strategy really was priced from the
same lists, a shared block at the config root or on the instance is still the
short form:

```yaml
asset_lists:
  - C:\Users\me\OneDrive\Zorro\History\AssetsIB.csv
contract_sizes:            # optional explicit pins; these win over the lists
  XYZ100: 0.0001
```

Relative paths resolve against the config file's directory. Precedence is
**rule block → instance block → root block → nothing (raw `Lots`)**, and each
level *replaces* the one above it rather than merging with it.

The backfill CLI takes the file-wide form as flags, both repeatable, plus a
policy for assets none of them price (default `fail`, see below):

```bash
alphaboard-zorro-backfill trades Log\trades.csv --out trades_canon.csv \
    --asset-list History\AssetsIB.csv --asset-list History\AssetsPerp.csv \
    --contract-size XYZ100=0.0001 --on-unknown-contract-size fail
```

Passing `--map-config config.yaml` inherits everything that config resolved —
including the per-rule scoping — so a backfill converts exactly the way live
ingestion does without re-typing the paths.

**Rules the loader deliberately enforces**, because each alternative is a way
for a four-orders-of-magnitude error to ship looking like a normal run:

- **Configure nothing and nothing changes.** With no asset list and no pins,
  `quantity` stays as raw `Lots`, exactly as before OPM-1304. The `trades`
  transform prints which path it took on *every* run, naming the strategies
  that carry their own sizes.
- **An asset the lists don't cover is an error, not a size-1 guess.** Once
  *any* sizes are configured, a `trades` run that meets an asset none of them
  price writes nothing, exits 2, and prints the names plus a stub to paste —
  `Name,LotAmount` rows and the matching `--contract-size NAME=1` pins — so
  the fix is a paste, not a hunt (OPM-1311). If they really are size 1 (a
  handful of US equities missing from the IB list is the usual case), paste
  the stub; or say what to do instead with `--on-unknown-contract-size one`
  (keep quantity as raw `Lots`, noted) or `skip` (drop their fills, noted).
  Coverage is judged per strategy: an asset priced for one strategy and not
  another is reported. Before OPM-1311 this was a trailing `note:` and the
  quantities went through as lots, which is how six unpriced equities landed
  looking exactly like a correct run.
- **A file with no `Name` column, or with neither contract-size spelling, is
  an error**, not an empty result. "No overrides needed" and "I could not read
  this file" must not look alike.
- **Two lists in the same block disagreeing about one asset is an error**, not
  last-write-wins. If the disagreement is genuine — the `AUD/USD` case above —
  the fix is to scope the lists to their strategies, not to pick a winner.

> **Already ingested trades keep their old quantities.** A fill's identity key
> is built from raw `Lots` on purpose — re-keying it on converted units would
> make every stored fill look new and duplicate the whole journal on the next
> backfill. The trade-off is that the backfill *dedupes* rather than updates,
> so rows ingested before you configured an asset list keep their lot-valued
> quantity. Correcting them means purging those strategies and re-ingesting;
> until then, treat pre-conversion history as lots.

### (3) Per-bar equity bridge (OPM-692)

Closed-trade events alone draw a *step* curve — equity only moves when a position closes. To see live equity moving *between* trades (and to pick up unrealized PnL the moment the bar advances), each strategy can append a row to a shared `Log\equity.csv` once per bar using a tiny lite-C include.

**Step 1 — install the include** (one-time per Zorro install):

Copy [`lite-c/alphaboard_equity.c`](lite-c/alphaboard_equity.c) into the Zorro folder that matches the include form you use. Zorro resolves `#include <file>` from `Zorro\include\` and `#include "file"` from `Zorro\Source\` (the script's own folder):

| Include line in your script | Put the file in |
|---|---|
| `#include <alphaboard_equity.c>` | `C:\Zorro\include\` |
| `#include "alphaboard_equity.c"` | `C:\Zorro\Source\` |

It defines `alphaboard_log_equity()`.

**Step 2 — wire each strategy** you want equity for: one include at the top of the script, one call as the last statement of your **existing** `run()`. Do not add a second `run()`, and do not add `set(UTC)` (see Timestamps below).

```c
#include <alphaboard_equity.c>      // top of the script

function run()                      // your existing run()
{
    BarPeriod = 60;
    // ...your strategy logic, unchanged...
    alphaboard_log_equity();        // last statement: one row per bar
}
```

The include writes one row per call to `Log\equity.csv` with columns `strategy,timestamp,equity`. The `strategy` column defaults to Zorro's built-in `Script` variable (the script file name). It **must byte-match the strategy your trades log under** (the `Name` column of the live `trades.csv`) or the dashboard splits one strategy in two — a curve with no trades, and trades with no curve. If your script file name differs from that `Name`, override it with one line above the include (and give two strategies sharing a script distinct names this way too):

```c
#define ALPHABOARD_STRATEGY "Coint_Pairs_DynError0_Exit_IB_TEST_Arb"  // == the trades Name
#include <alphaboard_equity.c>
```

Note: `Equity` is **account**-level, so this bridge is accurate only when one strategy maps to one (sub)account. Several scripts on one shared broker account each log the same account equity — log equity once from a single "portfolio" script instead.

**Step 3 — point the sidecar at the file:**

```yaml
instances:
  - name: live_equity
    csv_format: equity
    trade_log_path: C:\Users\me\OneDrive\Zorro\Log\equity.csv
    strategy_map:               # every row names its strategy; route each to its broker account
      "Coint_Pairs*": {strategy: Coint_Pairs, broker: ibkr, account: "U1234567"}
```

No `strategy:` here: every row carries its own, and the sidecar warns if one is set. With a fully routed `strategy_map` the instance needs no `broker:` / `account:` either; give it both if you want unmapped names to post under one default account instead (no map at all), or if any rule leaves one of them unset.

Each new row becomes one `POST /api/v1/equity` attributed to the row's strategy. The sidecar lazily spawns one `AlphaBoard` client per distinct strategy it sees (same machinery as `name_column` mode above), so a single `equity` instance covers every strategy on the host.

**Why a file-based bridge?** Lite-C runs synchronously inside Zorro's trading loop. A direct `http_send` would block your strategy if the API is slow or down. Local disk writes are microseconds; the sidecar handles network I/O on its own thread, so monitoring can never stall execution.

**Cadence.** `alphaboard_log_equity()` writes one row each time you call it. Once per bar = call it every `run()`. Once per day = guard with `if(day(0) != day(1))`. There is no minimum cadence on the AlphaBoard side.

**Timestamps.** The include uses the current bar time (`strdate(fmt, 0)`), which Zorro returns in UTC by default — no `set(UTC)` needed (it is not a documented `set()` flag). The sidecar interprets the timestamps as UTC unconditionally, so a script that overrides Zorro's time zone must not be used with either include: its points would land wrong-shifted.

### (4) Per-bar open-positions bridge — Zorro's IBOR (OPM-753, OPM-1386)

Reconciliation is three-way: what the strategy *thinks* it holds (IBOR), what the OMS recorded, and what the broker reports (Street). Zorro has no open-positions export, so without this bridge the Book for a Zorro strategy is a copy of Street and every break goes unseen. The include appends each open trade to one shared `Log\positions.csv` once per bar; the sidecar folds those rows into complete `sync_positions()` snapshots, including an explicit **quantity 0** for any symbol that dropped out, so a closed position clears from IBOR instead of lingering.

**Step 1 — install the include** (one-time per Zorro install):

Copy [`lite-c/alphaboard_positions.c`](lite-c/alphaboard_positions.c) into the Zorro folder that matches the include form you use — `Zorro\include\` for `#include <alphaboard_positions.c>`, `Zorro\Source\` for `#include "alphaboard_positions.c"` (same table as under (3)). It defines `alphaboard_log_positions()`.

**Step 2 — wire each strategy** you want IBOR for: one include at the top of the script, one call as the last statement of your **existing** `run()`. No second `run()`, no `set(UTC)` (timestamps are already UTC — see Timestamps under (3)).

```c
#include <alphaboard_positions.c>      // top of the script

function run()                         // your existing run()
{
    // ...your strategy logic, unchanged...
    alphaboard_log_positions();        // last statement: every bar
}
```

Call it on every bar, not only when a trade fires: a bar with a marker and no position rows is exactly how the sidecar learns a strategy went flat. The function returns immediately outside a live session (`is(TRADEMODE)` false), so a backtest of a wired script never writes into the live file.

**What one bar writes.** A boundary marker plus one row per open, non-pool trade, with columns `strategy,timestamp,symbol,side,quantity,avg_price,script,source`:

```
__BAR__,2026-09-07 04:00:00,Perp_HL_Native_V2,,,,Perp_HL_Native_V2,
Perp_HL_Native_V2,2026-09-07 04:00:00,BTCUSD,buy,0.05000000,61234.50000000,Perp_HL_Native_V2,HL_Real
Rot12,2026-09-07 04:00:00,EUR/USD,sell,10000.00000000,1.08210000,Rot12,IB_Live
Rot12:A2,2026-09-07 04:00:00,EUR/USD,sell,10000.00000000,1.08210000,Rot12,IB_Live
```

* `strategy` is the same identity Zorro writes to the `Name` column of `trades.csv`: `Script:Algo` for a trade opened under `algo()` (the `Rot12:A2` row above), plain `Script` otherwise. One `strategy_map` convention therefore covers both files — a per-script rule such as `"Rot12*"` matches the trades stream and the positions stream alike — and one script running several algos still reports each as its own book. The `Script` half is `ALPHABOARD_STRATEGY`, which defaults to the script file name; pin it with `#define ALPHABOARD_STRATEGY "…"` above the include to keep a **stable** name across script renames (OPM-1193: a renamed script became a new strategy and the old one's 24 positions never went flat).
* `script` is the owning script (`Script`). Every wired script appends to the **same file**, so their rows interleave; the sidecar keeps one open snapshot per script and only that script's next marker closes it. A marker from one script can never zero another script's half-written bar.
* `source` is Zorro's `SourceTrade`: the trading-account name from the asset list, the `HL_Real` in `HL_Real:BTCUSD` (`Account` if the asset has no source). This is what routes a book to the right broker account, below.

**Step 3 — map sources to broker accounts and point the sidecar at the file:**

```yaml
sources:                               # Zorro account name -> the broker account it is
  HL_Real: {broker: hyperliquid, account: "0x…your wallet…"}   # QUOTE hex / numeric ids
  IB_Live: {broker: ibkr, account: "U1234567"}

instances:
  - name: positions
    csv_format: positions
    trade_log_path: C:\Users\me\OneDrive\Zorro\Log\positions.csv
    strategy_map:                      # optional: rename only — `sources:` decides the broker account
      "Perp_HL_Native*": {strategy: Perp_HL_Native}
```

No `strategy:` on the instance (rows carry their own; the sidecar warns if one is set). With a `sources:` map, no `broker:` / `account:` either, on the instance or on its rules: every row's `source` names the broker account, and a rule only renames. Rules and the instance may still carry a broker account; it is only used by a row with an **empty** `source` (files from the older 6-column include). Such a row under an instance with nothing to fall back to is skipped, logged once as `sources.missing`.

Each snapshot is posted under `(strategy, sources[source].broker, sources[source].account)`. That is how a multi-broker strategy such as a perp-carry (perp on Hyperliquid, spot on IB) becomes **two** IBOR books that each reconcile against their own broker, and how a strategy running on two Hyperliquid wallets stays two books. `strategy_map` still renames the strategy and its `broker`/`account` still apply to trade rows, but for positions the row's `source` wins.

A `source` with no `sources:` entry is **skipped**, and logged once per name as `sources.unknown` at INFO. The sidecar deliberately does not fall back to the instance's account here: posting one wallet's positions under another's would manufacture breaks on both. If IBOR stops updating after an asset-list change, that log line is the first thing to look for. A per-instance `sources:` block replaces the top-level one for that instance.

Quote account ids in YAML. A bare `0xabc…` is a hex **integer** to a YAML parser; the sidecar refuses to load such a value rather than silently route to a mangled address.

**Upgrading from the 6-column layout.** Files written by the older include (no `script`/`source` columns) still parse: those rows have an empty source and route to their rule's `broker`/`account`, else the instance's. Rows the new include appends under an old 6-column header are recovered too. The clean rollout is still: stop the wired scripts, rename the old `positions.csv` away, copy the new include into `Zorro\include\`, add `sources:` to the sidecar config, restart the sidecar, then restart the scripts so the first live bar writes the new header.

### Seed historical equity (burn-in, OPM-770)

So the dashboard shows each strategy's curve from day one (not just a flat line that starts when the sidecar does), seed history before going live.

**Source — re-run the strategy in Zorro [Test] mode with the include.** In [Test] mode Zorro's `now` returns each *simulated* bar's time and `Equity` is marked-to-market per bar, so `alphaboard_log_equity()` appends a genuine back-dated, per-bar `equity.csv` spanning the whole test window. This is the truest per-bar curve available — it's the *backtest* equity (depends on the price history + a deterministic strategy), not the broker's exact realised equity, but it's the same quantity the live bridge logs going forward.

1. **Generate.** Run the strategy in **[Test]** over the burn-in window (`StartDate`/`EndDate`) with `set(UTC)`. Do this against a **fresh/copied `equity.csv`** so the seed export isn't interleaved with live rows. (Several strategies can accumulate into one file — equity rows are self-describing by `strategy`.)
2. **Transform** to canonical CSV (add `--date-format` only for a non-default locale; the include writes `%Y-%m-%d %H:%M:%S`, which the defaults parse):

   ```bash
   alphaboard-zorro-backfill equity Log\equity.csv --out equity_canon.csv
   ```

3. **Backfill** — deterministic UUIDv5 key + server `ON CONFLICT DO NOTHING`, so it's safely re-runnable:

   ```bash
   alphaboard backfill equity_canon.csv --kind equity
   ```

4. **Verify:** `GET /api/v1/equity/series?strategy=<name>&since=<window-start>` returns the curve, and the dashboard shows it under the *same* strategy as that strategy's trades.

**Name alignment (make-or-break).** The seeded equity `strategy`, the live equity `strategy`, and the live trades' `Name` must be byte-identical or the dashboard splits the strategy. The clean path: set `#define ALPHABOARD_STRATEGY "<the trades Name>"` in the script (used by both the Test and live runs). If you can't re-tag at the source, force it at transform time:

```bash
alphaboard-zorro-backfill equity Log\equity.csv --out equity_canon.csv \
    --strategy Coint_Pairs_DynError0_Exit_IB_TEST_Arb
```

(`--strategy` re-attributes *every* row, so use it only for a single-strategy equity file.)

**Live overlap.** Seeded and live points at the exact same `(strategy, snapshot_at)` dedupe to one row; points a few seconds apart coexist — the seam needs no special handling. Re-running the backfill reports `deduped`, confirming idempotency.

### No equity bridge? Derive the curve from `trades.csv` (OPM-1307)

The route above needs `alphaboard_equity.c` installed in your Zorro and a
[Test] re-run of every strategy. If you just want your live history on the
dashboard today, the finished-trades journal you already have is enough:

```bash
alphaboard-zorro-backfill equity-from-trades Log\trades.csv \
    --out equity_canon.csv --map-config config.yaml \
    --capital-base Pairs_LIVE=100000 --capital-base Perp_HL=50000

alphaboard backfill equity_canon.csv --kind equity
```

Rows are routed through the same `strategy_map` as live ingestion, sorted by
close time, and accumulated per strategy: `equity = capital base + cumulative
Profit`. Zorro's `Profit` is already net of `Commission`, so commission is not
subtracted twice.

**`--capital-base` is required, and the transform refuses to guess it.** A
finished-trades journal records closed-trade PnL and carries no account size at
all. Accumulating `Profit` alone gives a curve based at 0.00 that a losing
strategy drags negative — and percentage returns are undefined on a series that
touches or crosses zero, so that is precisely the curve AlphaBoard has to refuse
to price (see [What `equity` means](../../docs/sdk-quickstart.md#what-equity-means)).
Pass one bare `--capital-base 100000` to apply a single base to every strategy
in the file, or repeat `NAME=AMOUNT` per strategy. If a resulting curve still
ends at or below zero the transform says so, naming the capital base it used.

**What this curve is and isn't.** It is a *realised*-equity curve: one point per
closed trade, moving only when a trade closes. Open trades contribute nothing
because the journal does not record their unrealised PnL, so intra-trade
drawdown is invisible and the curve is flat between closes. Trades closing in
the same minute collapse to one point carrying that minute's closing equity —
`equity_snapshots` is `UNIQUE (strategy_id, snapshot_at)` and a repeated key
*dedupes* rather than updates, so emitting one row per trade would silently keep
the first value of each minute and drop the rest. For true per-bar
mark-to-market use the equity bridge above or a broker connector.

## Run

```bash
alphaboard-zorro-sidecar --config config.yaml
```

What it does:

1. Reads each instance's `trade_log_path` to remember the current position.
2. Watches each path for modifications via the OS file-system events API.
3. On change: parses any *new* rows since last read.
   - With `name_column:` set → each row's strategy comes from that column;
     lazy-creates an SDK client per distinct strategy.
   - Without `name_column:` → every row attributes to the static `strategy:`
     (one client per instance).
4. Pushes each fill through the AlphaBoard SDK (which batches + retries in
   the background).
5. Heartbeats periodically — once per *active* strategy (any strategy seen
   in the last 24h). Long-retired strategies stop pinging automatically.

State is persisted to `<state_dir>/tail-state.json` (default `~/.alphaboard-sidecar/`) so a restart resumes from where the previous run left off — no duplicate trades, no gaps.

### Startup check: does the server know your broker accounts? (OPM-1388)

Before it tails anything, the sidecar collects every `(broker, account)` pair the config can post under (each instance that declares one, each `strategy_map` rule's route, each `sources:` route; equity instances are skipped) and asks `GET /api/v1/health/accounts` which of them the server *reconciles*: a connector polls the account, or broker positions have already arrived for it. The server auto-creates any pair it is posted, so without this a typo in `account:` becomes a phantom account that reads healthy forever while the real one sits unreconciled.

- **Unknown pair, server reconciles other accounts** → refuses to start (exit 2) and prints the pair, e.g. `hyperliquid/0x1234abcd5678ef91: the server has no such account`. Fix the spelling, or register a connector for that account first.
- `--allow-new-accounts` → warns instead, for an account whose connector you are about to add.
- **Server reconciles nothing yet** (first install, IBOR-only) → warns only; there is no spelling to have missed.
- **Server unreachable or older than the endpoint** → check skipped with a warning; the sidecar still starts. The API logs `recon.unpaired_ibor_account` and counts `alphaboard_unpaired_ibor_snapshots_total` if a posted account turns out to be a phantom, and the dashboard's Health page lists it.

## Durability (no-data-loss, OPM-731)

The sidecar enables the SDK's **durable spool by default** (under
`<state_dir>/spool/<instance>/`; set `spool_dir: ""` in the YAML to opt
out, or point `spool_dir:` at another directory). Two guarantees follow:

1. Events the sidecar has read are mirrored to a SQLite outbox on disk and
   deleted only after the server confirms them — they survive a hard kill
   and an API outage of any length.
2. The CSV tail offset is committed **only after the SDK durably accepts
   the rows**. If the process dies between reading a row and the SDK
   accepting it, the restart re-tails that row instead of silently losing
   it; server-side dedup makes the re-send a no-op.

## What happens when…

- **A new strategy fires its first trade.** Sidecar sees a new `Name` value
  in the next row, creates a fresh SDK client for it, posts the trade, and
  starts heartbeating it on the next cadence. AlphaBoard auto-creates the
  strategy in the DB on the first POST. No config change required.
- **A strategy retires** and stops appearing in the file. Heartbeats for it
  stop after 24h (the active-strategy TTL). The historical trades stay in
  the DB — the strategy just shows up as quiet on Health.
- **The AlphaBoard API is unreachable.** Events spill to the on-disk spool
  (bounded by rows/bytes/age) and the sidecar keeps reading the CSV. When
  the API comes back, the spool drains automatically — no loss, original
  timestamps preserved.
- **The sidecar is killed mid-outage.** The spooled events are already on
  disk; on restart the sidecar pre-creates a client for every strategy
  with a pending outbox (even if that strategy writes no new rows) and the
  SDK replays them. Rows read but not yet spool-accepted re-tail from the
  uncommitted offset.
- **You restart the sidecar mid-day.** Tail state on disk → resume from the
  last durably-accepted byte offset; no replays, no gaps.

## Licence

Apache 2.0.
