Integrations
Backtesting with Historical Rates
Run a historical simulation on dated observations while controlling look-ahead bias, carry-forward values and uneven currency coverage.
The job
Replay a currency strategy against historical data without inventing information
that was not available at the decision date. Use
/v1/history for the daily series and
/v1/history/analysis for window
statistics.
/v1/history is available on every plan, with a 30-day window on Free and full
history on Starter and above. The analysis endpoint requires Growth or higher.
Three biases to control
| Bias | What goes wrong | Control |
|---|---|---|
| Look-ahead | A signal uses a value published after the trade date | Use only observations at or before the decision date |
| Carry-forward | A repeated weekend value is counted as a new observation | Detect repeats and decide whether they are actionable |
| Survivorship | The current currency list is treated as the universe that existed in the past | Check each currency’s first quote and document the universe |
A backtest that ignores these three will usually look better than the strategy actually was.
Look-ahead bias
At each simulated decision date, use only values that were available on or before that date. Do not join a signal computed from a later revision onto an earlier trade.
Build the series once, then slice it by date rather than querying a full range and reading forward. If your signal needs a 30-day lookback, the first decision date is the 31st observation, not the first row.
Carry-forward is not a new observation
History returns a dense date map. Weekends and holidays repeat the last published trading day, so a value can be unchanged for several calendar days. That is a repeat, not a fresh fixing. A genuinely unchanged fixing looks identical, so the API does not label the difference.
If your strategy trades on “new” data, detect repeats and skip them. If your strategy trades on calendar time, keep them and document that choice.
A complete signal example
This example fetches a series, marks repeats, and computes a simple 30-day momentum signal. It is a signal demonstration, not a profit-and-loss model. Add execution, spreads, financing and position sizing separately.
type HistoryResponse = {
currency: string;
base: string;
interval: string;
from: string;
to: string;
rates: Record<string, number>;
};
type Observation = {
date: string;
rate: number;
repeat: boolean;
};
type Signal = {
date: string;
rate: number;
lookbackRate: number;
changePct: number;
signal: "long" | "short";
};
async function fetchHistory(
currency: string,
base: string,
from: string,
to: string,
): Promise<HistoryResponse> {
const url = new URL("https://api.currency-core.com/v1/history");
url.searchParams.set("currency", currency);
url.searchParams.set("base", base);
url.searchParams.set("from", from);
url.searchParams.set("to", to);
url.searchParams.set("interval", "daily");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.CURRENCECORE_API_KEY}` },
signal: AbortSignal.timeout(30_000),
});
const body = await res.json();
if (!res.ok) {
throw Object.assign(new Error(body.error?.message ?? `HTTP ${res.status}`), {
status: res.status,
code: body.error?.code,
});
}
return body as HistoryResponse;
}
function toObservations(data: HistoryResponse): Observation[] {
let previous: number | undefined;
return Object.entries(data.rates).map(([date, rate]) => {
const repeat = previous !== undefined && rate === previous;
previous = rate;
return { date, rate, repeat };
});
}
export async function momentumBacktest(input: {
currency: string;
base: string;
from: string;
to: string;
lookback?: number;
}) {
const lookback = input.lookback ?? 30;
const data = await fetchHistory(input.currency, input.base, input.from, input.to);
const observations = toObservations(data);
const signals: Signal[] = [];
for (let i = lookback; i < observations.length; i += 1) {
const current = observations[i];
const past = observations[i - lookback];
// Act only on a changed value, not on a weekend repeat.
if (current.repeat || !past) continue;
const change = (current.rate - past.rate) / past.rate;
signals.push({
date: current.date,
rate: current.rate,
lookbackRate: past.rate,
changePct: change * 100,
signal: change > 0 ? "long" : "short",
});
}
return {
observations: observations.length,
actionable: signals.length,
signals,
};
}
Call it once per pair:
const result = await momentumBacktest({
currency: "EUR",
base: "USD",
from: "2015-01-01",
to: "2025-12-31",
lookback: 30,
});
console.log(result.observations, result.actionable, result.signals[0]);
A signal row looks like:
{
"date": "2026-01-30",
"rate": 1.0861,
"lookbackRate": 1.0352,
"changePct": 4.92,
"signal": "long"
}
The numbers are illustrative. The important part is that date is the decision
date, lookbackRate was available on or before that date, and a repeat row was
not treated as a new observation.
Coverage and universe
History coverage is uneven:
- GBP/USD and USD/INR annual parity starts in 1949.
- Daily major pairs start in 1971.
- EUR pairs start in 1999.
- Some Asian and emerging-market pairs start later.
Dates before a currency’s first quote are absent, not zero. Do not treat the current currency list as a survivorship-bias-free universe, and do not assume every pair has the same start date. Run the backtest per pair and compare only over the intersection of the available windows.
Plan and request limits
- Free: last 30 days of history. Older
fromdates return403 forbidden. - Starter and above: full history.
/v1/history/analysis: Growth or higher.- No maximum span on
/v1/history, so a single request can return a multi-decade series. Chunk for memory and retries, not because of an API limit. - The per-minute limit counts every request, including retries.
Worked example
Backtest a 30-day momentum signal on EUR/USD from 2015-01-01 to 2025-12-31:
- Fetch the daily series once.
- Convert the date map to ordered observations and mark repeats.
- Start the loop at index 30 so every signal has a full lookback.
- Skip repeat rows so a weekend does not create a trade.
- Record the signal date, the current rate, the lookback rate and the change.
- Add execution costs and position sizing in a separate layer.
Do not compute the signal from the last row and then apply it to earlier rows. That is look-ahead bias.
Failure handling
| Status | What to do |
|---|---|
400 invalid_input | Check the currency, base, date format and range |
401 unauthorized | Check the server-side key |
403 forbidden | A Free key asked for a range outside the 30-day window |
404 unknown_currency | The currency has no history in the window |
429 rate_limited | Honor Retry-After; retry the failed chunk |
5xx | Retry with backoff and keep completed chunks |