Skip to content

test(exchange): cover the Trading212 stack and add a cross-broker contract suite#1181

Open
bennycode wants to merge 2 commits into
mainfrom
test/trading212-and-broker-contract
Open

test(exchange): cover the Trading212 stack and add a cross-broker contract suite#1181
bennycode wants to merge 2 commits into
mainfrom
test/trading212-and-broker-contract

Conversation

@bennycode

Copy link
Copy Markdown
Owner

Summary

Tests only — no production code changes. Covers the previously untested Trading212 stack and adds a cross-broker contract suite so shared Broker semantics (and the intentional divergences) are pinned in one place.

  • Trading212BrokerMapper.test.ts — vendor-ticker → market-data pair conversion (AAPL_US_EQAAPL, BRK_B_US_EQBRK.B), sign-encoded quantity mapping (negative = SELL, neutral size unsigned), pending-order mapping, and fill mapping incl. tax-line fee summation in the account currency.
  • Trading212Broker.test.ts (mocked Trading212API + injected MarketDataSource) — placeLimitOrder (DAY + extendedHours: true, never GTC), placeMarketOrder (signed quantity), notional (sizeInCounter: true) rejection, getOpenOrders filtering of STOP/STOP_LIMIT/VALUE orders, fee logic (0% commission, CURRENCY_CONVERSION_FEE only on cross-currency instruments, getFeeAsset = account currency), and the polling watchOrders under fake timers: baseline snapshot (no replay of historical fills), pagination across multiple pages in a single tick, unwatchOrders stopping the timer, and API failures emitting 'error' while polling continues.
  • Trading212API.test.ts — per-endpoint retry delays from getRetryDelay verified end-to-end via an injected axios adapter under fake timers (2s cash / 5s orders & portfolio / 30s account info / 50s instruments / 60s history incl. paginated nextPath URLs with query strings / 1s single-order GET / linear default for non-GET order requests), Basic-auth header construction from apiKey:apiSecret, demo/live base-URL selection, and no-retry on HTTP 400.
  • brokerContract.test.ts — one parameterized suite run against both AlpacaBroker and Trading212Broker (APIs mocked): placeLimitOrder/placeMarketOrder return a consistently shaped PendingOrder, cancelOpenOrders resolves to the canceled ids, getFills is newest-first, estimateFee commission derives from getFeeRate, and watchOrders returns a topicId that emits a Fill. Legitimate venue divergences (Trading212 rejects notional orders; fee model & fee asset differ) are asserted explicitly in a dedicated block so this file is the single place divergences are visible.

Latent findings — documented here, deliberately NOT fixed in this PR

(Parallel PRs already address the Alpaca fill-fee mapping, the Trading212 getTime credential probe, and order-POST retry gating — this PR stays off those paths.)

  1. Trading212Broker.watchOrders can crash the process on a transient API failure. The poll tick emits 'error' on the broker's EventEmitter; per Node semantics, an 'error' event with no registered listener throws. Since it happens inside a timer callback, a consumer that never called broker.on('error', …) gets an uncaught exception on the first failed poll. The tests register a listener; production consumers may not.
  2. watchOrders pages the entire order history on every tick when the baseline contains no FILLED order. lastSeenId starts at 0, and the pagination loop only stops early when it sees an id <= lastSeenId — which never happens for positive ids. Combined with the 60s-per-request rate limit on /equity/history/orders, a fill-less account makes each tick walk the full history.
  3. cancelOpenOrders / getOpenOrders asymmetry. getOpenOrders filters out STOP/STOP_LIMIT and VALUE-strategy orders (no neutral representation), but cancelOpenOrders cancels every order matching the ticker, including ones getOpenOrders claims don't exist. Possibly intended ("cancel everything for this pair"), but undocumented.
  4. Trading212BrokerMapper.toFilledOrder sums Math.abs of all tax lines and hardcodes position: LONG. A positive (credit/rebate) tax entry would inflate the fee instead of netting it, and SELL fills closing a short still report LONG (mirrors the documented Alpaca behavior).

Test plan

  • npm test in packages/exchange — 12 files, 147 tests passed (types + depcruise + vitest w/ coverage); 62 of those are new
  • npm run lint in packages/exchange — clean
  • No network: all HTTP mocked (API classes via vi.mock, axios adapter injection for the retry tests); polling and retry timing via vi.useFakeTimers

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR expands test coverage in packages/exchange by adding a full Trading212 test stack and a new parameterized “broker contract” suite that runs the same behavioral expectations against multiple broker implementations (currently Alpaca + Trading212), while explicitly pinning known venue divergences in one place.

Changes:

  • Added unit tests for Trading212BrokerMapper covering pending/open/filled order mapping and vendor-ticker → market-data ticker conversion.
  • Added integration-style (mocked API + injected market data) tests for Trading212Broker, including order placement rules, open-order filtering, fee estimation behavior, and watchOrders polling behavior under fake timers.
  • Added a cross-broker contract test suite to enforce shared Broker semantics across implementations and document intentional divergences.

Reviewed changes

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

File Description
packages/exchange/src/broker/trading212/Trading212BrokerMapper.test.ts New tests for Trading212 mapping logic (ticker normalization, pending/open/filled mapping).
packages/exchange/src/broker/trading212/Trading212Broker.test.ts New tests for Trading212 broker behaviors (order placement, fees, open orders, polling order watcher).
packages/exchange/src/broker/trading212/api/Trading212API.test.ts New end-to-end retry-delay calibration tests via injected axios adapter + fake timers, plus auth/baseURL checks.
packages/exchange/src/broker/brokerContract.test.ts New parameterized contract suite to pin common broker semantics and explicitly assert allowed divergences.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expect(mockMethods.getHistoryOrdersPage).toHaveBeenCalledTimes(1);
});

it('emits "error" instead of throwing and keeps polling after an API failure', async () => {

await vi.advanceTimersByTimeAsync(1_000);
await vi.advanceTimersByTimeAsync(0);
expect(errorHandler).toHaveBeenCalledWith(new Error('Trading212 is down'));
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