Skip to main content
This guide is about getting oriented, not about a particular trading idea. You will scaffold a project, then step through how market data becomes streams of updates, how hooks plug together, and how one strategy can run in paper, live, or replay mode.

What this walkthrough covers

You will learn how to:
  • Define a strategy as streams of events instead of manual polling loops.
  • Combine streams so derived values (“signals”) refresh whenever any input changes.
  • Run on live venue data real-time feeds, with reconnect/retry so the pipeline survives network hiccups.
  • Backtest on stored history: fetch, persist, replay; same codebase as live, no forked project.
  • Hooks and composition—bundle connections, storage, and logging once; reuse them and keep strategy files small.
The sample uses Binance: live streams come from Binance’s public market feed, and replay pulls historical trades from Binance Vision. Those downloads can be large; the first sync for a wide date range may take noticeable time and bandwidth while data is fetched and stored.

Create the project

Prerequisites

  • Node.js and npm (the scaffold uses npm to install dependencies).
  • Network access when you run live or when replay syncs historical files from remote archives.
1

Scaffold a new folder

Run the create tool with your project directory name (replace my-strategy if you like):
npx @quantform/create my-strategy
The CLI expects that folder name as its argument.
2

Open the project

cd my-strategy
3

Check the layout

You should see a src/ tree like:
src/
  app.ts                         # app entry: wires storage and starts your strategy function
  binance/
    use-binance.ts               # venue hook: bundles trade streams behind one API
    watch-trade.ts               # chooses live vs replay, maps events to price stream
    watch-trade.live.ts          # real-time trades from the public market feed
    watch-trade.replay.ts        # historical trades: sync into storage, replay in order
4

Use the npm scripts

The generated package.json includes scripts such as:
  • npm run start — paper / simulation run (non-live execution mode).
  • npm run live — live run with a real-time market feed.
  • npm run replay — backtest-style run over a date range (from / to are set in the script).

Build the strategy

Start from the entry app.ts. The first step is to define a strategy hook, your main entry, that pulls in other hooks and turns their streams into signals you can act on. In this walkthrough the strategy listens to three Binance trade streams (three symbols), folds them into one combined stream so you always see the latest prices together, and from that joint view computes a spread to surface cross-market inefficiency.
export function triangularArbitrageInefficiency() {
  const { watchTrade } = useBinance();
  const { info } = useLogger('arbitrage');

  return combineLatest([
    watchTrade('btcusdc'),
    watchTrade('ethusdc'),
    watchTrade('ethbtc')
  ]).pipe(
    tap(([btcusdc, ethusdc, ethbtc]) => {
      const spread = ethusdc.div(btcusdc).sub(ethbtc).abs();

      info(`spread: ${spread.toFixed(6)}`);
    })
  );
}
The second half of app.ts registers modules, exposes the entry handle, and binds your strategy to the runtime:
export default app().use(sqlite()).start(triangularArbitrageInefficiency);
  • app() creates the base runtime and its default dependency graph (logging, execution context, in-memory storage factory, and other services hooks expect).
  • .use(sqlite()) registers the SQLite-backed module so persistence-backed parts of the strategy resolve storage through the same graph.
  • .start(triangularArbitrageInefficiency) binds the strategy function to the runner: the runtime invokes it and drives the main stream it returns—your strategy entry tied to the running process.

Strategy execution: live & replay

LiveCommand (project root):
npm run live
Expect — The live script drives the feed. For BTCUSDC, ETHUSDC, and ETHBTC, each update recomputes the spread and prints one log line (values follow the live market). Replay (backtest)Command (project root). Change from / to in package.json if you need another window:
npm run replay
ExpectFirst run for a range: fetch historical trades from external sources, persist them, then replay in time order through the same strategy. Output matches live: spread lines from stored history. First sync can take longer while downloads finish.

Technical walkthrough

The sections below step through the strategy implementation: live feed, replay storage, switching live versus replay, venue hooks, and how they connect back to app.ts.

1. Real-time trades: watchTradeLive

watch-trade.live.ts uses useSocket to subscribe to Binance spot composite stream trades. Incoming messages are schema validated, retried on failure, and mapped to a normalized { timestamp, payload: { price, size } } shape.
export function watchTradeLive(symbol: string) {
  const { watch } = useSocket(
    `wss://stream.binance.com/stream?streams=${symbol.toLowerCase()}@trade`
  );

  return watch().pipe(
    retry(),
    map(({ timestamp, payload }) => {
      const { data } = schema.parse(payload);

      return { timestamp, payload: { price: d(data.p), size: d(data.q) } };
    })
  );
}
d(...) builds decimal values suitable for precise math in the strategy layer.

2. Historical trades: watchTradeReplay

watch-trade.replay.ts uses useReplayStorage to manage historical data. The sync callback is invoked for the replay time range: it walks each day, downloads the Binance Vision spot daily trades ZIP for that symbol, parses CSV rows, and **storage.save**s normalized events. Then watch() emits events in timestamp order through the same map as live for a consistent payload shape.
export function watchTradeReplay(symbol: string) {
  const { watch } = useReplayStorage(uri(`binance://trade`, { symbol }), {
    sync: async (query, storage) => {
      const { min, max } = query.where.timestamp;
      // ... iterate days, fetch ZIP, parse rows, await storage.save([...])
    }
  });

  return watch().pipe(
    map(({ timestamp, payload }) => {
      const { 1: price, 2: quantity } = schema.parse(payload);

      return { timestamp, payload: { price: d(price), size: d(quantity) } };
    })
  );
}

3. Execution mode: useReplay

watch-trade.ts delegates to useReplay(watchTradeReplay, watchTradeLive). That helper reads useExecutionMode(): in replay runs it uses the first function; otherwise it uses the second (live/paper paths both use the “real-time” implementation).
export function watchTrade(symbol: string) {
  return useReplay(
    watchTradeReplay,
    watchTradeLive
  )(symbol).pipe(map(it => it.payload.price));
}
The qf paper, qf live, and qf replay commands each inject the appropriate execution mode and replay options when they call run on your default export.

4. Venue composition: useBinance

use-binance.ts exposes a single object so strategy code does not import every low-level file:
export function useBinance() {
  return {
    watchTrade
  };
}
Keeping venue-specific wiring behind useBinance() lets you reuse the same Binance implementation in other strategies without copying imports or stream setup.

Next steps

Overview

Return to the product overview and execution model summary.

Composition

Go deeper on hooks, tokens, and dependency wiring.