Prices you can prove.
Every synthetic tick a trader sees on our platform is generated by an in-house engine that reads only its own configuration and its own random-number cursor. It cannot see your positions, your balance, your stop losses, or which side of the market you are on. This page explains how that works, in plain English, so you can decide for yourself whether the firm deserves the trust.
A synthetic is a price we generate ourselves.
When a trader opens a chart for ALP25 or DLR on our terminal, they are not looking at a live feed from the FX interbank market. They are looking at a market that exists only inside our platform. We built the engine that produces the price. We publish the configuration for every symbol. The behavior of the market is a matter of arithmetic, not opinion.
We use three families of synthetic instruments. Alpha markets are pure volatility random-walks. Delta markets look normal until an occasional one-direction spike (boom or crash). Sigma markets either tick continuously in fixed increments or hold steady and jump in discrete steps. Every symbol names its family up front so a trader can pick the behaviour that fits their strategy.
The important part: the same tick, at the same millisecond, is delivered to every trader on the platform. There is no per-trader feed. There is one feed. Everyone reads from it.
Deterministic seed → PRNG → per-symbol rule → tick.
Every symbol starts life with two numbers: a hash of its ticker string (so ALP25 and ALP50get different starting seeds) and a platform-wide seed constant. XOR them and you get the initial state for that symbol’s deterministic pseudo-random number generator (a Mulberry32 PRNG). From then on, every tick is produced by advancing that PRNG one step and applying the symbol’s family rule.
In pseudocode — the actual function is 320 lines of TypeScript with no branches on any trader-owned data:
// Per-symbol tick loop, once every tickIntervalMs
function stepSymbol(config, previousState, now):
// 1. draw a new random number from the PRNG cursor
(nextPrng, u) = mulberry32(previousState.prngState)
// 2. apply the family rule to get a candidate mid
candidateMid = rule[config.family](config, previousState, u)
// 3. clamp to [0.5 x basePrice, 2 x basePrice]
newMid = clamp(candidateMid, config.basePrice)
// 4. spread is a fixed per-symbol constant, halved on each side
halfSpread = config.spread / 2
bid = newMid - halfSpread
ask = newMid + halfSpread
// 5. persist the tick and broadcast to every subscriber
return { symbol, bid, ask, mid: newMid, timestamp: now }
Read the whole function and you will find exactly three inputs: the symbol’s static configuration, the symbol’s prior price state, and the wall clock. No trader identity. No account balance. No open positions. No stop losses. No conditional branches on any of those things. It cannot see them because the function is never given them.
What we cannot do, by design.
The engine’s public interface is five functions: start, stop, getMid, getBidAsk, and asMidLookup. That surface is defined in the codebase as a TypeScript type. The type has no methods for reading a trader’s position, taking a trader-derived input, or overriding a tick after it has been broadcast. Adding such a method would fail the type checker and the build would not ship.
The boundary is documented in the code:
MidLookup is what the trading engine receives to price a fill or evaluate SL / TP. It is a pure read side of the price engine; it cannot mutate price. The trading engine may NOT be given the price engine instance itself.
—lib/synthetic/types.ts
In plain terms: the part of the system that sees your account (the trading engine) can only readthe current bid and ask. The part of the system that decides what the next tick is (the price engine) can only see its own configuration and its own random-number cursor. The two halves are separate. Neither can touch the other side’s data. That is the fairness boundary.
Concretely, the price engine has no ability to:
- Read your position size, direction, entry, stop loss, or take profit.
- Read your balance, equity, drawdown, or challenge phase.
- Read your account age, country, IP, or any other identity attribute.
- See whether anyone has open positions on the symbol at all.
- Skew the next tick to trigger a stop or bypass a take-profit.
- Widen the spread on a specific trader while narrowing it for another.
None of those knobs exist in the codebase. Not turned off, not hidden, not conditionally disabled — they were never written.
Every tick lands in a table you can query.
Every tick the engine produces is persisted to a Postgres table called synthetic_ticksin our Supabase database, with the symbol, bid, ask, mid, and a millisecond timestamp. The table is the source of truth for anything the platform needs to know about historical prices — charts, fill prices, stop evaluations, and the retroactive reconstruction of any trade.
Because the record is the same one every trader sees at any given moment, a trader disputing a fill can ask us for the tick sequence that covers the trade. We publish the sequence, timestamps included, and the trader can verify the fill price against the tick record. If we deviated from our own feed, the discrepancy shows up in the row.
A public replay endpoint is on the roadmap: pick a minute of history, get back every tick that fired in that minute for every active symbol. When it ships we will link it here.
Fairness of the feed is not the same as a perfect trading experience.
We would rather be honest about the limits of this page than have a trader assume it guarantees more than it does. Being able to prove the tick is fair does not eliminate every risk of trading with us.
- Execution latency. The tick you see and the tick that fills your order are the same tick, but network latency between your device and our servers can put a few hundred milliseconds between them. Fast markets can move against you inside that window.
- Model risk. Our engine draws from a normal distribution and applies clamps. Real financial markets show fatter tails, longer trends, and occasional dislocations that a deterministic model does not replicate perfectly. The delta family adds spikes to approximate this, but no synthetic can be a full mirror of the real thing.
- Spreads and commission.Every symbol has a published spread and, where applicable, a per-lot commission. Both favor the house on aggregate over a long enough sample. That is the economic model of the firm, not a fairness violation — but it is a real cost you should account for.
- Risk-mode overrides. An admin can put a symbol into
close_onlyorpausedmode, or apply a spread multiplier during unusual events. These actions are audited (every change writes a row to the audit log with actor, before, after, and reason). They cannot be applied on a per-trader basis — only to the symbol universe. - Evaluation rules still apply. Fairness of the tick does not override the daily drawdown, maximum drawdown, or minimum-day rules of the challenge. Those are separate rules; see the rules page.
The prose here is not the source of truth. The code is.
This page is a translation of the codebase into English. The codebase itself is the real artefact. The engine lives at lib/synthetic/priceEngine.ts, its type contracts at lib/synthetic/types.ts, and its per-symbol configuration at lib/synthetic/symbols.ts. Each file is under 400 lines and the fairness boundary is annotated inline with // fairness: comments so an auditor can walk the surface in one sitting.
We are not open-source today — the codebase includes payment, KYC, and other components that need to stay private. When we can, we will publish the price engine as a standalone module so third parties can audit it in place. Until then, we will hand over the relevant files under NDA to any counterparty or regulator that asks. Email compliance@tfcfunder.com.
We built the engine so we could not cheat, and we are showing you the shape of it so you do not have to take our word for it. Read the file. Read the type. If the surface has a hidden knob, tell us and we will fix it. That is the deal.