Quantitative research

Building a quantitative research pipeline: data, features and event labels

How our research pipeline turns raw crypto market observations into testable model inputs through consistent bars, constrained feature grids and triple-barrier event labels.

If you research systematic strategies on crypto market data, you've probably reached the point where notebooks stop scaling. Each experiment reloads history, picks a bar size by habit, hand-tunes a few indicators and splits the data wherever it's convenient. The results can look promising until overlapping labels quietly leak information across the train/test boundary.

At 3Jane Technologies we run proprietary crypto trading, mainly market making, arbitrage and liquidity rebates on Binance. Our research has to give that work inputs we can trust.

We built a research pipeline that turns raw market observations into testable model inputs through fixed, reproducible stages. This article covers data, bars, features and event labels, and shows where their outputs go next in our research workflow.

Why we treat research as a pipeline

Most researchers start with one notebook per idea. That's fast for a single question, but every notebook makes its own silent choices about how bars are built, which parameter values get tried and where the train/test boundary falls. When two notebooks disagree, you can't tell whether the market caused the difference or the plumbing did.

Our pipeline makes those choices explicit and shared. Each stage has a fixed contract with the next one, so a researcher can change one decision, such as the sampling clock, and keep everything else constant. "Does this signal work?" becomes a sharper question: does it still work when we sample the market differently?

The stack is built on standard tools, each with a specific job:

  • NumPy and pandas provide the array and frame interface researchers already know.
  • scikit-learn provides estimator conventions and cloneable models.
  • Native C++ handles a few intensive loops, and Numba accelerates other array code.
  • joblib and batched jobs parallelize independent calculations.
  • Caches keep expensive data and intermediate results from being recomputed between experiments.

The pipeline at a glance

The stages run in this order, from raw observations to sized positions:

market history -> consistency checks -> bars -> feature grid -> event labels (t1)
    -> time-aware CV splits -> model fitting + feature importance
    -> simulation + position sizing        (resampling where used)

This article covers the first half in depth: data, bars, features, labels and feature assessment. Validation, resampling, simulation and sizing have their own articles, linked below.

Every arrow is a contract. Bars carry consistent OHLCV fields. Each feature keeps the configuration that produced it. Each label carries the end time of its event, which later stages need to handle overlap.

Loading market history and checking it

Everything starts with minute-level market history from our trading platform's data store. The loader first looks for a local HDF copy. If none exists, it fetches the remote OHLCV index and downloads the shards that index lists. It then concatenates and sorts them by timestamp and caches the combined frame, so the expensive fetch isn't repeated.

Next, it filters the requested interval with an inclusive start and an exclusive end, so adjacent intervals never share a boundary observation. It also builds total columns from the buy and sell components: total base volume, total quote volume and total ticks. Those totals later become alternative sampling clocks.

Consistency checks run before any derived features or labels exist, because a bad row here would spread into every feature and label downstream. The checks confirm that:

  • numeric fields are numeric and finite, with no NaN or infinity
  • no timestamp appears twice
  • no column is duplicated

Choosing the sampling clock

Time bars are the default almost everywhere, but they're only one option. Markets don't trade at a constant pace, so a fixed clock gives quiet bars and frantic bars equal weight. Our pipeline treats the sampling clock as an experiment input and keeps everything else about bar construction fixed.

Aggregation rules

Whatever closes a bar, the observations inside it are aggregated the same way:

  • open: the first open in the group
  • high: the maximum high
  • low: the minimum low
  • close: the last close
  • base volume, quote volume and ticks: the sum

Because these rules are fixed, bar families differ only in where a bar closes. They never differ in how they summarize what happened inside it.

Bar families

The bar module offers several families:

  • time bars on a fixed clock interval
  • cumulative-threshold bars, where a chosen column accumulates until it reaches a threshold, so tick count, base volume or quote volume can each act as the clock
  • price-movement bars that close after a percentage move
  • adaptive variants with rolling, activity-sensitive thresholds

The adaptive thresholds' parameters are research choices, not constants built into the code. Swapping the clock lets you compare equal-time observations with observations tied to activity or price movement, which is how you test whether a conclusion depends on how you sampled the market.

Worked example: a volume bar

Here's an illustrative synthetic series of four observations with volumes [2, 3, 1, 4] and a volume threshold of 5. The steps run in order:

  1. Observation 1 adds 2. The accumulator holds 2, and no bar closes.
  2. Observation 2 adds 3. The accumulator reaches 5, the bar closes, and the accumulator resets to zero.
  3. Observation 3 adds 1. The accumulator holds 1.
  4. Observation 4 adds 4. The accumulator reaches 5, and the second bar closes.

The closing flags are [0, 1, 0, 1], and each completed bar contains exactly 5 units of volume. The aggregation rules then take each bar's open from its first observation and its close from its last, and they sum its volumes and ticks.

To carry the example forward, give the observations explicitly synthetic prices: observation 1 opens at 99, observation 2 closes at 100, observation 3 opens at 100 and observation 4 closes at 102. The first completed bar therefore opens at 99 and closes at 100, and the second opens at 100 and closes at 102.

Turning bars into a feature grid

Once you have bars, it's tempting to hand-pick a few indicators with familiar defaults. We prefer to declare a grid, let the system calculate, name and store every valid variant, and leave the evaluation stage to decide which variants matter.

Feature families

The feature modules cover a wide range:

  • momentum, local highs and lows, and drawdown or runup state
  • entropy, fractal dimension and descriptive statistics
  • time-of-day and calendar features, plus statistical tests
  • technical indicators from Tulip Indicators
  • Alpha101 functions

One grid specification

Take ADOSC, the accumulation/distribution oscillator from Tulip Indicators. Its configuration maps our high, low, close and volume columns, plus any transformations, onto the indicator's inputs. It also declares a grid over short_period and long_period. The constraint short_period < long_period removes nonsensical combinations before any compute is spent on them.

As a synthetic illustration, take short periods {3, 5, 10} and long periods {5, 10, 20}. That grid has nine combinations, and the constraint keeps six: (3, 5), (3, 10), (3, 20), (5, 10), (5, 20) and (10, 20). Real grids are experiment inputs, not fixed values.

The job runner evaluates the constraint as an expression with restricted builtins. We treat it as a trusted research configuration mechanism: our own researchers write these expressions, and it isn't a sandbox for untrusted input.

Running the grid

Each configuration entry has six separate parts:

  • the function to call
  • the input mapping from bar columns to function inputs
  • the parameter grid
  • transformations
  • a condition (the constraint) and an alias for naming

A job manager runs entries either in a single process or as batched parallel jobs. Parallel batches write to temporary output folders and serialize their job metadata. The researcher declares what they want, and the system does the repetitive calculating, naming and storing.

Worked example continued: a bar feature

Back to the two synthetic volume bars. Separately from the ADOSC grid, take a simple illustrative feature: each bar's close / open - 1. The first bar gives 100 / 99 - 1, approximately 1.01%, and the second gives 102 / 100 - 1, or 2%.

A real grid computes many such columns for every bar, each named after the configuration that produced it. The next stage attaches a target to those rows.

Event labels and their information intervals

A feature matrix needs a target. We label events with the triple-barrier method from Marcos Lopez de Prado's Advances in Financial Machine Learning, a book whose ideas run through much of this pipeline. Our contribution is the integration and a native implementation for speed. The method itself isn't ours.

Two label contracts

The pipeline exposes two related labelers, and they return different things:

  • The directional labeler sets profit and loss boundaries scaled by daily volatility and returns an outcome label for each event.
  • The timestamp labeler uses profit, stop and vertical time boundaries and returns each event's end time, t1, which is when the first barrier is touched.

Directional labels are the targets classifiers train on. End times feed every stage that needs to know how long a label's information lasts. It's easy to mix the two up, so check which contract a stage expects before you wire it in.

Native acceleration

The timestamp labeler takes an event mask, scalar or per-event profit and stop multipliers, a unit width and a vertical boundary. Python maps timestamps to integer positions and passes contiguous typed arrays through ctypes to a C++ library. That library maintains active threshold structures and offers single-threaded and threaded entry points.

Our tests cover the directional labeler: its outputs, missing boundary periods and disabled profit or stop barriers.

Worked example continued: an event label

Continuing the illustrative series, suppose an event starts at the first bar's close of 100. For this example, assume the barriers are evaluated at subsequent bar closes, with an upper barrier at 102, a lower barrier at 98 and a vertical deadline later than the second bar's close.

The next close is 102, which reaches the upper barrier first. That close's timestamp is t1, and the illustrative directional outcome is positive.

In general, if price touches a horizontal barrier first, t1 is the time of that touch. Otherwise t1 is the vertical boundary. Either way, the label covers the span from the event's start to t1. That span is the label's information interval.

Why t1 matters downstream

Spans overlap whenever an event starts before an earlier one ends. Overlapping labels share information, so counting them as independent observations overstates how much evidence you have.

The pipeline carries t1 into time-aware cross-validation. There it supplies the intervals used to filter out test observations that overlap training data. Embargo is a separate, count-based operation: its length is the larger of a fixed count and a percentage of the adjacent test segment.

t1 also feeds sequential bootstrap, where the same intervals drive uniqueness weights. Every stage after labeling depends on these intervals.

Assessing features and comparing models

A feature grid produces far more features than any model should use. This stage tests which features carry signal under time-aware validation and compares model families on the same splits.

Permutation importance

Mean decrease accuracy works on the supplied cross-validation splits:

  1. Fit the model on each training split.
  2. Score the held-out data to get a baseline.
  3. Shuffle one feature, or a whole cluster of correlated features.
  4. Record the baseline score minus the shuffled score.

Shuffling whole clusters matters because correlated features can cover for each other. Permuting the cluster removes the shared signal, so a correlated sibling can't hide the loss. Mean decrease impurity and mutual information add bootstrap-based estimates, with optional clustering of correlated features.

Combining importance estimates

Different importance estimators score on different scales. Ensemble feature importance converts each estimator's output to reciprocal ranks, averages them, and reports the mean, the dispersion and a final rank. High dispersion tells you the estimators disagree, which is worth knowing before you trust a feature.

Model families

Classifier wrappers support several research model families:

  • LightGBM
  • XGBoost
  • random forest
  • nearest-neighbor
  • voting ensembles

Decision thresholds can be fixed probabilities or quantiles. A quantile threshold is estimated from training predictions during fitting and then reused on test probabilities, so the cutoff never peeks at the test distribution. Calibration curves from the scoring stage show whether predicted probabilities match outcomes, which matters once those probabilities drive position size.

Where validation and simulation connect

Every score above comes from time-aware splits. Walk-forward splits train on earlier observations, skip a gap and test on a later interval. Combinatorial splits enumerate combinations of test folds. They then filter out test observations that overlap the preceding training blocks and embargo the start of training blocks that follow test blocks. The dedicated cross-validation article explains both procedures.

Both split types build on the cross-validation ideas in Advances in Financial Machine Learning. The test-filtering convention described here is our implementation's own, so don't read it as the book's exact procedure.

Resampling uses the same information intervals. Sequential bootstrap draws rows according to their uniqueness. Our bagging estimator adapts scikit-learn's bagging internals to sample rows this way whenever event spans are supplied. Separate wrappers around the recombinator library provide IID, moving-block, circular-block and stationary bootstrap.

From there, predictions go into a simulation built on Backtrader, with modeled costs, sizing objects and pre-trade risk callbacks, and then into Kelly-based sizing. Backtesting and position sizing covers those stages.

Evidence: the autocorrelation paper

The clearest public output of the data, bar and statistical-analysis stages is our paper, Autocorrelation of returns in major cryptocurrency markets, by Eugene Tartakovsky, Ksenia Plesovskikh, Anastasiia Sarmakeeva and Alexander Bibik. The runnable notebook, the datasets and the calculation and plotting code are in our articles repository.

What the notebook does

The notebook offers four datasets: Bitfinex BTC/USD, ETH/BTC and ETH/USD, and BitMEX XBT/USD. Bitfinex BTC/USD is selected by default. To reproduce another market, you rerun the notebook with that dataset selected. The procedure has four steps:

  1. Aggregate minute observations into 5-minute, hourly, daily and weekly bars, then compute returns as close / open - 1. These are open-to-close returns within each bar.
  2. Calculate autocorrelation coefficients through 31 lags with a 0.05 confidence parameter.
  3. Run Ljung-Box tests through 31 lags with a 0.05 threshold.
  4. Estimate first-lag autocorrelation in rolling one-year windows, using numpy_ext.rolling_apply with 24 parallel jobs.

Each window's length is a bar count, so "one year" means a different number of bars on each timeframe. The same calculation procedure repeats across each selected bar frequency, while the dataset is a separate selection.

What it found and what it doesn't show

Statistically significant autocorrelation is persistently present on the 5-minute and hourly timeframes in all four markets, while the tests disagree on daily and weekly bars. The paper is the first in a planned series on market efficiency, with work on long memory and return distributions to follow. The paper's page has the full results.

The paper is a statistical study of return autocorrelation, and its claims stop at market statistics. It exercises only the data, bar, statistics and tooling stages. The predictive models, event labels and trading simulation described above are separate capabilities of the same stack.

Open tools from the stack

Two pieces of the stack's tooling are public. Each one solved a problem we kept running into during research.

numpy_ext

NumPy Extensions adds operations we kept needing: missing-value handling, rolling and expanding windows, windowed function application and parameter-range helpers. Its rolling_apply takes one or more equal-length arrays and applies your function over aligned windows. It runs directly with one worker or dispatches jobs through joblib, and it can prepend NaNs so the output length matches the input.

The paper's rolling autocorrelation uses rolling_apply, and our feature configurations use its exponential-step range helper to build parameter grids. We haven't published speed comparisons for the library.

Indicator library lineage

Our feature grids call Tulip Indicators through the tulipindicators Python package, and the credit for those indicator implementations goes to Tulip Indicators and its contributors. Separately, our team maintains tindicators, a fork of Tulip Indicators with over 160 indicators and C, Python and C# interfaces.

The fork declares its indicators in a single YAML index and generates its public header and runtime lookup table from that index. That keeps a large API consistent across languages. Its Python binding loads the native library through ctypes and maps native return codes to Python exceptions.

Next step

If you're building a similar pipeline, start where the paper starts. Pick one market in the notebook and reproduce its 5-minute, hourly, daily and weekly results, so your baseline matches the paper's.

The notebook covers clock-time frequencies, so an activity-clock comparison is your next experiment. Use the pipeline described above to build volume or tick bars with the same aggregation rules, then check whether your statistic changes. Before you trust any score built on overlapping labels, read the cross-validation article, then browse the rest of our research.