# 06 · Risk, Safety & Compliance

The strategy/risk gate sits **between signals and orders** and is the most
important code you will write. A mediocre strategy with great risk control
survives; a great strategy with no risk control eventually blows up. This layer
is also where **paper vs live** is enforced.

```
Signal(s) ──► Strategy (combine/filter) ──► size ──► Risk gate ──► Intent
                                                          │ reject
                                                          ▼
                                                   drop + notify
```

---

## 1. Position sizing (never trade a "gut" quantity)

Size every trade from **risk-per-trade**, not from a fixed share count.

**Fixed-fractional (recommended default):**

```
riskPerTrade   = equity * riskPct            # e.g. 0.5%–1% of equity
stopDistance   = abs(entry - stop)           # from the Signal geometry
qty            = floor( riskPerTrade / stopDistance )   # then round to lot size
```

So a wider stop ⇒ smaller size; risk in currency stays constant. Variants to
support: **fixed-notional**, **volatility-targeted** (size by ATR so each
position contributes equal volatility), and **Kelly-fraction-capped** (use a
*fraction* of Kelly, never full Kelly). Always clamp to `maxPositionNotional`
and the venue's min/step size.

---

## 2. Strategy layer — turning signals into intents

The strategy layer subscribes to `Signal`s and decides whether/how to act.
Express **confluence and filters as rules**, not new detectors:

```ts
type StrategyRule = {
  id: string;
  entry: {
    require: string[];            // detector ids that must agree, e.g. ["osc.rsi","osc.macd"]
    within: number;               // bars window for agreement
    filters?: RuleFilter[];       // regime/time/session filters
  };
  sizing: { method: "fixed_fractional" | "vol_target" | "fixed_notional"; riskPct?: number; };
  exit: { stop: "signal" | "atr"; atrMult?: number; targets?: number[]; trail?: TrailSpec; };
  mode: "advisory" | "approval" | "auto";
  maxConcurrent: number;          // cap open positions from this rule
};
```

Common **filters** (doc 03 §5): trade breakouts only when `ADX>25`; take
mean-reversion only when `ADX<20`; only in the higher-timeframe trend direction;
respect **session/market hours**; skip around scheduled high-impact news/earnings;
enforce a **cooldown** so one symbol can't re-fire every bar.

---

## 3. The risk gate — hard limits (pre-trade, every order)

The gate runs **again at order time** (caps can change between alert and fill).
Reject if *any* limit is breached:

| Limit | Example | Why |
|-------|---------|-----|
| **Per-trade risk** | ≤ 1% of equity | one loss can't hurt much |
| **Daily loss stop** | halt new entries after −3% day | stops tilt/death-spiral |
| **Max open risk** | Σ open risk ≤ 6% | correlated positions can align |
| **Max positions** | ≤ N total, ≤ M per symbol | concentration |
| **Max leverage / buying power** | ≤ account allowance | margin calls |
| **Per-symbol exposure** | ≤ x% of equity | single-name blowups |
| **Order sanity** | price within y% of last; qty > 0; notional ≥ venue min | fat-finger / rejects |
| **Duplicate guard** | `clientOrderId` unseen | double-fill |
| **Rate limit** | ≤ Z orders/min | runaway loops |

```
risk.check(intent):
  if killSwitch.active:                    return reject("kill switch on")
  if dayPnL <= -maxDailyLoss*equity:       return reject("daily loss stop hit")
  if intent.risk.riskPct > maxPerTrade:    return reject("per-trade risk too high")
  if openRisk + intent.risk.riskAmount > maxOpenRisk*equity: return reject("max open risk")
  if positions.count >= maxPositions:      return reject("max positions")
  if priceFarFromMarket(intent):           return reject("price sanity")
  if not withinTradingHours(intent.symbol):return reject("market closed")
  return ok()
```

---

## 4. The kill switch (one flag that stops everything)

- A single global `killSwitch` flag (in Redis + persisted) that, when set,
  **blocks all new orders** across every strategy, account, and channel.
- Triggerable from **any** channel (`/kill` in Telegram/WhatsApp, a big red
  button in the web app), and **auto-triggered** by circuit breakers: daily loss
  stop hit, data feed stale > N seconds, broker reject-rate spike, or reconciliation
  drift detected.
- Kill switch **does not** auto-liquidate (that could realise losses at the worst
  time) — it *freezes new risk*. Provide a separate explicit `/flatten all` for
  deliberate exit.

---

## 5. Paper → Live promotion gates

Going live is a **deliberate, gated** transition — never a default.

1. `EXECUTION_MODE` defaults to `paper`. `live` requires an explicit env flag.
2. Per **account**, a live order is refused until a **signed acknowledgement**
   is on file (a stored, timestamped "I accept live-trading risk" record).
3. **Promotion criteria** (recommended before enabling `auto` live for a
   strategy): a minimum paper track record (e.g. ≥ N trades, ≥ K weeks),
   acceptable drawdown, and a backtest that matches paper results within
   tolerance.
4. **Start tiny:** first live runs use minimum size and `approval` mode (human
   taps every order) before any `auto`.
5. Live mode shows a persistent **`LIVE`** banner everywhere and colours alerts
   differently so no one confuses paper and live.

---

## 6. Secrets & security

- **No broker keys, tokens, or secrets in the repo, ever.** Load from a vault /
  cloud secret manager / KMS at runtime. `.env` files are git-ignored and used
  only for local dev with **paper/testnet** keys.
- Scope API keys to the **minimum** (e.g. Alpaca "trading" only if needed;
  disable withdrawals on exchange keys; IP-allowlist where supported).
- Sign and verify every inbound webhook (WhatsApp/Telegram/broker) — doc 04 §4.
- Encrypt secrets at rest; rotate on a schedule and on staff changes.
- Principle of least privilege for the DB and message bus; separate credentials
  per worker role.

---

## 7. Audit, observability & testing

- **Append-only audit** linking `signal → alert → decision → order → fill` with
  ids and timestamps — for compliance *and* for answering "why did it trade?".
- **Metrics/alerts** on: signal→alert latency, approval latency, order
  reject rate, fill slippage vs model, feed staleness, reconciliation drift,
  daily P&L vs limits.
- **Tests:** detectors/indicators are pure ⇒ unit-test against fixtures;
  broker adapters pass a shared conformance suite against sandbox/testnet; run
  the whole pipeline in paper as an **integration test** before any live change.

---

## 8. Compliance & disclaimers (do not skip)

- **This system is a tool, not advice.** If you operate it for others or publish
  signals, you may be providing **investment advice / running a trading service**
  — which is **regulated** (SEBI in India, SEC/FINRA in the US, FCA/MiFID II in
  the EU/UK, etc.). Get licensed or get legal advice *before* doing so.
- Show a clear **risk disclaimer** in the app and in alerts ("Trading involves
  risk of loss. Past/backtested performance does not guarantee future results.
  Not investment advice.").
- Respect each broker/exchange's **API terms**, automated-trading rules, and
  market-data licensing (redistribution of quotes is often restricted).
- Keep records (trade blotter, comms) per your jurisdiction's retention rules.
- Handle user data (device tokens, phone numbers, emails) under GDPR/DPDP as
  applicable; let users opt out of channels.

---

## 9. "Claude task" prompts

> **Claude task — Risk gate + position sizing**
> Create `risk/gate.ts` (`risk.check(intent)` per §3), `risk/sizing.ts`
> (fixed-fractional + vol-target + notional, clamped to venue lot/step), and
> `risk/killswitch.ts` (global flag + circuit breakers). Wire the gate into the
> Order Router so it runs at order time. **Acceptance:** an intent breaching any
> limit is rejected with a reason and a notification; sizing produces constant
> currency-risk across different stop distances; setting the kill switch blocks
> all new orders while leaving existing positions untouched.

> **Claude task — Mode guardrails + secrets + audit**
> Implement `EXECUTION_MODE` handling (default `paper`; `live` needs the env flag
> **and** a stored signed ack per account), the `LIVE` banner, secret loading
> from the vault (no keys in repo), and the append-only audit linking
> signal→alert→decision→order→fill. **Acceptance:** a live order without the ack
> is refused; the audit trail reconstructs the full path of any fill; no secret
> is present in the codebase or logs.

Build order and acceptance for the whole system:
[`07-claude-build-prompts.md`](07-claude-build-prompts.md).
