A trade copier for prop-firm futures accounts. Trades placed on one leader account are mirrored to every follower account across different firms and brokers, with stop-loss and take-profit levels carried across, live per-account P/L, and one-click flatten.
Built as a NinjaTrader 8 AddOn in C#. That choice is the interesting part of this project — see why.
Status: feature-complete, not yet run against live accounts. Written on macOS against NinjaTrader's API documentation; NinjaTrader is Windows-only, so compilation and the test checklist are the remaining step. Nothing here has traded real money yet.
Funded futures traders typically run several accounts across several prop firms at once — each with its own drawdown limits and payout schedule. The same trade has to be placed on all of them, by hand, fast enough to get comparable fills. Miss one and the accounts diverge; fat-finger one and you've broken a rule on a funded account.
Three firms, two broker stacks:
| Firm | Broker |
|---|---|
| Topstep | ProjectX / Rithmic |
| Lucid Trading | Tradovate |
| Tradeify | Tradovate |
- Copies positions from the leader to every enabled follower, sized by a per-account multiplier.
- Mirrors brackets — the leader's stop and target prices are replicated onto followers as linked OCO orders, and re-issued when the leader's move.
- Shows live P/L per account, open and realised.
- Flattens any single account, or everything at once.
- Source-agnostic — because it tracks positions rather than NinjaTrader's own order flow, trades placed from a phone or web platform are copied identically.
TradingView / mobile / web
│
▼
┌───────────────────────┐
│ Leader account │
└───────────┬───────────┘
│ position updates
▼
┌───────────────────────┐
│ CopyTrader AddOn │ target = leader qty × multiplier
│ (inside NinjaTrader)│ order = target − current
└───────┬───────┬───────┘
▼ ▼
Follower A Follower B + mirrored SL/TP as OCO pairs
The obvious design is a service that authenticates to each broker's REST API. It doesn't work, and establishing exactly why drove the architecture.
Before writing any adapters, a throwaway validation script
(spike/auth-check.ts) tried to authenticate against the
real accounts. Every Tradovate login came back with The app is not registered.
The diagnostic detail matters: Tradovate validates the password before it validates app registration, so that specific error proves the credentials were correct and the failure was downstream. The block isn't the account — it's that Tradovate authenticates two things, who you are and what software is asking, and a self-built client has no registered application identity. Third-party copiers work precisely because they supply one. Obtaining it is commercial partner onboarding, not a developer signup.
The pivot: NinjaTrader owns Tradovate, and NinjaTrader is itself an authorised application. Running as an AddOn inside it means never making an API call and never needing a registration — the copier operates on connections NinjaTrader already holds and has already authenticated. All three firms support it, and it maintains multiple simultaneous broker connections.
Full write-up: docs/INVESTIGATION.md
Positions, not orders. Rather than replicating each order event, the engine
computes each follower's target position (leader qty × multiplier) and sends
one market order for the difference. The operation is idempotent, so partial
fills, dropped events and rejected orders all converge on the next update instead
of compounding into drift. A background sweep re-runs the same calculation every
two seconds, which means the safety net and the fast path share one code path
rather than being two implementations that can disagree.
Position flips are deliberately two-step. Reversing long-to-short closes first and lets the resulting position update open the other side, rather than sending an entry that the broker would reject because the close hasn't filled.
A disconnected leader reports no positions — which naively reads as "the leader went flat" and would flatten every follower. Found in review before it could ever run. Every sync is now gated on connection status.
Account.Change() applies whatever the three *Changed properties hold at
call time, so setting only the price silently rewrites quantity to zero. All
three are set together on every modify.
Disarmed by default. Nothing is sent until explicitly armed, behind a confirmation naming every account that will receive orders. The leader cannot be switched while armed. Flatten-all disarms first so nothing re-enters behind you.
ninjatrader/CopyTrader.cs the AddOn — engine, safety logic, WPF dashboard
spike/auth-check.ts broker API validation script (read-only)
docs/SETUP.md install, operation, test checklist, known gaps
docs/INVESTIGATION.md why the REST approach was abandoned
Entries work with any leader order type. Bracket mirroring handles a single stop and single target per instrument — scaled exits are not supported, MIT orders are ignored, and stop-limit is downgraded to stop-market on followers. Trailing stops do mirror correctly. Detail in docs/SETUP.md.
Personal tooling for the author's own accounts, published as a work sample. It places real orders on real funded accounts. Prop firms set their own rules on automation and copy trading, and it is the operator's responsibility to comply with them. No warranty — see LICENSE.