Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/backtest-kit/uzse-backtest-app/llms.txt

Use this file to discover all available pages before exploring further.

build-candles.ts implements a deterministic 5-step algorithm for generating continuous OHLCV candle series from raw trade data stored in the trade-results collection. Given a display ticker and its corresponding ISIN, the script produces gap-free candle series for all eleven supported timeframes in a single pass and writes them to the candle-items collection. Because the algorithm processes one calendar day at a time and relies on a unique compound index for duplicate rejection, it is both memory-efficient and safe to re-run at any time without corrupting existing data.

Usage

npx tsx scripts/build-candles.ts [symbol] [isin]
# Example:
npx tsx scripts/build-candles.ts HMKB UZ7011340005
symbol is the short display ticker written to candle-items; isin is the ISIN code used to query trade-results. Both arguments default to HMKB / UZ7011340005 if omitted.

Algorithm Overview

1
Step 1 — Aggregate Real Minutes
2
Trades for the current calendar day are loaded from MongoDB and bucketed into 1-minute intervals using the floorToMin helper, which floors any millisecond timestamp to the nearest multiple of minutes × 60,000 ms:
3
function floorToMin(tsMs: number, minutes: number): number {
  const stepMs = minutes * MIN_MS;
  return Math.floor(tsMs / stepMs) * stepMs;
}
4
Within each 1-minute bucket the aggregation rules are:
5
FieldValueopenPrice of the first trade in the buckethighMaximum price across all tradeslowMinimum price across all tradesclosePrice of the last trade in the bucketvolumeSum of quantity across all trades
6
Results are stored in a Map<number, OHLCV> keyed by the floored timestamp.
7
Step 2 — Fill Intraday Gaps
8
The algorithm iterates over every minute of the calendar day — from 00:00:00 UTC to 23:59:00 UTC (1440 steps of MIN_MS = 60,000 ms). For each minute:
9
  • If a real trade bucket exists for that timestamp, it is used as-is and lastClose is updated to its close price.
  • If no trades occurred in that minute, a synthetic candle is created: open = high = low = close = lastClose, volume = 0.
  • 10
    This guarantees that after Step 2 every minute of the trading day has exactly one entry in the in-memory series, regardless of how sparse the actual trade activity was.
    11
    lastClose is initialised to the tradePrice of the very first trade record in the entire date range, and is then carried forward across both intraday gaps and non-trading days.
    12
    Step 3 — Fill Non-Trading Days
    13
    When the outer day loop encounters a day that has zero trades (a weekend, public holiday, or exchange closure), the trade query returns an empty array. The algorithm still walks all 1440 minutes of that day and fills them with open = high = low = close = lastClose, volume = 0, using the closing price carried over from the last day that had real trades.
    14
    This ensures the final candle series is truly continuous from the first to the last trading day in the dataset — there are no gaps at the day boundary that would confuse Pine Script indicators or backtest-kit strategy runners.
    15
    Step 4 — Aggregate Higher Timeframes
    16
    All eleven timeframes are built simultaneously from the gap-filled 1-minute series produced in Steps 2 and 3. For each timeframe T the algorithm groups 1-minute candles by flooring their timestamp to the nearest T-minute boundary using the same floorToMin function:
    17
    const tfTs = floorToMin(ts, minutes); // e.g. minutes = 60 for "1h"
    
    18
    Aggregation per higher-timeframe period follows the same OHLCV rules as Step 1:
    19
    FieldValueopenopen of the first 1m candle in the periodhighMaximum high across all 1m candleslowMinimum low across all 1m candlescloseclose of the last 1m candle in the periodvolumeSum of volume across all 1m candles
    20
    The eleven accumulators (Map<number, OHLCV>) are updated inside the same loop that walks the 1-minute series, so the entire rollup for all timeframes is completed in a single O(1440) pass per day.
    21
    Supported timeframes and their minute counts:
    22
    IntervalMinutes1m13m35m515m1530m301h602h1204h2406h3608h4801d1440
    23
    Step 5 — Idempotent Batch Insert
    24
    All candle documents produced for the current day are flattened into a single array and written to MongoDB in one call:
    25
    await CandleModel.insertMany(docs, { ordered: false });
    
    26
    The ordered: false option instructs MongoDB to continue inserting remaining documents even when it encounters a duplicate key. The unique compound index { symbol, interval, timestamp } rejects any document that already exists in the collection with error code 11000. The insertBatch helper catches this error and extracts insertedCount from the error result, returning { inserted, skipped } counts rather than propagating the exception.
    27
    async function insertBatch(docs: object[]): Promise<{ inserted: number; skipped: number }> {
      if (docs.length === 0) return { inserted: 0, skipped: 0 };
      try {
        const res = await CandleModel.insertMany(docs, { ordered: false });
        return { inserted: res.length, skipped: 0 };
      } catch (e: any) {
        const inserted = e.result?.insertedCount ?? 0;
        return { inserted, skipped: docs.length - inserted };
      }
    }
    
    28
    Re-running build-candles.ts on a symbol that has already been fully processed will result in inserted: 0, skipped: <total> — all documents are rejected as duplicates and the collection is left unchanged.

    Day-by-Day Processing

    The outer loop increments one calendar day at a time from the UTC date of the earliest trade to the UTC date of the latest trade. Only the trades belonging to the current day are loaded from MongoDB in each iteration, capping peak memory usage to the volume of a single day’s trades regardless of the total dataset size.
    Range: 2026-01-02 — 2026-04-17 (106 days)
    [1/106] 2026-01-02  trades:847  real_min:312  candles:2402  inserted:2402  skipped:0
    [2/106] 2026-01-03  trades:0    real_min:0    candles:2402  inserted:2402  skipped:0   ← gap day
    ...
    
    The progress line is written with \r (carriage return) so it overwrites itself in place. The final summary line reports total inserted and skipped (duplicates) counts across all days.

    Timestamp Storage

    All timestamp values are stored as milliseconds since the Unix epoch (Date.getTime() convention). The 1d candle for a given UTC date has timestamp equal to the midnight boundary of that date in milliseconds:
    const dayStartMs = startDay.getTime() + d * DAY_MS;
    // e.g. 2026-04-17T00:00:00.000Z → 1744848000000
    
    To reconstruct the date from a stored timestamp:
    new Date(1744848000000).toISOString(); // "2026-04-17T00:00:00.000Z"
    

    Build docs developers (and LLMs) love