When to Use
Use this skill when connecting algorithmic trading systems to Saxo Bank using their multi-asset OpenAPI REST endpoints. Saxo Bank OpenAPI enables cross-asset trading across FX Spot, Global Stocks, Contract Futures, Options, and Stock CFDs. Unlike single-asset brokers, Saxo Bank identifies instruments via numeric Unique Instrument Codes (UICs) and requires explicit AssetType declarations on order payloads.
When NOT to Use
- As a token/OAuth manager. This skill consumes an already-valid
access_token. Saxo's authorization-code flow, the 20-minute access-token lifetime, and refresh rotation belong upstream — seeheadless-broker-auth-patternsandsecrets-rotation-without-bot-downtime. - As a streaming market-data feed. These are REST polling endpoints. Real-time prices and position updates use Saxo's WebSocket streaming gateway (
live-streaming.saxobank.com/sim-streaming.saxobank.com), which is a different transport with its own subscription lifecycle — seewebsocket-reconnection-with-state-recovery. - As an idempotency layer for order submission. Saxo does not deduplicate on
ExternalReference(see Pitfalls). Retry-safe submission isorder-placement-idempotency. - As the risk gate.
place_ordervalidates payload shape, not exposure. Pre-trade limits, drawdown halts, and kill switches must sit out-of-band — seekill-switch-and-drawdown-circuit-breakers. - For multi-leg option strategies. Saxo routes those through the dedicated multi-leg strategy endpoints, not the single-order payload this client builds.
Prerequisites
- Saxo OpenAPI OAuth2 Access Token (
access_token) and Account Key (account_key). - Target environment (
is_simulation: True forgateway.saxobank.com/sim/openapi, False for livegateway.saxobank.com/openapi). Application key/secret are not shared between the two environments, and the Developer Portal's 24-hour token authorizes simulation only. - An HTTP transport callable, injected as
http_fn(method, url, headers, body), returning(status_code, body)or(status_code, body, headers). Returning headers is what enables rate-limit back-off.
Workflow
- UIC Instrument Resolution:
- Query
/ref/v1/instruments?Keywords={ticker}&AssetTypes={asset_type}to resolve ticker symbols to Saxo numeric UICs. The UIC is theIdentifierfield of each instrument summary. - Keyword search is a fuzzy match, not a lookup. Confirm
Symbol,ExchangeIdandCurrencyCodeon the chosen row before routing — never takeData[0]on trust when several rows return.
- Query
- Multi-Asset Order Placement:
- Issue POST request to
/trade/v2/ordersspecifyingAccountKey,Uic,AssetType,BuySell,Amount,OrderType,OrderDuration, andManualOrder. - Set
ManualOrder: falsefor algorithmically generated orders; Saxo documents this field as mandatory for almost all applications. - Supply
OrderPricefor everyOrderTypeexceptMarket. Attach a randomExternalReference(≤ 50 chars) so an ambiguous submission can be reconciled later. - Treat the response as acceptance, not a fill: it returns
OrderIdand any relatedOrders, and carries no execution status. IfOrderIdis absent, the order state is UNKNOWN — reconcile, do not resubmit.
- Issue POST request to
- Position & P&L Retrieval:
- Query
/port/v1/positions?AccountKey={account_key}&FieldGroups=PositionBase,PositionView,DisplayAndFormat. - Read
PositionId/NetPositionIdfrom the row root, instrument fields fromPositionBase, valuation fromPositionView, andSymbol/CurrencyfromDisplayAndFormat. - Check
PositionView.CalculationReliabilitybefore trusting any valuation, and aggregate portfolio P&L onProfitLossOnTradeInBaseCurrency.
- Query
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Unresolved UIC Identifiers: Passing ticker string identifiers directly into order endpoints instead of performing UIC resolution first.
- Incorrect AssetType Enum Values: Passing string
"Equity"instead of Saxo's exact enum string"Stock"or"FxSpot". - Trading
OptionRootas an AssetType:OptionRootis an instrument-search concept used to enumerate a contract option space; it is not in Saxo's tradableAssetTypeenum and will not route. Resolve the contract's own UIC and trade it asStockOption,FuturesOption,StockIndexOption, orFxVanillaOption. - Assuming the 24-hour token behaves like a live session token: The Developer Portal's one-day token is authorized for the simulation environment only. Live trading uses the OAuth2 authorization-code flow, whose access token expires after 20 minutes; refresh on a timer, not after the first HTTP 401 lands mid-order-submission.
- Retrying an order because the HTTP request timed out: Saxo does not check
ExternalReferencefor uniqueness and will not reject a repeated one — it is a correlation tag, not an idempotency key. A blind retry places a second order. Query/port/v1/ordersfiltered on yourExternalReferencefirst, and remember that an empty result does not prove non-placement: that endpoint returns working orders only, so an order that already filled has left it. Check positions before concluding anything. - Reading
SymbolorPositionIdout ofPositionBase:PositionBasecontains neither.PositionIdandNetPositionIdsit at the top level of eachDatarow, andSymbolis only returned insideDisplayAndFormat— which Saxo omits unless you request thatFieldGroupsvalue. Code that misses this silently produces blank position identifiers, breaking any close-by-PositionIdlogic. - Summing
ProfitLossOnTradeacross a multi-currency book: That figure is denominated in the instrument's own currency. Adding USD and JPY P&L produces a meaningless number. AggregateProfitLossOnTradeInBaseCurrency, which Saxo has already converted to the account's base currency. - Ignoring
CalculationReliability:PositionViewvaluations carry a reliability marker. Sizing or de-risking off a valuation Saxo has not marked"Ok"propagates a stale or approximated price into risk decisions. - Treating a
Dataarray as the complete set: Saxo's collection endpoints are OData-paged ($top/$skip). A positions response whose__countexceeds the rows returned is a partial book — feeding it into an exposure or drawdown check silently understates risk. Page to exhaustion, or at minimum detect and refuse to act on a truncated result. - Treating HTTP 429 as a generic failure: Saxo rate-limits per dimension and returns
X-RateLimit-<dimension>-{Limit,Remaining,Reset}, whereResetis seconds until that quota resets. Back off using the exhausted dimension'sReset. Note the order-submission bucket is far tighter than the general session bucket — a burst loop will trip it.
Verification
- Instantiate
SaxoBankOpenAPIClient. Search instrument "EURUSD" (FxSpot) $\implies$ verify UIC 21 returned. Place limit buy order $\implies$ verifyOrderIdreturned andstatusisNone(Saxo returns no status on placement). Query positions $\implies$ verifyPositionIdread from the row root,SymbolfromDisplayAndFormat, and unrealized P&L parsed in both instrument and base currency. - Confirm a non-
Marketorder without a price raises before any HTTP call, and that HTTP 401/429 raiseSaxoAuthError/SaxoRateLimitErrorrespectively. - Run
python -m unittest discover -s skills/saxo-bank-openapi-integration/scripts.