# 04. Your first Rust TBBO strategy

A guided walkthrough that ends with [`imbalance.rs`](https://quantchartsllc.com/docs/templates/templates.md#imbalance-rs): a TBBO strategy that goes long when bid pressure dominates and CVD is rising, short when the inverse. No EMAs, no SL/TP, just tick-level microstructure.

Rust strategies run on the raw TBBO tick stream. They are the right path when your edge depends on per-tick `bid_size`/`ask_size`, sub-second timing, or large-day high-throughput sweeps. This walkthrough mirrors the built-in `imbalance.rs` template; by the end you will know how to declare a [`#[strategy]`](https://quantchartsllc.com/docs/rust/rust-strategy.md#strategy), hoist work into `prepare()`, read tick columns in `calculate()`, and emit a [`SignalOutput`](https://quantchartsllc.com/docs/rust/rust-strategy.md#signaloutput).

<a id="step-1-why-rust-and-tbbo"></a>
## Step 1. Why Rust and TBBO

Pick the right path before writing code. Rust = TBBO ticks. Python = OHLC bars.

Quant Charts splits strategy paths by data shape:

- Python on OHLC: aggregated bars, vectorized math, fast iteration. Use this for everything that fits a bar timeframe.
- Rust on TBBO: per-tick reads, native speed, no GIL. Use this when your idea reads bid_size/ask_size, fires on individual ticks, or needs to run dozens of times per parameter combo without breaking the sweep budget.

TBBO files in this app carry timestamp, bid, ask, mid, spread, bid_size, ask_size as the canonical 7-field stream. Optional volume/delta/vwap may also be present.

### Notes

- `#[strategy]` is the macro shape for tick strategies. There is also `#[ohlc_strategy]` for Rust strategies on OHLC bars, but for tick-microstructure work you want `#[strategy]`.
- Rust strategies live under `your workspace/strategies/` next to Python ones. The file extension drives the loader.

<a id="step-2-crate-prelude-and-macro-shape"></a>
## Step 2. Crate prelude and macro shape

The prelude pulls in everything you need: the trait, helpers, type definitions.

### Example

```python
use qc_strategy_api::prelude::*;

#[strategy(
    name = "Imbalance",
    description = "Bid/ask imbalance + CVD-slope entry; imbalance-flip exit.",
    data_mode = "tick"
)]
#[tag(name = "long_entry",  label = "Long Entry",  color = "#26A69A")]
#[tag(name = "short_entry", label = "Short Entry", color = "#EF5350")]
#[derive(Default)]
pub struct Imbalance {
    // params go here
}
```

### Notes

- `use qc_strategy_api::prelude::*;` brings `Strategy`, [`TickData`](https://quantchartsllc.com/docs/rust/rust-data.md#tickdata), [`DayPrep`](https://quantchartsllc.com/docs/rust/rust-data.md#dayprep), `SignalOutput`, helpers ([`imbalance`](https://quantchartsllc.com/docs/python/py-signals.md#imbalance), [`cvd`](https://quantchartsllc.com/docs/python/py-signals.md#cvd), [`rolling_mean`](https://quantchartsllc.com/docs/rust/rust-helpers.md#rolling_mean)), and the [`Timeframe`](https://quantchartsllc.com/docs/python/py-logging.md#timeframe) enum into scope.
- `#[tag(...)]` declarations are equivalent to Python [`define_tag()`](https://quantchartsllc.com/docs/python/py-styling.md#define_tag) calls. They are metadata; the actual tag boolean comes from the `SignalOutput.with_tag()` calls in `calculate()`.
- `#[derive(Default)]` is required because the runner constructs the strategy with `Default::default()` and then sets parameter fields per combo.

<a id="step-3-parameters"></a>
## Step 3. Parameters

Use [`#[param]`](https://quantchartsllc.com/docs/rust/rust-strategy.md#param) on each field. Same shape as Python `input.*`.

### Example

```python
pub struct Imbalance {
    #[param(default = 200, min = 1, max = 200000, label = "Smoothing (ticks)")]
    pub smooth: usize,

    #[param(default = 0.10, min = 0.01, max = 0.49, step = 0.01, label = "Entry Threshold")]
    pub entry_threshold: f64,

    #[param(default = 0.05, min = 0.0, max = 0.49, step = 0.01, label = "Exit Threshold")]
    pub exit_threshold: f64,

    #[param(default = 1000, min = 2, max = 1000000, label = "CVD Lookback")]
    pub cvd_lookback: usize,
}
```

### Notes

- Field types: `usize` for integer params, `f64` for floats, `bool` for checkboxes.
- `step = 0.01` controls the increment in the panel arrows and the analyzer step grid.

<a id="step-4-the-strategy-trait"></a>
## Step 4. The Strategy trait

`prepare()` runs once per day per worker (good place for param-independent compute). `calculate()` runs once per parameter combo.

### Example

```python
impl Strategy for Imbalance {
    fn prepare(data: &TickData) -> DayPrep {
        let mut prep = DayPrep::empty();
        // raw imbalance is param-independent: hoist it.
        prep.insert_f64("imb_raw", imbalance(&data.bid_size, &data.ask_size));
        // CVD = cumulative (bid_size - ask_size). Param-independent too.
        let signed: Vec<f64> = data.bid_size.iter()
            .zip(data.ask_size.iter())
            .map(|(b, a)| b - a)
            .collect();
        prep.insert_f64("cvd", cvd(&signed));
        prep
    }

    fn calculate(&self, data: &TickData, prep: &DayPrep) -> SignalOutput {
        // ... walk ticks, build entry/exit/tag arrays ...
        SignalOutput::new(vec![], vec![], vec![], vec![])
    }
}
```

### Notes

- Anything you put in `DayPrep` is shared across every parameter combo for that day. On a 700-combo sweep, hoisting a 6M-tick array saves you 700x the work.
- Read it back in `calculate()` with `prep.f64("key")` (returns `Option<&[f64]>`).

<a id="step-5-building-the-signal-arrays"></a>
## Step 5. Building the signal arrays

Smooth the raw imbalance with the swept window, walk ticks, set entries/exits.

### Example

```python
    fn calculate(&self, data: &TickData, prep: &DayPrep) -> SignalOutput {
        let n = data.len();
        if n == 0 {
            return SignalOutput::new(vec![], vec![], vec![], vec![]);
        }

        let imb_raw = prep.f64("imb_raw").map(|s| s.to_vec())
            .unwrap_or_else(|| imbalance(&data.bid_size, &data.ask_size));
        let imb = if self.smooth > 1 { rolling_mean(&imb_raw, self.smooth) } else { imb_raw };
        let cvd_arr = prep.f64("cvd").unwrap_or(&[]);

        let entry_up = 0.5 + self.entry_threshold;
        let entry_dn = 0.5 - self.entry_threshold;
        let exit_up  = 0.5 + self.exit_threshold;
        let exit_dn  = 0.5 - self.exit_threshold;
        let lk = self.cvd_lookback.min(n.saturating_sub(1));

        let mut entry_long  = vec![false; n];
        let mut exit_long   = vec![false; n];
        let mut entry_short = vec![false; n];
        let mut exit_short  = vec![false; n];
        let mut long_tag    = vec![false; n];
        let mut short_tag   = vec![false; n];

        for i in 0..n {
            let v = imb[i];
            if !v.is_finite() { continue; }

            let rising = i >= lk && cvd_arr.get(i).zip(cvd_arr.get(i - lk))
                .map(|(now, then)| now > then).unwrap_or(false);
            let falling = i >= lk && cvd_arr.get(i).zip(cvd_arr.get(i - lk))
                .map(|(now, then)| now < then).unwrap_or(false);

            if v > entry_up && rising  { entry_long[i]  = true; long_tag[i]  = true; }
            if v < entry_dn && falling { entry_short[i] = true; short_tag[i] = true; }
            if v < exit_dn { exit_long[i]  = true; }
            if v > exit_up { exit_short[i] = true; }
        }

        SignalOutput::new(entry_long, exit_long, entry_short, exit_short)
            .with_tag("long_entry", long_tag)
            .with_tag("short_entry", short_tag)
    }
```

### Notes

- `SignalOutput::new(entry_long, exit_long, entry_short, exit_short)` is the four-array constructor. The order matters.
- Builder methods chain: `.with_tag(...)` for each tag, `.with_sl_long(arr)` / `.with_tp_long(arr)` for stops, `.with_entry_only_sltp()` to bundle stops at entry only.
- Returning `SignalOutput::new(vec![], vec![], vec![], vec![])` for empty days is the canonical guard - the engine treats zero-length arrays as "no signals".

<a id="step-6-run-it"></a>
## Step 6. Run it

Save, recompile, attach.

2. The first save takes a few seconds (full crate build). Subsequent edits are incremental.
3. From the strategy panel, pick "Imbalance". Make sure the chart is loaded with a TBBO parquet (e.g. `TBBO_Converted.parquet`) - this strategy will refuse to run on plain OHLC.
4. Open the analyzer to sweep `smooth`, `entry_threshold`, and `cvd_lookback`. With `prepare()` doing all the param-independent work, even 700-combo sweeps run in seconds per day.

### Notes

- For a similar template that ships with the app, see `templates/workspace/strategies/built-in/rust/imbalance.rs`.
- For SL/TP, add `.with_sl_long(sl_arr).with_tp_long(tp_arr)` and consider `.with_entry_only_sltp()` so brackets only update on the entry tick (cuts memory on large sweeps).
