Backtest Your Own Python Signal on Trigr

Upload a signal computed in Python or a notebook, read it in a formula with upload(), and backtest it point in time on Trigr. Timestamps, lags and limits.

Trigr Research5 min read
On this page
  1. When does uploading a signal make sense?
  2. How does the upload workflow work?
  3. What timestamp should each value carry?
  4. How does Trigr treat gaps and missing values?
  5. How do you use an uploaded series in a formula?
  6. Why are uploaded series research only?
  7. What are the limits?
  8. A sensible research loop

TL;DR: If your signal lives in Python (a model score, an alternative dataset, a factor you built in a notebook), you can now test it on Trigr without rebuilding it as indicators. Upload it as timestamped values, read it in an Expression formula with upload("name"), and backtest it point in time on Trigr's price data and fill rules. The catch is deliberate: uploaded series are research only. Trigr cannot verify when your values were knowable, so a strategy that reads one can be backtested but never published or deployed.

When does uploading a signal make sense?

Trigr's formulas cover most price-based ideas, and api("source_id") covers the point-in-time data sources in its catalog. An upload is for everything else:

  • A model's output: a gradient-boosted classifier's probability, a regression forecast, a meta-label score.
  • Your own data: an on-chain metric you pull from a node, a sentiment index you build, a cross-sectional rank across a universe Trigr does not hold.
  • A factor from a notebook that would take dozens of nodes to rebuild, if it can be rebuilt at all.

What you gain is an honest execution layer. Your signal is joined to Trigr's candles at each bar close, entries fill at the next bar's open, fees apply to every trade, and you can add slippage and real historical funding. That is often where a notebook result that ignored costs stops looking good.

How does the upload workflow work?

There are three steps:

  1. Compute the signal in your own code as (timestamp, value) pairs.
  2. Upload it with an API key (paid plans) to POST /api/v1/signal-series, or on any plan from an AI assistant with the trigr_upload_signal_series MCP tool, where each upload goes through the usual MCP approval step. Uploading costs no credits.
  3. Reference it in any Expression formula as upload("name"), and backtest. The run is labelled exploratory research.

Here is a complete Python upload. Requests are capped at 50,000 points and at about 2 MB of JSON, so it rounds values and sends 25,000 points per request:

import pandas as pd, requests

API_KEY = "..."                      # from your Trigr API settings
BASE = "https://trigr.xyz"

df = pd.read_csv("my_signal.csv")   # columns: time (UTC), signal
time = pd.to_datetime(df["time"], utc=True)
ts = ((time - pd.Timestamp(0, tz="UTC")) // pd.Timedelta(milliseconds=1)).tolist()
values = df["signal"].astype(float).round(8).tolist()

for i in range(0, len(ts), 25_000):
    r = requests.post(
        f"{BASE}/api/v1/signal-series",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "name": "my_signal",
            "mode": "replace" if i == 0 else "append",
            "ts": ts[i:i + 25_000],
            "values": values[i:i + 25_000],
            "lagSeconds": 0,
        },
    )
    r.raise_for_status()

The first chunk uses replace, which creates or overwrites the series; later chunks append points strictly after the last stored timestamp. API keys require a paid plan and are described in the API reference; on the Free plan, use the MCP tool instead. The pandas Timestamp documentation covers the time conversion if your data starts in another format.

What timestamp should each value carry?

This is the single most important decision, because it is the one Trigr cannot check for you. Stamp each value with the moment it became knowable, as integer UTC epoch milliseconds:

  • A value computed from the 2026-03-01 daily close is knowable at 2026-03-02T00:00:00Z, so stamp it there, not at 2026-03-01T00:00:00Z.
  • A model score computed from the 14:00 to 15:00 hourly candle is knowable at 15:00.
  • A value published by a third party with a delay is knowable when it was published.

For a fixed publication delay, keep the natural timestamp and set lagSeconds. Each point becomes visible at ts + lagSeconds, evaluated at the next base-candle close, never earlier. A value stamped 00:00 but only released at 01:00 gets lagSeconds: 3600. The lag is fixed per series (from 0 to 30 days) and changes only with a replace upload.

Getting this wrong is the same mistake as look-ahead bias in any backtest, just harder to spot because the leak sits in your preprocessing, not in Trigr. If a model was trained on labels that overlap the bars it later scores, the leak happened before the upload; data leakage in machine learning for trading and purged walk-forward validation cover how to prevent that.

How does Trigr treat gaps and missing values?

An uploaded series behaves like a stale data feed:

  • Before the first visible point, the value is missing, and the backtest window is clipped to start at the series' coverage.
  • Between points, the last value holds, but only for up to three times the series' median spacing. After a longer gap the value is missing.
  • A missing value blocks. Any comparison with a missing side is unknown, and an unknown condition is false, so no trade is taken on stale data.

That last rule is conservative on purpose. A daily series with a two-week outage will not keep trading on the last value it saw.

How do you use an uploaded series in a formula?

upload("name") behaves like close: any function that takes a series takes it. Some patterns:

Use Formula
Signal unusually high versus its recent history zscore(upload("my_signal"), 50) > 1
Signal turns positive crosses_above(upload("my_signal"), 0)
Signal in its top decile, only in an uptrend rank(upload("my_signal"), 500) > 0.9 and close > ema(close, 200)
Exit when the signal fades upload("my_signal") < 0 (as exitLong)

Combining your signal with a plain price condition, as in the third row, is a cheap robustness check: if the signal only works when a simple trend filter is also on, the trend may be doing the work. The Expression node overview lists every function available, and event vs state signals explains how to wire exits when your entry is a one-bar event like a cross.

Why are uploaded series research only?

Every other input Trigr uses comes from a source it stores and timestamps itself, which is what lets it guarantee a backtest is point in time. An uploaded series is self-reported. Trigr checks that timestamps are increasing, values are finite and the series is yours, but it has no way to check whether a value stamped 15:00 was really computable at 15:00.

So a strategy that reads upload("name") is labelled exploratory research. It can be backtested and iterated on as experiments, but it can never be published to the marketplace, never get a marketplace paper track, and never be deployed to an Agent, Paper included. If the research holds up, the path to live trading is to rebuild the idea from inputs Trigr can verify: formulas over its market data and api() sources.

What are the limits?

Limit Value
Series per account 20
Points per series 200,000
Points per request 50,000
Series name lowercase letter first, then lowercase letters, digits or underscores, up to 48 characters
Values finite numbers with an absolute value up to 10^12
Timestamps integer epoch milliseconds, strictly increasing, from 2009-01-03 to now
lagSeconds 0 to 2,592,000 (30 days)
Description up to 500 characters

Series are private to the account that uploads them. The owner always comes from your login or API key, never from a field in the request, so another account's formula cannot read your series.

A sensible research loop

  1. Upload the signal with honest timestamps and the right lagSeconds.
  2. Build one strategy around it, backtest gross, then with slippage and funding.
  3. Read the trade log, not just the summary, and check how many trades the signal really drives.
  4. Run variations as labelled experiments on the same strategy, so the number of things you tried stays visible.
  5. If it survives, rebuild it from verifiable inputs before thinking about paper or live trading.

Backtests are not guarantees, and perps are leveraged instruments that can lose more than expected.

Frequently asked questions

Can I backtest a signal I computed outside Trigr?

Yes. Upload it as a series of timestamped values, then read it inside an Expression formula with upload("name"), for example zscore(upload("my_signal"), 50) > 1. The backtest runs on Trigr's own price data and fills, with your series as one input.

Can I publish or run a live agent on an uploaded signal?

No. Uploaded series are self-reported data, and Trigr cannot verify that each value was knowable at its timestamp. A strategy that reads upload() is exploratory research only: it cannot be published, get a marketplace paper track, or be deployed to any Agent, Paper included.

What timestamp should each value have?

The moment the value became knowable, in UTC epoch milliseconds. A value computed from the daily candle that closes at midnight is stamped at that midnight, not at the start of the day. If the value was only published later, add a lagSeconds delay.

Is uploading free?

Uploading costs no credits. You can keep up to 20 series per account, each up to 200,000 points. The REST endpoint needs an API key from a paid plan; the MCP tool works on any plan. Backtests that read a series use credits like any other backtest.

Put the idea to an honest test.

Describe a strategy in plain English or from your own AI assistant, backtest it on point-in-time data, and forward-test it on paper before any real money is involved.