Prop Firm Technology For FTMO's Consistency Rule Change: A Versioned Rule Engine Pinned To Order Time, With Reproducible Payout Decisions
FTMO replaced its Best Day Rule with a Consistency rule for orders created from 12 October 2026, 9:00 GMT+3, while earlier orders keep the old rule. On the same five trading days the share moves from 50% to 80%, and two rule sets now run side by side, keyed to order time. This guide builds the versioned rule registry, the worked example as a unit test and a payout-eligibility workflow that recomputes from the ledger and sends every refusal to a person.
AlchmAI Engineering16 min read
50% to 80%
Best Day share for the same five trading days under FTMO's Best Day Rule and its new Consistency rule, in FinanceFeeds' worked example
09:00 GMT+3
Cut-over on 12 October 2026: orders created from then get the Consistency rule; earlier orders keep the Best Day Rule
$14.4m
Paid by FTMO across about 7,100 payouts in September 2026, by the company's own figures as reported by Finance Magnates
80 to 100
Prop firms that closed in 2024, according to Finance Magnates' report on the closure of FundedSeat
On 8 October 2026 FTMO announced that from Monday 12 October 2026, 9:00 MetaTrader time (GMT+3), its Consistency rule replaces the Best Day Rule for all new orders, while 'Accounts from orders created before 12 October 2026, 9:00, stay under the Best Day Rule.' For anyone building prop firm technology, that sentence matters more than the formula. From that minute two rule sets are live side by side, and which one an account answers to is fixed by when its order was created, not by when anyone checks it. A rules engine that reads one global configuration, the easy way to build one, either moves every legacy account onto the new rule overnight or never applies the new rule at all.
The money explains why this is worth doing properly. FTMO says it paid more than $14.4m across about 7,100 payouts in September 2026, and more than $650m in total, company figures Finance Magnates reported on 8 October 2026. New entrants keep arriving: CMC Markets Funded launched on 1 October 2026 with a $100,000 simulated evaluation, and CMC Markets told Finance Magnates 'We are clear that every customer account remains simulated'. Others are leaving. The futures prop firm FundedSeat closed citing 'limitations from big trading platforms', after monthly payouts it put at nearly $700,000, and the same Finance Magnates report puts prop firm closures in 2024 at 80 to 100. At that volume every refused payout is a possible dispute, and the only good answer to a dispute is a decision you can replay: the same rule version, the same daily results, the same arithmetic. This guide builds that in TypeScript, in three parts: the worked example as a unit test, a versioned rule registry pinned by order-creation time in platform time, and a payout-eligibility workflow that recomputes from the ledger, records what it used and sends every refusal to a person.
1. What Is The Difference Between The Best Day Rule And The Consistency Rule?
Both rules ask the same question, how much of the profit came from the best day, and differ only in what they divide by. FinanceFeeds worked the change through with five trading days:
- Day 1: a profit of $4,000, the Best Day.
- Day 2: a profit of $2,000.
- Day 3: a loss of $2,000.
- Day 4: a profit of $2,000.
- Day 5: a loss of $1,000.
Under the Best Day Rule the denominator is profit from positive days only, $4,000 + $2,000 + $2,000 = $8,000, so the share is '$4,000 divided by $8,000', or 50%. Under the Consistency rule it is total profit, the $8,000 of gains less $3,000 of losses, so the share is '$4,000 divided by $5,000', or 80%. Nothing about the trading changed. The same history reads 30 percentage points more concentrated because the losing days now count.
Two engineering consequences follow. A losing day raises the share under the Consistency rule, because it shrinks the denominator, and changes nothing under the Best Day Rule. And total profit can reach zero or go negative while a best day still exists, which leaves the ratio undefined; the engine has to say what that means rather than divide by zero. The code below treats an undefined share as not yet met, which fits FTMO's own framing that the account simply keeps trading.
This guide does not state FTMO's threshold. The update points to the Trading Objectives page for the full rule text, and the threshold, together with whether a share exactly at the limit passes, belongs in configuration checked against that text, not in a constant. The tests use 60% purely as an illustration. Write the worked example down as a test before writing the engine: it is the one set of numbers on which the firm's rule, the press coverage and the trader's own arithmetic already agree.
// Node 20+: node --test, through a TypeScript loader such as tsx or after compiling
import { test } from "node:test";
import assert from "node:assert/strict";
import { buildRegistry, evaluate, platformTimeToUtc, type DailyResult } from "./ruleRegistry";
// 60% is an illustration for these tests only. Real thresholds come from the firm's published rule text.
const registry = buildRegistry({ bestDayMaxShareBps: 6_000, consistencyMaxShareBps: 6_000, passOnEqual: true });
const bestDay = registry.byId("best-day.v1");
const consistency = registry.byId("consistency.v2");
// FinanceFeeds' worked example, in integer cents.
const days: DailyResult[] = [
{ tradingDay: "2026-10-12", netCents: 400_000 },
{ tradingDay: "2026-10-13", netCents: 200_000 },
{ tradingDay: "2026-10-14", netCents: -200_000 },
{ tradingDay: "2026-10-15", netCents: 200_000 },
{ tradingDay: "2026-10-16", netCents: -100_000 },
];
const withDay5 = (netCents: number) =>
days.map((d) => (d.tradingDay === "2026-10-16" ? { ...d, netCents } : d));
test("Best Day Rule: $4,000 divided by $8,000 of positive days is 50%", () => {
const r = evaluate(bestDay, days);
assert.equal(r.bestDayCents, 400_000);
assert.equal(r.denominatorCents, 800_000);
assert.equal(r.shareBps, 5_000);
assert.equal(r.status, "MET");
});
test("Consistency rule: $4,000 divided by $5,000 of total profit is 80%", () => {
const r = evaluate(consistency, days);
assert.equal(r.denominatorCents, 500_000);
assert.equal(r.shareBps, 8_000);
assert.equal(r.status, "NOT_YET_MET"); // not a breach: the account keeps trading
});
test("a bigger loss on day 5 moves only the Consistency share", () => {
assert.equal(evaluate(bestDay, withDay5(-150_000)).shareBps, 5_000);
assert.equal(evaluate(consistency, withDay5(-150_000)).shareBps, 8_889); // 4,000 / 4,500
});
test("no net profit means no share, and no share is never met", () => {
const r = evaluate(consistency, withDay5(-600_000));
assert.equal(r.shareBps, null);
assert.equal(r.status, "NOT_YET_MET");
});
test("the rule is pinned by order creation time in platform time (GMT+3)", () => {
assert.equal(registry.forOrder(platformTimeToUtc("2026-10-12T08:59:59", 180)).id, "best-day.v1");
assert.equal(registry.forOrder(platformTimeToUtc("2026-10-12T09:00:00", 180)).id, "consistency.v2");
// 08:30 in London (BST, UTC+1) is 10:30 GMT+3: already the new rule.
assert.equal(registry.forOrder(Date.parse("2026-10-12T08:30:00+01:00")).id, "consistency.v2");
});2. Why Does Prop Firm Technology Need A Versioned Rule Registry, Not A Config Flag?
Because the update keys the rule to the order, the rule set is a property of the account, fixed at creation like the account size or the plan bought. Treat it as global configuration and four things break.
- Evaluating against 'the current rule'. Flip a flag at 09:00 and every legacy account is judged against a denominator it never signed up for, including accounts part-way through a payout cycle.
- Pinning to the wrong timestamp. The update names order creation. Account activation, first trade and evaluation start can each fall after 09:00 for an order created at 08:58, and each would put that account on the wrong rule.
- Comparing in the wrong clock. 09:00 GMT+3 on 12 October 2026 is 06:00 UTC, and 07:00 in London, which was still on British Summer Time that day. A support tool that compares London wall-clock times with '09:00' files two hours of new orders under the old rule.
- Summing days in the wrong clock. If the platform's trading day starts at midnight GMT+3, a ledger that groups deals by UTC date files the first three hours of every platform day under the previous day, which can change which day is the best day.
The registry below makes the rule set data. Each version has a formula, a threshold, a half-open window of order-creation times stored as UTC instants, and a content hash. Windows must be contiguous with exactly one open-ended version, so a new rule cannot be added without closing the old one at the same instant. The cut-over is written once, in platform time exactly as the firm published it, and converted to UTC at the edge. The pin is written to the account when the order is created and is never silently recomputed.
One question the code cannot answer: the update speaks only of orders, so how a reset, a free retry or a funded account issued after a passed evaluation inherits its rule set is a product decision. Make it explicitly, write it down beside the registry and in the terms, and do not let the code infer it.
// Node 20+, no dependencies. Money is integer cents; time is UTC epoch milliseconds.
import { createHash } from "node:crypto";
export type Formula = "BEST_DAY_OVER_POSITIVE_DAYS" | "BEST_DAY_OVER_TOTAL_PROFIT";
/** One row per closed trading day, summed by the platform's trading day, not the server's. */
export interface DailyResult { tradingDay: string; netCents: number }
export interface RuleVersion {
id: string;
formula: Formula;
maxShareBps: number; // from the firm's published rule text; 10_000 bps = 100%
passOnEqual: boolean; // does a share exactly at the threshold meet it?
ordersFromUtc: number; // inclusive, by order creation time
ordersBeforeUtc: number | null; // exclusive; null marks the current version
sourceRef: string; // the rule text this version implements
}
export interface Evaluation {
ruleVersionId: string;
ruleVersionHash: string;
bestDayCents: number;
denominatorCents: number;
shareBps: number | null; // rounded, for display only
status: "MET" | "NOT_YET_MET"; // neither rule can be breached: not met means keep trading
}
/** "2026-10-12T09:00:00" at the offset the firm published for that instant, to UTC. */
export function platformTimeToUtc(local: string, offsetMinutes: number): number {
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(local)) throw new Error(`not a platform timestamp: ${local}`);
return Date.parse(`${local}Z`) - offsetMinutes * 60_000;
}
// FTMO trading update, 8 October 2026: new orders from 12 October 2026, 9:00 GMT+3 (06:00 UTC).
export const CUTOVER_UTC = platformTimeToUtc("2026-10-12T09:00:00", 180);
const sha256 = (s: string) => createHash("sha256").update(s).digest("hex");
export const versionHash = (v: Readonly<RuleVersion>) =>
sha256(JSON.stringify([v.id, v.formula, v.maxShareBps, v.passOnEqual, v.ordersFromUtc, v.ordersBeforeUtc]));
export class RuleRegistry {
private readonly versions: readonly Readonly<RuleVersion>[];
constructor(versions: RuleVersion[]) {
if (new Set(versions.map((v) => v.id)).size !== versions.length) throw new Error("duplicate rule version id");
const sorted = [...versions].sort((a, b) => a.ordersFromUtc - b.ordersFromUtc);
sorted.forEach((v, i) => {
const next = sorted[i + 1];
if (v.maxShareBps <= 0 || v.maxShareBps > 10_000) throw new Error(`${v.id}: threshold out of range`);
if (next ? v.ordersBeforeUtc !== next.ordersFromUtc : v.ordersBeforeUtc !== null) {
throw new Error(`${v.id}: windows must be contiguous, and only the last may be open-ended`);
}
});
this.versions = Object.freeze(sorted.map((v) => Object.freeze({ ...v })));
}
/** Fixed by when the order was created. Never by "now", never by account activation. */
forOrder(orderCreatedAtUtc: number): Readonly<RuleVersion> {
const v = this.versions.find((r) => orderCreatedAtUtc >= r.ordersFromUtc &&
(r.ordersBeforeUtc === null || orderCreatedAtUtc < r.ordersBeforeUtc));
if (!v) throw new Error(`no rule version for an order created at ${new Date(orderCreatedAtUtc).toISOString()}`);
return v;
}
byId(id: string): Readonly<RuleVersion> {
const v = this.versions.find((r) => r.id === id);
if (!v) throw new Error(`unknown rule version ${id}`);
return v;
}
}
/** Thresholds are configuration reviewed against the firm's rule text, never constants in code. */
export function buildRegistry(cfg: { bestDayMaxShareBps: number; consistencyMaxShareBps: number; passOnEqual: boolean }) {
return new RuleRegistry([
{ id: "best-day.v1", formula: "BEST_DAY_OVER_POSITIVE_DAYS", maxShareBps: cfg.bestDayMaxShareBps,
passOnEqual: cfg.passOnEqual, ordersFromUtc: 0, ordersBeforeUtc: CUTOVER_UTC,
sourceRef: "Trading Objectives: Best Day Rule" },
{ id: "consistency.v2", formula: "BEST_DAY_OVER_TOTAL_PROFIT", maxShareBps: cfg.consistencyMaxShareBps,
passOnEqual: cfg.passOnEqual, ordersFromUtc: CUTOVER_UTC, ordersBeforeUtc: null,
sourceRef: "Trading update, 8 October 2026; Trading Objectives: Consistency rule" },
]);
}
/** Pure and deterministic: same version, same rows, same answer on any machine. */
export function evaluate(v: Readonly<RuleVersion>, days: readonly DailyResult[]): Evaluation {
const seen = new Set<string>();
for (const d of days) {
if (!Number.isSafeInteger(d.netCents)) throw new Error(`${d.tradingDay}: P&L must be integer cents`);
if (seen.has(d.tradingDay)) throw new Error(`${d.tradingDay}: duplicate trading day`);
seen.add(d.tradingDay);
}
const bestDayCents = days.reduce((m, d) => Math.max(m, d.netCents), 0);
const denominatorCents = v.formula === "BEST_DAY_OVER_POSITIVE_DAYS"
? days.reduce((s, d) => s + Math.max(d.netCents, 0), 0) // Positive Days' Profit only
: days.reduce((s, d) => s + d.netCents, 0); // total profit: losing days count
const base = { ruleVersionId: v.id, ruleVersionHash: versionHash(v), bestDayCents, denominatorCents };
if (bestDayCents <= 0 || denominatorCents <= 0) return { ...base, shareBps: null, status: "NOT_YET_MET" };
// Decide on exact integers. The rounded share is for the dashboard, never for the decision.
const lhs = bestDayCents * 10_000;
const rhs = v.maxShareBps * denominatorCents;
const met = v.passOnEqual ? lhs <= rhs : lhs < rhs;
return { ...base, shareBps: Math.round(lhs / denominatorCents), status: met ? "MET" : "NOT_YET_MET" };
}Money is integer cents from the ledger to the decision, so the same rows give the same answer on any machine, and pass or fail is decided by comparing exact integer products rather than a rounded percentage. The rounded share exists only for display. That matters on the day a dashboard rounds a share to exactly the threshold while the unrounded figure sits just above it.
3. How Do You Make Every Payout Eligibility Decision Reproducible?
A payout dispute comes down to three questions: which rule applied, which numbers it used, and whether a person looked. Payouts are where trading automation meets money, so the workflow answers all three by construction. It checks the stored pin against the order time, recomputes the daily results from the ledger rather than trusting a cached metric, and compares its answer with the figure the trader was shown. FTMO publishes results in Account MetriX; whatever a firm's equivalent is, if it and the payout path disagree, a person should find out before the trader does.
Each decision goes to an insert-only audit log with the rule version ID and hash, the exact daily rows and a hash of them, the engine build and the outcome. Nothing is refused by code alone. A share not yet met, a pin that disagrees with the order time, a dashboard mismatch, an amount above the automatic limit or a kill switch routes the decision to a reviewer, whose verdict, written reason and replay result are logged. A pass is not a transfer either: releasing money goes through a separate approval step.
// Node 20+. Recompute from the ledger, record what was used, and send every refusal to a person.
import { createHash, randomUUID } from "node:crypto";
import { evaluate, type DailyResult, type Evaluation, type RuleRegistry } from "../rules/ruleRegistry";
export interface FundedAccount {
accountId: string;
orderId: string;
orderCreatedAtUtc: number; // from the order record, never from account activation
pinnedRuleVersionId: string; // written once, when the order was created
}
export interface Ledger {
/** Closed trading days only, summed by the platform's trading day from the deal history. */
dailyResults(accountId: string, throughTradingDay: string): Promise<DailyResult[]>;
}
export interface AuditLog { append(event: { kind: string } & Record<string, unknown>): Promise<void> } // insert-only
export interface ReviewQueue { enqueue(d: EligibilityDecision): Promise<void> }
export interface PayoutApprovals { request(d: EligibilityDecision): Promise<{ approvalId: string }> } // a second person releases money
export interface KillSwitch { isOn(scope: "payouts"): Promise<boolean> }
export interface EligibilityDecision {
decisionId: string; accountId: string; orderId: string; decidedAt: string; engineVersion: string;
throughTradingDay: string; amountCents: number;
days: DailyResult[]; inputsHash: string; // the rows themselves, and proof they have not changed
evaluation: Evaluation;
dashboardShareBps: number | null; // the figure the trader was shown
outcome: "SEND_FOR_APPROVAL" | "REFER_TO_REVIEWER";
reasons: string[];
}
const ENGINE_VERSION = process.env.RULE_ENGINE_VERSION ?? ""; // e.g. the git SHA of this build
const MAX_AUTO_PAYOUT_CENTS = Number(process.env.MAX_AUTO_PAYOUT_CENTS ?? "0");
if (!ENGINE_VERSION) throw new Error("RULE_ENGINE_VERSION is required: every decision names the code that made it");
const sha256 = (s: string) => createHash("sha256").update(s).digest("hex");
const canonical = (days: readonly DailyResult[]): DailyResult[] =>
days.map(({ tradingDay, netCents }) => ({ tradingDay, netCents }))
.sort((a, b) => a.tradingDay.localeCompare(b.tradingDay));
const COMPARED = ["ruleVersionHash", "bestDayCents", "denominatorCents", "shareBps", "status"] as const;
export class PayoutEligibilityWorkflow {
constructor(
private readonly registry: RuleRegistry, private readonly ledger: Ledger, private readonly audit: AuditLog,
private readonly reviewers: ReviewQueue, private readonly approvals: PayoutApprovals, private readonly kill: KillSwitch,
) {}
async decide(acc: FundedAccount, throughTradingDay: string, amountCents: number,
dashboardShareBps: number | null): Promise<EligibilityDecision> {
const reasons: string[] = [];
if (await this.kill.isOn("payouts")) reasons.push("payout kill switch is on");
// The pin is the contract. If it disagrees with the order time, a person decides; code never re-pins.
const fromOrderTime = this.registry.forOrder(acc.orderCreatedAtUtc).id;
if (fromOrderTime !== acc.pinnedRuleVersionId) {
reasons.push(`pinned ${acc.pinnedRuleVersionId}, order time gives ${fromOrderTime}`);
}
const rule = this.registry.byId(acc.pinnedRuleVersionId);
// Recompute from the ledger every time. A cached metric is a claim, not an input.
const days = canonical(await this.ledger.dailyResults(acc.accountId, throughTradingDay));
const evaluation = evaluate(rule, days);
if (evaluation.status === "NOT_YET_MET") reasons.push(`${rule.id} not yet met: not a breach, the account keeps trading`);
if (dashboardShareBps !== evaluation.shareBps) {
reasons.push(`dashboard showed ${dashboardShareBps} bps, ledger gives ${evaluation.shareBps}`);
}
if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || amountCents > MAX_AUTO_PAYOUT_CENTS) {
reasons.push("amount outside the automatic limit");
}
const decision: EligibilityDecision = {
decisionId: randomUUID(), accountId: acc.accountId, orderId: acc.orderId, decidedAt: new Date().toISOString(),
engineVersion: ENGINE_VERSION, throughTradingDay, amountCents, days, inputsHash: sha256(JSON.stringify(days)),
evaluation, dashboardShareBps, outcome: reasons.length ? "REFER_TO_REVIEWER" : "SEND_FOR_APPROVAL", reasons,
};
await this.audit.append({ kind: "eligibility_decided", ...decision }); // no audit record, no decision
if (decision.outcome === "REFER_TO_REVIEWER") {
await this.reviewers.enqueue(decision);
} else {
const { approvalId } = await this.approvals.request(decision);
await this.audit.append({ kind: "payout_approval_requested", decisionId: decision.decisionId, approvalId });
}
return decision;
}
/** For a dispute: same pinned rule, same rows, same answer. Any difference is the finding. */
replay(d: EligibilityDecision) {
const rows = canonical(d.days);
const now = evaluate(this.registry.byId(d.evaluation.ruleVersionId), rows);
return {
now,
rowsUnchanged: sha256(JSON.stringify(rows)) === d.inputsHash,
sameAnswer: COMPARED.every((k) => now[k] === d.evaluation[k]),
sameEngine: d.engineVersion === ENGINE_VERSION,
};
}
/** Every refusal ends here: a named reviewer, a written reason and a replay, all logged. */
async review(d: EligibilityDecision, reviewerId: string, verdict: "UPHOLD" | "OVERTURN", note: string) {
if (d.outcome !== "REFER_TO_REVIEWER") throw new Error("only referred decisions are reviewed");
if (!note.trim()) throw new Error("a review needs a written reason");
if (verdict === "OVERTURN" && (await this.kill.isOn("payouts"))) throw new Error("payout kill switch is on");
const replay = this.replay(d);
await this.audit.append({ kind: "eligibility_reviewed", decisionId: d.decisionId, reviewerId, verdict, note, ...replay });
// An overturn still goes through payout approval: the reviewer decides eligibility, not the transfer.
return verdict === "OVERTURN" ? this.approvals.request(d) : null;
}
}- Store the rows, not only the hash. A hash proves the inputs did not change; only the rows let a reviewer, or anyone adjudicating a complaint, redo the sum by hand.
- Keep old engine builds runnable. If a dispute arrives after a calculator fix, replay with the build named in the record, then explain the difference rather than quietly applying the new code.
- Write the trader message from the reviewed record. 'Not yet met, keep trading' is a status, not a breach notice, and the wording should say so.
“A rule that applies by order date is not a setting. It is a contract term, and the engine should treat it like one.”
Where This Belongs: Prop Firms, Brokers, Platform Teams And Risk Leads
- 01Prop trading firms: pin a rule version to every order at creation, backfill the pin for existing accounts from order records before the next rule change, and have the dashboard and the payout path call the same calculator. Keep FinanceFeeds' five-day example in CI as a regression test.
- 02Brokers and CFD providers launching evaluation products: CMC Markets Funded shows brokers now run evaluations of their own. Version the objectives from launch day, because the first rule change is when an unversioned engine fails.
- 03Platform and engineering teams that sell evaluation stacks to firms: expose the pinned rule version and its hash on every account through the API and in exports, so a client firm can prove which rule applied without opening a ticket with you.
- 04Risk and compliance leads: put refusals, pin mismatches and dashboard disagreements on a risk dashboard with reviewer turnaround and overturn rates. A rising overturn rate points at the calculator or the data, not at the traders.
The Bottom Line
Prop firm technology has to treat a rule change as a new version, not an edit. FTMO's update of 8 October 2026 moved orders created from 12 October 2026, 9:00 GMT+3 onto a Consistency rule that compares the best day with total profit, and left earlier orders on the Best Day Rule; on FinanceFeeds' five-day example the same history moves from 50% to 80%. Getting it right takes four habits: pin the rule version to the order at creation, convert the cut-over from platform time to a UTC instant once, recompute every decision from the daily ledger in integer cents, and record the version, rows, hashes and reviewer for every refusal. That is the trading workflow architecture AlchmAI, a London trading technology and AI studio, builds for prop firms and brokers, and it is what lets a firm answer a payout dispute with a replay instead of an argument.
References & Further Reading
- FTMO - Trading update, 8 October 2026. ftmo.com/en/blog/trading-updates/trading-update-8-oct-2026
- FinanceFeeds - FTMO swaps its Best Day Rule for a Consistency Rule, losing days now count against your best day. financefeeds.com/ftmo-swaps-its-best-day-rule-for-a-consistency-rule-from-today-losing-days-now-count-against-your-best-day
- Finance Magnates - FTMO Adds $200 Million to Lifetime Payouts in One Year as Monthly Rewards Near $15 Million (8 October 2026). financemagnates.com/forex/ftmo-adds-200-million-to-lifetime-payouts-in-one-year-as-monthly-rewards-near-15-million
- Finance Magnates - CMC Markets to Launch Prop Trading With an Interview Opportunity (28 September 2026). financemagnates.com/forex/brokers/cmc-markets-to-launch-prop-trading-with-an-interview-opportunity
- Finance Magnates - Futures Prop Firm FundedSeat Closes Due to Limitations From Big Trading Platforms (29 September 2026). financemagnates.com/forex/futures-prop-firm-fundedseat-closes-due-to-limitations-from-big-trading-platforms
- Node.js - Test runner documentation. nodejs.org/api/test.html
- AlchmAI - Prop firm technology. alchmai.com/solutions/prop-firm-technology
- AlchmAI - Risk and surveillance dashboards. alchmai.com/solutions/risk-and-surveillance-dashboards
AlchmAI Engineering
Engineering, London
Written by the AlchmAI engineering team in Mayfair, London. We build trading platforms, real-time charts, market data pipelines and AI features for brokers, prop firms and fintech teams. The Playbook is where we explain how we approach these systems, with code you can run and sources you can check.
Code in this guide is illustrative and supplied without warranty. Review and test it before production use. Nothing here is investment advice. Important information