Skip to content

feat(exchange): add Trading212 broker integration#1073

Merged
bennycode merged 37 commits into
mainfrom
feat/trading212-exchange
May 11, 2026
Merged

feat(exchange): add Trading212 broker integration#1073
bennycode merged 37 commits into
mainfrom
feat/trading212-exchange

Conversation

@bennycode
Copy link
Copy Markdown
Owner

@bennycode bennycode commented May 6, 2026

Summary

This PR adds Trading212 as a second broker integration and reshapes the package's abstractions around the lessons learned. Highlights:

  • Trading212 broker (Trading212Broker) under packages/exchange/src/broker/trading212/, built the same way as Alpaca: zod-validated REST client (axios + axios-retry with a per-endpoint rate-limit table calibrated to Trading212's documented limits), a neutral mapper, and a broker class.
  • Execution / market-data split: the old Exchange class is renamed to Broker (orders, balances, fills, order-stream watchers), and a new abstract MarketDataSource owns candle methods. Brokers without their own candles (Trading212) take a mandatory marketData: MarketDataSource and delegate. AlpacaMarketData is extracted from AlpacaBroker so the same data source can be paired with any broker.
  • Polling fallbacks for missing APIs: Trading212 has no order-stream WebSocket, so watchOrders polls /api/v0/equity/history/orders with cursor pagination (60s default, matching the documented 1 req / 60s rate limit). Candle methods delegate to the injected MarketDataSource.
  • 24/5 orders: every Trading212 order is tagged extendedHours: true so submissions outside NASDAQ regular hours route through the overnight venue instead of being silently cancelled.
  • Currency-conversion fees exposed: FeeRate.CURRENCY_CONVERSION_FEE is populated for Trading212 cross-currency accounts, and Broker.estimateFee() returns {commission, currencyConversion, total, feeAsset} so strategies can subtract round-trip costs before entering.
  • Domain types stripped of the Exchange prefix: Candle, Fill, PendingOrder, OrderSide, OrderType, FeeRate, TradingRules, etc. Alpaca's vendor enums get an Alpaca prefix to avoid name collisions with the neutral types.
  • RVOL indicator added to trading-signals (current value / SMA of the prior period values), matching the standard public definition.
  • Documentation: BROKER_TEMPLATE.md codifies the recipe (REST tier, schema validation, mapper invariants, streaming tier, the neutral base, code organisation); the package README documents the Trading212 + Alpaca pairing with a runnable code example.

Pair convention

pair.base is the Trading212 vendor ticker (e.g. "AAPL_US_EQ"); pair.counter is the instrument's currencyCode (e.g. "USD", "EUR"). getTradingRules validates the pairing against /equity/metadata/instruments. When delegating to a MarketDataSource, Trading212Broker.toMarketDataPair strips the _<COUNTRY>_<TYPE> suffix and joins remaining intra-symbol underscores with dots (BRK_B_US_EQBRK.B), matching how US data providers render class shares.

Notes

  • Smoke-tested live end-to-end on a Trading212 paper account fed by Alpaca live market data: BUY/SELL fills resolve via getFillByOrderId, take-profit triggers off the actual broker fill price, currency-conversion fees are surfaced in estimateFee().
  • HistoryOrderSchema was rewritten after the live test exposed that the response is nested {order, fill}, not flat — getFills() was returning empty before that fix.
  • Five rounds of GitHub Copilot review have been resolved; all 25 review threads closed. CI passes across the five workspace packages with 100% coverage on trading-signals.

bennycode added 2 commits May 6, 2026 17:44
Adds a Trading212 exchange implementation following the same layered
recipe as Alpaca: zod-validated REST client (axios + axios-retry with
per-endpoint rate-limit table), neutral mapper, and an Exchange subclass.
Trading212 has no historical-bar endpoint and no public WebSocket, so
streaming and candle methods throw with a clear message.

Also documents the cross-broker architectural style in BROKER_TEMPLATE.md
and links it from the package CLAUDE.md so future broker integrations
have a written template to follow.
bennycode added 4 commits May 6, 2026 18:46
- Switch Trading212 auth from raw API key to `Basic base64(key:secret)`,
  matching the current vendor scheme. Adds `apiSecret` parameter through
  Trading212API/Trading212Exchange/getTrading212Client and updates the
  factory + .env.defaults (`TRADING212_PAPER_API_SECRET`,
  `TRADING212_LIVE_API_SECRET`).
- Use `DAY` instead of `GTC` for limit orders. Trading212 rejects GTC
  for stock limit orders ("Invalid payload"); DAY works on both paper
  and live.
- Loosen response schemas (`.nullish()`) for fields that are sometimes
  absent on paper responses: most of OrderSchema, several InstrumentSchema
  fields, almost everything in HistoryOrderSchema. Mapper and getFills
  now defend against missing values.
- Add `demo/testAll.ts` end-to-end script exercising every public method.
- Default `TRADING212_USE_PAPER=true` in `.env.defaults` (matches
  `ALPACA_USE_PAPER=true`); previous `false` made `npm run trading212:buy`
  fire on live as soon as someone populated `TRADING212_LIVE_API_KEY`.
- Match `HISTORY_ORDERS` retry delay with `startsWith` so paginated
  cursor URLs (`?cursor=…`) hit the calibrated 60s wait instead of
  busy-looping the 1s default under Trading212's 1 req / 60s ceiling.
- `toExchangePendingOrder`: wrap size in `Math.abs` so SELL orders
  don't return a negative `size` (the SELL is already encoded in
  `options.side`); aligns with `toOpenOrder` / `toFilledOrder`.
- `toOpenOrder` / `toFilledOrder`: fall back to `value` /
  `orderedValue` / `filledValue` when QUANTITY-strategy fields are
  null, and derive side from whichever signed source is present.
  Without this, VALUE-strategy orders placed via the Trading212 app
  (e.g. "sell EUR 100 of AAPL") get mis-classified as 0-size BUYs.
- `getOpenOrders`: drop `STOP` / `STOP_LIMIT` orders. The neutral
  `ExchangeOrderType` only models MARKET and LIMIT, so mapping these
  would silently lose `stopPrice` and lie about the type.
- `getFills`: pass the account `currencyCode` (from `getAccountInfo`)
  to the mapper. Trading212 debits FX/duty fees in the account
  currency, not the instrument currency, so labelling `feeAsset`
  with `pair.counter` corrupts P&L for cross-currency accounts.
…R_TEMPLATE

Lift broker-agnostic lessons from the Trading212 review into the template
so future integrations inherit them: cursor-URL matching for per-endpoint
retry-delay tables, safe env defaults, and a new Mapper invariants
subsection (unsigned size, multi-representation fallback, filter-don't-coerce,
fee asset = account currency).
@bennycode bennycode review requested due to automatic review settings May 7, 2026 21:30
Introduces MarketDataSource as a separate abstraction from Exchange so
brokers without candle endpoints don't have to throw on half their
interface. Exchange now covers brokerage only (orders, fills, balances,
order-stream watchers); candle methods (getCandles, getLatestCandle,
watchCandles, unwatchCandles) live on MarketDataSource.

- AlpacaExchange extends Exchange implements MarketDataSource — exposes
  both, with the implements clause guarding signature drift.
- Trading212Exchange accepts an optional marketData?: MarketDataSource
  in its constructor and delegates candle calls to it. Stripping the
  vendor ticker suffix (AAPL_US_EQ -> AAPL) happens inside the broker
  so the data source sees clean symbols. Lifecycle of marketData is
  owned by the caller; broker disconnect() does not close it.
- Adds TwelveDataMarketData using the official @twelvedata SDK
  (REST-only v1; polling-based watchCandles).
- Adds polling-based watchOrders to Trading212Exchange (tails
  /equity/history/orders with a cursor; 60s default to match the rate
  limit) and a getHistoryOrdersPage single-page helper.
- Demo cleanup: getDemoClient() factors env loading + assert.ok across
  all three Trading212 demo scripts; ExchangeMock return types collapse
  to inference where possible.
- BROKER_TEMPLATE.md documents the split and the delegation pattern.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 31 changed files in this pull request and generated 6 comments.

Comment thread packages/exchange/src/broker/Broker.ts
Comment thread packages/exchange/src/index.ts
Comment thread packages/exchange/src/exchange/twelvedata/getTwelveDataMarketData.ts Outdated
Comment thread packages/exchange/src/exchange/twelvedata/TwelveDataMarketData.ts Outdated
Comment thread packages/exchange/src/broker/trading212/Trading212Broker.ts
Comment thread packages/exchange/src/exchange/twelvedata/getTwelveDataMarketData.ts Outdated
bennycode added 7 commits May 8, 2026 00:22
- Re-export `MarketDataSource` from the package barrel so consumers can
  import / extend it via `@typedtrader/exchange`.
- `Trading212Exchange implements MarketDataSource` (it already has the
  delegating methods), so `getExchangeClient` now returns
  `Exchange & MarketDataSource` and existing in-repo callers
  (WatchMonitor, /price, /candle, watchAdd, etc.) keep compiling.
- Replace `setInterval` polling loops with self-rescheduling
  `setTimeout` chains in `TwelveDataMarketData.watchCandles` and
  `Trading212Exchange.watchOrders` so a slow tick (network stall,
  rate-limit retry) cannot overlap with the next request.
- Move SIGINT registration in `getTwelveDataMarketData` to after the
  instance is created so the handler doesn't reference an
  uninitialised binding.
AlpacaExchange already implements MarketDataSource and streams 1-minute
bars over WebSocket, which is the better default than polling Twelve
Data REST. Removing the placeholder integration avoids carrying an
unused SDK dependency and a paywall-prone fallback path.

- Delete src/exchange/twelvedata/ and the @twelvedata/twelvedata-node
  dependency.
- Drop TWELVEDATA_API_KEY from .env.defaults and the twelvedata:test
  npm script.
- Update BROKER_TEMPLATE.md so the reference pairing for "broker without
  market data" is Trading212 + Alpaca instead of Trading212 + Twelve Data.
…uire marketData on every broker

The optional `marketData?: MarketDataSource` slot on Trading212Exchange
was a polite lie — it let the candle methods exist as throws when no
source was wired, which is exactly the dishonest interface the split
was supposed to eliminate. Make it mandatory on every broker, and pull
Alpaca's market-data layer into its own class so the contract holds
symmetrically across integrations.

- New `AlpacaMarketData extends MarketDataSource`: owns the WebSocket
  bar subscriptions (CandleBatcher, alpacaWebSocket subscriptions) and
  the historical-bar REST endpoints. Independently usable as a data
  source for any broker.
- New `getAlpacaMarketData` composition root for standalone use.
- Shared `alpacaSymbol.ts` helpers (`createAlpacaSymbol`,
  `isAlpacaCryptoSymbol`) since both broker and data class need them.
- `AlpacaExchange` drops the in-class candle code; constructor now
  takes a mandatory `marketData: MarketDataSource` and delegates.
  `getAlpacaClient` constructs an `AlpacaMarketData` from the same
  credentials when none is passed in, so the caller-facing API for
  the all-in-one Alpaca case is unchanged.
- `Trading212Exchange.marketData` is now non-optional; conditional
  throws are gone. `getTrading212Client` requires `marketData`.
- `getExchangeClient` accepts `options?.marketData`. For Alpaca it's
  optional (default = AlpacaMarketData with same creds). For
  Trading212 it's required and throws a clear error if missing.
- BROKER_TEMPLATE.md updated to document the mandatory-injection rule
  and the lifecycle ownership distinction.
The Exchange class is behaviourally a broker integration — it places
orders, holds balances, lists fills. NASDAQ doesn't have listBalances().
The "exchange" name was inherited from crypto-CEX terminology where the
matching engine and the brokerage are the same firm; for traditional
equities they're separate. Rename the abstraction to match.

- Class renames: Exchange → Broker, AlpacaExchange → AlpacaBroker,
  Trading212Exchange → Trading212Broker, ExchangeMock → BrokerMock,
  AlpacaExchangeMock → AlpacaBrokerMock.
- Function rename: getExchangeClient → getBrokerClient.
- File renames matching the class names (git mv preserves history).
- Domain-type prefixes (ExchangeBalance, ExchangeFill, ExchangeCandle,
  ExchangeOrderSide, etc.) are intentionally left untouched — they're
  separate from the class abstraction and a wider rename can follow.
- Folder layout (`src/exchange/`) and npm package
  (`@typedtrader/exchange`) keep their names — registry-level changes
  are out of scope for this refactor.
- Updates messaging, trading-strategies, and trading-signals-docs
  consumers; full lerna test:types and test:units pass across all
  five workspace packages.
Now that the abstraction is called Broker, the Exchange* type prefixes
on broker-neutral domain types read as residual cruft. Strip them.

Renamed (with all consumers updated):
- ExchangeBalance               -> Balance
- ExchangeFill                  -> Fill
- ExchangeCandle                -> Candle
- ExchangeCandleBase            -> CandleBase
- ExchangeCandleSchema          -> CandleSchema
- ExchangeCandleBaseSchema      -> CandleBaseSchema
- ExchangeCandleImportRequest   -> CandleImportRequest
- ExchangeOrderSide             -> OrderSide
- ExchangeOrderType             -> OrderType
- ExchangeOrderPosition         -> OrderPosition
- ExchangeOrderOptions          -> OrderOptions
- ExchangeMarketOrderOptions    -> MarketOrderOptions
- ExchangeLimitOrderOptions     -> LimitOrderOptions
- ExchangeOrderBase             -> OrderBase
- ExchangePendingOrder          -> PendingOrder
- ExchangePendingMarketOrder    -> PendingMarketOrder
- ExchangePendingLimitOrder     -> PendingLimitOrder
- ExchangeFeeRate               -> FeeRate
- ExchangeTradingRules          -> TradingRules

To free the OrderSide / OrderType / OrderStatus / AssetClass names,
Alpaca's wire-format enums (previously bare) are now Alpaca-prefixed,
matching the existing Trading212-prefix convention:
- OrderSide   (Alpaca wire) -> AlpacaOrderSide
- OrderType   (Alpaca wire) -> AlpacaOrderType
- OrderStatus (Alpaca wire) -> AlpacaOrderStatus
- AssetClass  (Alpaca wire) -> AlpacaAssetClass

Full lerna test:types and test:units pass across all five workspace
packages (1,022 tests).
Aligns the folder name with the abstraction it contains. The npm
package name (@typedtrader/exchange) is intentionally left alone —
that's a registry-level rename and out of scope here.

- git mv preserves history for every file in the tree.
- Internal import paths (./exchange/, ../exchange/, ../../exchange/)
  rewritten to ./broker/, etc.
- src/index.ts barrel re-exports updated.
- External consumers are unaffected: they import from
  '@typedtrader/exchange', not from internal paths.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 100 out of 114 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (1)

packages/exchange/src/broker/Broker.ts:211

  • The static helper is still named toExchangeOrderSide, but the public API has been renamed to Broker/OrderSide. Consider renaming this to something like toOrderSide/inferOrderSide to avoid mixing the old “Exchange” terminology into the new broker domain model.

Comment thread packages/exchange/package.json Outdated
Comment thread packages/exchange/src/broker/trading212/demo/testAll.ts Outdated
Comment thread packages/exchange/src/broker/trading212/Trading212Broker.ts
Comment thread packages/exchange/src/broker/trading212/Trading212Broker.ts
Comment thread packages/exchange/src/broker/trading212/Trading212ExchangeMapper.ts Outdated
Comment thread packages/exchange/src/broker/alpaca/getAlpacaClient.ts Outdated
Comment thread packages/exchange/src/trader/TradingSession.test.ts
Comment thread packages/exchange/BROKER_TEMPLATE.md
- packages/exchange/package.json: npm scripts pointed at the now-gone
  src/exchange/... paths after the folder rename. Update to src/broker/.
- Trading212Broker.toMarketDataPair: split('_')[0] truncated tickers
  containing underscores (e.g. BRK_B_US_EQ -> BRK). Strip only the last
  two segments and join the rest with dots, so BRK_B_US_EQ -> BRK.B,
  matching how US data providers render class shares.
- Trading212ExchangeMapper.toExchangePendingOrder: throw on a LIMIT
  order with null limitPrice instead of stringifying "null"/"undefined"
  into the neutral order.
- testAll.ts: candle methods no longer throw on Trading212Broker (they
  delegate to the wired-in marketData), so expectThrow on them was
  misleading. Replace with success-path section calls; drop the now-
  unused expectThrow helper.
- getAlpacaClient: registered a new SIGINT listener on every call.
  Move the handler to a module-level guard that closes every tracked
  broker (and any market-data source it owns) once.
- BROKER_TEMPLATE.md: stale references to src/exchange/alpaca/ and
  Exchange.ts updated to src/broker/alpaca/ and Broker.ts.
- TradingSession.test.ts: fix indentation on a misaligned it() block.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 100 out of 114 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (3)

packages/exchange/src/broker/Broker.ts:118

  • ExchangePendingOrderBase naming looks left over from the pre-rename API (everything else is now Broker/Pending*). Renaming this base interface (e.g. PendingOrderBase) would avoid confusion and keep the public surface consistent.
    packages/exchange/src/broker/Broker.ts:168
  • ExchangeAvailableBalance is still named with the old Exchange* prefix even though the rest of the file exports Broker/Balance/TradingRules. Consider renaming to something like AvailableBalance (and aligning downstream imports) to keep the domain model consistent.
    packages/exchange/src/broker/Broker.ts:204
  • Broker.toExchangeOrderSide() appears to be an old name carried over from the Exchange API. Renaming it (e.g. Broker.toOrderSide() / toOrderSideFromBalances()) would better reflect the new Broker terminology and avoid implying an Exchange type still exists.

Comment thread packages/exchange/src/broker/trading212/Trading212Broker.ts
Comment thread packages/exchange/src/broker/trading212/Trading212Broker.ts Outdated
Comment thread packages/exchange/BROKER_TEMPLATE.md Outdated
Comment thread packages/exchange/src/broker/trading212/Trading212BrokerMapper.ts
Comment thread packages/exchange/src/broker/trading212/Trading212Broker.ts
bennycode added 2 commits May 8, 2026 13:59
- Trading212Broker.getTradingRules: base_min_size fell back to '0',
  letting computed sizes of zero pass TradingSession's min-size guard.
  Use the same non-zero default as base_increment.
- Trading212Broker.watchOrders: only the first 50-row history page was
  scanned per tick, so any burst >50 fills would silently lose the
  older ones. Page through nextPagePath until we hit lastSeenId.
- Trading212API.getHistoryOrdersPage: accept an optional nextPath so
  callers can continue paging without hand-rolling the pagination.
- Trading212BrokerMapper.toOpenOrder: same nullish-limitPrice guard
  as toPendingOrder — schema allows null, was producing
  price: "null"/"undefined" on the neutral order.
- Rename remaining Exchange-prefixed mapper artifacts to match the
  Broker terminology used everywhere else:
  AlpacaExchangeMapper -> AlpacaBrokerMapper,
  Trading212ExchangeMapper -> Trading212BrokerMapper,
  toExchangeCandle -> toCandle,
  toExchangePendingOrder -> toPendingOrder.
- BROKER_TEMPLATE.md: update remaining "Exchange*" references to
  "Broker*" / domain-type names so new integrations don't copy stale
  APIs (XxxExchangeMapper -> XxxBrokerMapper, getExchangeClient ->
  getBrokerClient, ExchangeCandle -> Candle, etc.).

Copilot also flagged "no Trading212Broker.test.ts yet" — left for a
follow-up; the current behaviour is exercised by the live demo script.
…, fill}

Trading212's /equity/history/orders returns each item as a nested
{order, fill} pair (status/side on order; price/timestamp/walletImpact
on fill). Our HistoryOrderSchema treated those fields as top-level, so
all reads were silently undefined — getFills() and getFillByOrderId()
always returned 0 rows / undefined, even when fills had cleared.

This was masked because z.looseObject() lets the parse succeed with
unknown fields. Caught while validating real fill prices on a paper
strategy run.

- HistoryOrderSchema: model the nested {order, fill} shape, including
  fill.walletImpact (currency, fxRate, netValue, realisedProfitLoss,
  taxes[]).
- Trading212BrokerMapper.toFilledOrder: read price/quantity/timestamp
  from fill, side from sign of fill.quantity, and fee from
  fill.walletImpact.taxes (using Math.abs because Trading212 reports
  fees as negative deltas).
- Trading212Broker.getFills + watchOrders: filter on
  item.order.status / item.order.id and pass the nested item to the
  mapper.

Live-verified end-to-end: getFillByOrderId now returns the real
$446.51 buy / $448.30 sell prices with the €0.57 FX-conversion fee.

Plus two small unrelated tidy-ups noticed along the way:
- MultiIndicatorConfluenceSchema extracted into its own file.
- MultiIndicatorConfluenceStrategy's getResult()! non-null assertions
  replaced with getResultOrThrow() (the canonical accessor).
- New .claude/rules/no-non-null-assertion-on-getResult.md captures
  the rule for future work.
bennycode added 4 commits May 9, 2026 12:15
… non-standard wrappers

The previous RVOL was a session-aware cumulative-vs-historical-curve
indicator (Bloomberg / ThinkOrSwim style) — sophisticated but not the
formula most retail sources describe. The simpler VolumeRatio matched
the Aron Groups (and most online) definition: current / SMA(prior N).
BarRange was a custom wrapper that has no standard public name (ATR
covers the same use case).

Following established naming and implementations:
- Delete the cumulative session-aware RVOL (had no canonical public
  formula to validate against).
- Rename VolumeRatio -> RVOL (the standard public name for this
  formula).
- Fix the formula to exclude the current input from the baseline:
  RVOL = current / mean(last `period` prior values), matching the
  Aron Groups worked example exactly (10-day avg 200K, today 300K =>
  RVOL 1.5). Tests include the worked example as a fixture.
- Delete BarRange (no standard public name; ATR is the established
  equivalent).
- Drop the runRvolDemo script that depended on the session-aware API.

@see https://arongroups.co/technical-analyze/relative-volume-indicator/
v8 coverage's parser (rolldown) was choking on TS syntax inside
fetchCandles.ts (`as StringValue`, `values.from!`) when trying to
instrument it for coverage. Those files are CLI scripts with top-level
await + process.exit, not library code under test, so excluding them
from coverage is the right fix anyway.

Adds src/broker/alpaca/demo.ts, src/broker/alpaca/fetchCandles.ts,
and src/broker/trading212/demo/** to the coverage exclude list.
… (CI fix)

Trading-signals enforces 100% coverage. The new RVOL had two
uncovered branches:
- the throw on `replace()` before any input has been added,
- the trim path that bounds the priorVolumes buffer to ~2× period.

Add tests for both (the trim path also doubles as a check that the
sliding-window math stays correct as the buffer wraps).

Plus codebase style cleanups while the file is open:
- drop the redundant `<number>` generic argument on IndicatorSeries
  (the default already covers it),
- drop the explicit `: number | null` return type on update() (let
  inference do the work),
- add blank lines around control statements,
- quote the variable in the "period must be >= 1" error message.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 111 out of 124 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

packages/exchange/src/broker/alpaca/AlpacaWebSocket.ts:94

  • process.exit(1) on WebSocket close bypasses any SIGTERM/SIGINT-based graceful shutdown handlers (log flushing, disconnects, etc.). If the intention is to let an orchestrator restart the worker after cleanup, prefer process.kill(process.pid, 'SIGTERM') (or emit an error and let the application decide/reconnect) rather than a hard exit from inside a library component.

Comment thread packages/trading-signals/src/volume/RVOL/RVOL.ts
Comment thread packages/trading-strategies/src/start/runStrategy.ts
bennycode added 3 commits May 11, 2026 09:14
…53b432

Six findings across two Copilot review passes.

Trading212 mapper / broker:
- Trading212BrokerMapper.toPendingOrder + toOpenOrder no longer fall
  back to order.value when order.quantity is null. `value` is notional
  (currency), not base-asset quantity, so the fallback was corrupting
  the neutral `PendingOrder.size` (interpreted as base units) for
  VALUE-strategy orders. Both mappers now throw if quantity is null;
  callers are expected to filter.
- Trading212Broker.getOpenOrders adds an `order.quantity != null`
  filter so VALUE-strategy orders (placed via the Trading212 app)
  are dropped before mapping, alongside the existing STOP/STOP_LIMIT
  filter.

CandleBatcher rename:
- toExchangeCandles -> toCandles to match the post-Broker terminology.
  Local consumer variables (`exchangeCandles`) renamed too, choosing
  names that actually describe what they hold:
  - ScalpStrategy / ScalpScannerReport: result of `toCandles()` is
    plain (un-batched) `Candle[]`, so renamed to `plainCandles`.
  - runBacktest: result of `toBatchedCandles()` is genuinely batched,
    so the original `batchedCandles` is correct.

RVOL.replace() rewind:
- Copilot caught that the snapshot saved `previousResult` from before
  the most recent add, but `setResult(_, replace=true)` deliberately
  leaves `previousResult` untouched — so after more than one update,
  the restored value was too far back. Fix: snapshot the *current*
  `result` instead; on restore, set `previousResult` to that value.
  Then setResult(_, true) overwrites only `result` and the rewind
  matches what a fresh `add(replacement)` would have produced. New
  test asserts that equivalence directly.

runStrategy.ts:
- Replace raw `'SELL'` literal with `OrderSide.SELL` to stay typed.
Adds a Trading212 section to the package README. Highlights the key
limitation (no historical bars, no public WebSocket) and shows the
broker-delegation pattern using Alpaca as the market-data source.

Reframes the Design Decisions section from a list of "no X" constraints
into a positive description of the extension model:
- bring your own broker,
- extend Broker → get TradingSession + BacktestExecutor for free,
- extend MarketDataSource → live streaming; or plug in an existing
  source like Alpaca when your broker doesn't have a data API.

Drive-by: Exchange → Broker terminology fixed in the existing line
that introduces the design-decisions section.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 114 out of 127 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

packages/exchange/src/broker/alpaca/AlpacaWebSocket.ts:94

  • stream.once('close', ...) calls process.exit(1) from inside the exchange library. This bypasses the host application's graceful shutdown path (SIGTERM/SIGINT handlers, log flushing, cleanup) and makes the package unsafe to embed (e.g. in the messaging server). Prefer emitting an error/event up to the caller, or keep the previous behavior of signaling the process (e.g. SIGTERM) so the app can run its centralized shutdown routine.

Comment thread packages/exchange/src/broker/alpaca/AlpacaMarketData.ts
Comment thread packages/exchange/README.md Outdated
Comment thread packages/exchange/README.md
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…ldest); fix README

Copilot review on f782d8e/892904a flagged two bugs and one PR-description
mismatch (the latter already updated).

- AlpacaMarketData.getLatestCandle returned `candles[0]` after fetching a
  multi-bar range, which is the *oldest* candle in chronological order,
  not the latest. Sort by openTimeInMillis and return the most recent
  one. Also rewords the inline comment to describe what actually happens
  (server-side aggregation through getCandles, alignment to the latest
  1-min bar's timestamp), not the inaccurate "aggregate locally" claim.
  Adds an explicit empty-result guard so an unexpected empty response
  throws a clear error instead of returning undefined cast as Candle.
- README Trading212 snippet had a missing closing quote
  (`apiKey: 'ALPACA_API_KEY,`), so anyone copy-pasting got a syntax error.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 114 out of 127 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

packages/exchange/src/broker/alpaca/AlpacaWebSocket.ts:94

  • process.exit(1) inside the exchange library will hard-kill any host process on transient WebSocket drops, bypassing the app’s own shutdown / log-flush path and making this package unsafe to embed (e.g., in long-running servers). Prefer emitting an error / closing the connection and letting the application decide whether to terminate (or reintroduce the previous SIGTERM-based approach so a centralized handler can run).

Comment thread packages/exchange/src/broker/trading212/Trading212Broker.ts
Comment thread packages/trading-signals/src/volume/RVOL/RVOL.ts
…OL stale result

Copilot review on 3a816f5 flagged two real correctness bugs.

- Trading212Broker.disconnect() stopped order watchers but left every
  active candle subscription wired through `marketData`. Forwarders
  stayed registered and the broker kept emitting after the caller
  thought it was shut down. disconnect() now iterates the active
  candle topics and calls unwatchCandles() for each (which removes
  the forwarder, deletes it from the map, and unsubscribes from the
  marketData topic). The injected marketData source itself is
  intentionally not disconnected here — its lifecycle is owned by
  the caller.

- RVOL.update() returned null when baseline === 0 but left this.result
  pointing at the previous (now-stale) value, so isStable kept lying
  and getResultOrThrow() returned a stale ratio that the caller had
  no way to detect. Clear this.result on baseline-zero so isStable
  and getResultOrThrow() honestly reflect "no valid result". Adds a
  regression test that drives the indicator into a previously-stable
  state, then forces baseline to zero, and asserts isStable flips
  back to false and getResultOrThrow() throws.

Direct assignment to this.result (rather than going through setResult)
is the documented exception for the "clear state" case — setResult
only accepts concrete numbers, and the CLAUDE.md rule guards against
breaking signal tracking for value updates, which is not what we're
doing here. Comment in the code explains.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 114 out of 127 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

packages/exchange/src/broker/alpaca/AlpacaWebSocket.ts:93

  • stream.once('close', ...) calling process.exit(1) from within the exchange/broker library will hard-terminate any consuming application and bypass its graceful shutdown handlers (SIGTERM/SIGINT, log flushing, etc.). Prefer emitting an error / marking the connection unhealthy and letting the host process decide (or triggering a signal like SIGTERM) rather than exiting directly from this dependency.

Comment on lines +6 to +12
const marketData = new AlpacaMarketData(options);

process.on('SIGINT', () => {
console.log('Received signal interrupt...');
marketData.disconnect();
process.exit(0);
});
Comment on lines +125 to +128
const signedQty = fill.quantity ?? order.quantity ?? order.filledQuantity ?? 0;
const side = signedQty < 0 ? OrderSide.SELL : OrderSide.BUY;
const fee = (fill.walletImpact?.taxes ?? []).reduce((sum, tax) => sum + Math.abs(tax.quantity ?? 0), 0);

Comment on lines +129 to +133
return {
created_at: fill.filledAt ?? order.createdAt ?? '',
fee: `${fee}`,
feeAsset: accountCurrency,
order_id: `${order.id}`,
Comment on lines +5 to 8
candles: Candle[];
/** The mock exchange instance that handles order matching, balance tracking, and fee calculation. */
exchange: ExchangeMock;
broker: BrokerMock;
/** The strategy instance to backtest. */
@bennycode bennycode enabled auto-merge (squash) May 11, 2026 21:47
@bennycode bennycode merged commit 9f8bfe9 into main May 11, 2026
6 checks passed
@bennycode bennycode deleted the feat/trading212-exchange branch May 11, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants