# Research: TS Strategy ↔ Deno Runner ↔ Session Runner Relationship

**Date**: 2026-03-12
**Git Commit**: 2d0cf45
**Branch**: main

## Research Question

How does the relationship between the TypeScript strategy and the Deno strategy runner look? Can a strategy itself declare which weather data sources (if any) it wants? Will the session runner respect that?

## Summary

Strategies are TypeScript files that run inside an embedded Deno (V8) runtime on the Rust side. Each strategy **must** declare a manifest via `ops.declareManifest({ feeds: [...] })` during `onInit`, specifying which data feeds it needs (`"book"`, `"weather"`, or both). The Rust session runner reads this manifest and respects it: events for undeclared feeds are short-circuited with empty output, never reaching the strategy's `onTick`.

---

## Architecture: Three-Layer Stack

```
┌─────────────────────────────────────────────────────┐
│  TypeScript Strategy (.ts file)                     │
│  Exports: onInit, onTick, onFill, onShutdown        │
│  Calls: ops.declareManifest(), ops.submitIntent()   │
└───────────────────┬─────────────────────────────────┘
                    │  serde_json serialization
┌───────────────────▼─────────────────────────────────┐
│  strategy-runtime crate (Deno embedding)            │
│  DenoStrategy: transpile TS→JS, run in V8           │
│  Exposes: on_tick(), on_fill(), manifest()           │
│  ops.rs: op_log, op_submit_intent,                  │
│          op_declare_manifest, op_store_return        │
└───────────────────┬─────────────────────────────────┘
                    │
┌───────────────────▼─────────────────────────────────┐
│  weatherman crate (Session runner)                   │
│  session_init.rs: initialize_core()                  │
│  session_core.rs: SessionCore::process_event()       │
│  session.rs: tokio::select! event loop               │
└─────────────────────────────────────────────────────┘
```

---

## Layer 1: TypeScript Strategy

**Location**: `strategies/` directory

### File Layout

```
strategies/
├── lib/
│   ├── types.d.ts          # IDE type declarations (TickContext, Intent, StrategyManifest, etc.)
│   ├── kelly.ts            # Kelly criterion library
│   └── ensemble.ts         # Ensemble analysis helpers
├── registry/
│   ├── london-weather-brackets/
│   │   ├── v1.ts
│   │   └── v2.ts           # Weather + book strategy
│   └── negrisk-sum-arb/
│       └── v1.ts           # Book-only strategy (no weather)
├── test-echo.ts            # Test: both feeds
├── test-book-only.ts       # Test: book feed only
├── test-weather-only.ts    # Test: weather feed only
└── test-no-manifest.ts     # Test: no manifest (should fail)
```

### Strategy API Contract

Every strategy must export `onInit(config)` and `onTick(ctx)`. Optional exports: `onMarketsReady(markets)`, `onFill(ctx, fill)`, `onShutdown()`.

**Manifest declaration** (required in `onInit`):
```typescript
export function onInit(cfg: Record<string, unknown>): StrategyParams {
  ops.declareManifest({ feeds: ["weather", "book"] });
  // ...
  return { ensembleIqrThreshold: 2.0 };
}
```

The `StrategyManifest` type (`types.d.ts:133-135`):
```typescript
interface StrategyManifest {
  feeds: ("book" | "weather")[];
}
```

**Intent submission** — strategies emit intents via `ops.submitIntent()`:
```typescript
ops.submitIntent({
  kind: "order",    // or "mint" or "merge"
  marketId: "...",
  outcome: "YES",
  side: "buy",
  qty: 10,
  limitPx: 65,
  tif: "GTC",
  reason: "...",
});
```

**Tick context differentiation** — strategies distinguish weather ticks from book ticks by checking `ctx.weather`:
```typescript
export function onTick(ctx: TickContext): void {
  if (ctx.weather) {
    handleWeatherRefresh(ctx);  // Weather data present
  } else {
    handleBookUpdate(ctx);      // Pure book update tick
  }
}
```

### `onInit` Return Value

`onInit` can optionally return a `StrategyParams` object to tune Rust-side ensemble parameters:
```typescript
interface StrategyParams {
  ensembleIqrThreshold?: number;
  ensembleBimodalityPenalty?: number;
}
```
These are captured by `DenoStrategy` and used by `SessionCore::fetch_weather_state()` when computing bracket probabilities (`session_core.rs:340-341`).

### Real Strategy Examples

| Strategy | Feeds | Purpose |
|----------|-------|---------|
| `london-weather-brackets/v2.ts` | `["weather", "book"]` | Weather-driven bracket trading with book depth monitoring |
| `negrisk-sum-arb/v1.ts` | `["book"]` | Pure book arbitrage, no weather needed |
| `test-book-only.ts` | `["book"]` | Test fixture for book-only feed |
| `test-weather-only.ts` | `["weather"]` | Test fixture for weather-only feed |

---

## Layer 2: Deno Strategy Runtime

**Crate**: `crates/strategy-runtime/`
**Dependencies**: `deno_core 0.390`, `deno_ast 0.53` (transpiling)

### DenoStrategy Lifecycle (`deno_strategy.rs`)

1. **`DenoStrategy::new(source_path, config)`** (`deno_strategy.rs:107-215`):
   - Reads the `.ts` file from disk
   - Transpiles TS→JS via `deno_ast` with content-addressed caching (`transpile.rs`)
   - Creates a `JsRuntime` with the `strategy_ext` Deno extension (custom ops)
   - Bootstraps `globalThis.ops` (log, submitIntent, declareManifest) and removes `globalThis.Deno` for sandboxing
   - Loads the strategy as an ES module via `StrategyModuleLoader` (which sandboxes imports to `file://` within the strategies directory)
   - Calls `onInit(config)` and captures the return value as `StrategyParams`
   - The `StrategyManifest` is stored in Deno's `OpState` by `op_declare_manifest`

2. **`manifest()`** (`deno_strategy.rs:266-270`):
   - Returns the `StrategyManifest` from `OpState` (set during `onInit` via `ops.declareManifest()`)
   - If strategy never called `declareManifest`, returns `StrategyManifest::default()` (empty feeds)

3. **`on_tick(ctx)`** (`deno_strategy.rs:217-234`):
   - Clears the intent collector in `OpState`
   - Serializes `TickContext` to JSON and calls `globalThis.__strategy.onTick(ctx)`
   - Drains and returns collected `Vec<Intent>`

4. **`on_fill(ctx, fill)`** (`deno_strategy.rs:236-247`):
   - Calls `globalThis.__strategy.onFill(ctx, fill)` if exported

### Ops Bridge (`ops.rs`)

Four ops are registered in the `strategy_ext` Deno extension:

| Op | Purpose |
|----|---------|
| `op_log(level, message)` | Route strategy logs to Rust tracing |
| `op_submit_intent(intent)` | Collect an `Intent` into `Vec<Intent>` in OpState |
| `op_declare_manifest(manifest)` | Store `StrategyManifest` in OpState |
| `op_store_return(json)` | Capture return value from `onInit` |

### Module Loader & Sandboxing (`module_loader.rs`)

- Only `file://` imports are allowed (rejects `https://`, `data:`, etc.)
- Path-traversal protection: resolved imports must be within the strategies base directory (uses `canonicalize()`)
- `.ts` files are transparently transpiled via the `TranspileCache`

### Transpile Cache (`transpile.rs`)

- Content-addressed: SHA-256 of source text → cache key
- Two-tier: in-memory HashMap + disk cache with integrity checksums
- Disk cache uses domain-separated SHA-256 signatures (`weatherman-transpile-v1:`)

### Book Buffer (shared Float64Array)

Strategies that declare `"book"` in their feeds get access to `globalThis.books`, a typed accessor over a shared `Float64Array`. Layout per bracket (stride 7):
```
[bid, ask, bidQty, askQty, timestamp, bidDepthUsd, askDepthUsd]
```
Initialized by `init_book_buffer(num_brackets)` and updated by `update_book_buffer()` on the Rust side.

---

## Layer 3: Session Runner (Rust)

### Initialization (`session_init.rs:181-275`)

`initialize_core()` performs these steps:

1. **Create strategy**: `DenoStrategy::new(strategy_path, strategy_config)` — this runs `onInit`, which sets the manifest
2. **Validate manifest**: `strategy.manifest()` — **fails the session if feeds are empty** (line 210-223):
   ```rust
   if manifest.feeds.is_empty() {
       error!("strategy did not call ops.declareManifest()...");
       return Err(SessionResult { ... });
   }
   ```
3. **Init book buffer** (only if `wants_book`): `strategy.init_book_buffer(market_bindings.len())`
4. **Call onMarketsReady**: passes static market metadata to the strategy
5. **Build SessionCore**: stores `wants_book` and `wants_weather` flags

### SessionCore: Event Processing (`session_core.rs:119-193`)

`process_event()` is the central dispatch. It respects the manifest at two levels:

**WeatherTick events** (line 129-151):
```rust
SessionEvent::WeatherTick => {
    if !self.wants_weather {
        return Ok(TickOutput { intents: vec![], weather_state: None });
    }
    // fetch weather, update book buffer, build TickContext with weather, call on_tick
}
```

**BookUpdate events** (line 153-192):
```rust
SessionEvent::BookUpdate { .. } => {
    if !self.wants_book {
        return Ok(TickOutput { intents: vec![], weather_state: None });
    }
    // update single bracket in book buffer, build TickContext without weather, call on_tick
}
```

Key: `wants_book` and `wants_weather` are derived from the manifest in `SessionCore::new()` (line 91-92):
```rust
let wants_book = strategy.manifest().wants_book();
let wants_weather = strategy.manifest().wants_weather();
```

### Live Session Event Loop (`session.rs:671+`)

The `tokio::select!` loop has three branches:

1. **Time-based exit deadline** — fires once if configured
2. **Weather timer** (`interval.tick()`) — fires on the configured interval (e.g., every 5 minutes). Calls `core.process_event(SessionEvent::WeatherTick, ...)`. Even though this branch fires for all strategies, `SessionCore::process_event` short-circuits to empty output if the strategy didn't declare `"weather"`.
3. **Book update** (`book_event_rx`) — fires on WebSocket book updates. Calls `core.process_event(SessionEvent::BookUpdate { ... }, ...)`. Similarly short-circuits if strategy didn't declare `"book"`.

**Important**: The weather timer always ticks (it also serves as the market-closure polling mechanism), but the actual weather fetch and strategy callback only happen if `wants_weather` is true.

---

## The Manifest Type (Rust side)

**Location**: `crates/types/src/lib.rs:1193-1217`

```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Feed {
    Book,
    Weather,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StrategyManifest {
    #[serde(default)]
    pub feeds: Vec<Feed>,
}

impl StrategyManifest {
    pub fn wants_book(&self) -> bool { self.feeds.contains(&Feed::Book) }
    pub fn wants_weather(&self) -> bool { self.feeds.contains(&Feed::Weather) }
}
```

---

## Answer to the Questions

### 1. How does the relationship between the TS strategy and the Deno runner look?

The TS strategy is a plain ES module file. The Rust `strategy-runtime` crate:
- Transpiles it to JS via `deno_ast`
- Runs it inside an embedded V8 via `deno_core::JsRuntime`
- Exposes `globalThis.ops` (log, submitIntent, declareManifest) as the strategy's API surface
- Calls exported functions (`onInit`, `onTick`, `onFill`, `onShutdown`, `onMarketsReady`) at the appropriate lifecycle points
- Collects `Intent` objects from `ops.submitIntent()` calls and returns them to Rust

The strategy has no direct access to Deno APIs, network, or filesystem. It communicates entirely through the ops bridge and receives data via serialized JSON arguments.

### 2. Can the strategy declare which weather data sources it wants?

**Yes.** Every strategy declares its feeds via `ops.declareManifest({ feeds: [...] })` in `onInit`. The available feed types are `"book"` and `"weather"`. A strategy can declare:
- `["book"]` — book updates only (e.g., `negrisk-sum-arb`)
- `["weather"]` — weather updates only
- `["weather", "book"]` — both (e.g., `london-weather-brackets`)
- Empty feeds → session initialization **fails** with an error

Additionally, the strategy's `onInit` return value (`StrategyParams`) can tune the Rust-side weather processing parameters (`ensembleIqrThreshold`, `ensembleBimodalityPenalty`).

However, the strategy **cannot** select specific weather models or data providers. The models fetched (GFS Seamless, ECMWF IFS025 deterministic, ECMWF IFS025 ensemble) are hardcoded in `SessionCore::fetch_weather_state()` (`session_core.rs:302-338`).

### 3. Will the session runner respect the manifest?

**Yes, fully.** The manifest is enforced at two levels:

1. **`session_init.rs`**: Empty manifest (no `declareManifest()` call) → session fails to start
2. **`session_core.rs`**: `process_event()` checks `wants_book` / `wants_weather` and short-circuits undeclared event types to `TickOutput { intents: vec![], weather_state: None }`

This means:
- A book-only strategy never triggers weather API calls
- A weather-only strategy ignores all WebSocket book updates
- Both branches always fire in the `tokio::select!` loop, but the SessionCore gate prevents unnecessary work

## Code References

- `strategies/lib/types.d.ts` — TypeScript type declarations for the strategy API
- `crates/strategy-runtime/src/deno_strategy.rs:107-215` — DenoStrategy::new lifecycle
- `crates/strategy-runtime/src/deno_strategy.rs:266-270` — manifest() accessor
- `crates/strategy-runtime/src/ops.rs:24-29` — op_declare_manifest
- `crates/types/src/lib.rs:1193-1217` — Feed enum, StrategyManifest, wants_book/wants_weather
- `crates/weatherman/src/session_init.rs:208-232` — manifest validation (empty feeds → error)
- `crates/weatherman/src/session_core.rs:79-105` — SessionCore::new captures wants_book/wants_weather
- `crates/weatherman/src/session_core.rs:119-193` — process_event short-circuits on undeclared feeds
- `crates/weatherman/src/session_core.rs:299-393` — fetch_weather_state (hardcoded models)
- `crates/weatherman/src/session.rs:671-900` — tokio::select! event loop
- `strategies/registry/negrisk-sum-arb/v1.ts:69` — book-only manifest example
- `strategies/registry/london-weather-brackets/v2.ts:179` — weather+book manifest example
