From bcc16fe5c39b30bf70342c632e20d7549a410b22 Mon Sep 17 00:00:00 2001 From: ryanwelan Date: Wed, 8 Jul 2026 11:50:42 +0100 Subject: [PATCH 1/3] Add Trading212 plugin Monitors a Trading 212 Invest/Stocks ISA account: cash and account value, open positions, pies, pending orders, and order/dividend/transaction history, via the Trading 212 public API (beta). Co-Authored-By: Claude Sonnet 5 --- plugins/Trading212/v1/configValidation.json | 11 + plugins/Trading212/v1/custom_types.json | 23 ++ .../v1/dataStreams/accountSummary.json | 79 ++++ .../v1/dataStreams/dividendHistory.json | 74 ++++ .../v1/dataStreams/orderHistory.json | 178 +++++++++ .../v1/dataStreams/pendingOrders.json | 133 +++++++ .../Trading212/v1/dataStreams/pieDetail.json | 87 ++++ plugins/Trading212/v1/dataStreams/pies.json | 75 ++++ .../Trading212/v1/dataStreams/positions.json | 118 ++++++ .../v1/dataStreams/scripts/dividendHistory.js | 19 + .../v1/dataStreams/scripts/orderHistory.js | 63 +++ .../v1/dataStreams/scripts/pendingOrders.js | 29 ++ .../v1/dataStreams/scripts/pieDetail.js | 25 ++ .../v1/dataStreams/scripts/positions.js | 26 ++ .../v1/dataStreams/transactionHistory.json | 47 +++ .../v1/defaultContent/manifest.json | 7 + .../overviewDashboard.dash.json | 372 ++++++++++++++++++ .../v1/defaultContent/pieDashboard.dash.json | 198 ++++++++++ .../positionDashboard.dash.json | 354 +++++++++++++++++ .../Trading212/v1/defaultContent/scopes.json | 38 ++ plugins/Trading212/v1/docs/README.md | 44 +++ plugins/Trading212/v1/icon.svg | 6 + .../v1/indexDefinitions/default.json | 37 ++ plugins/Trading212/v1/metadata.json | 41 ++ plugins/Trading212/v1/ui.json | 27 ++ 25 files changed, 2111 insertions(+) create mode 100644 plugins/Trading212/v1/configValidation.json create mode 100644 plugins/Trading212/v1/custom_types.json create mode 100644 plugins/Trading212/v1/dataStreams/accountSummary.json create mode 100644 plugins/Trading212/v1/dataStreams/dividendHistory.json create mode 100644 plugins/Trading212/v1/dataStreams/orderHistory.json create mode 100644 plugins/Trading212/v1/dataStreams/pendingOrders.json create mode 100644 plugins/Trading212/v1/dataStreams/pieDetail.json create mode 100644 plugins/Trading212/v1/dataStreams/pies.json create mode 100644 plugins/Trading212/v1/dataStreams/positions.json create mode 100644 plugins/Trading212/v1/dataStreams/scripts/dividendHistory.js create mode 100644 plugins/Trading212/v1/dataStreams/scripts/orderHistory.js create mode 100644 plugins/Trading212/v1/dataStreams/scripts/pendingOrders.js create mode 100644 plugins/Trading212/v1/dataStreams/scripts/pieDetail.js create mode 100644 plugins/Trading212/v1/dataStreams/scripts/positions.js create mode 100644 plugins/Trading212/v1/dataStreams/transactionHistory.json create mode 100644 plugins/Trading212/v1/defaultContent/manifest.json create mode 100644 plugins/Trading212/v1/defaultContent/overviewDashboard.dash.json create mode 100644 plugins/Trading212/v1/defaultContent/pieDashboard.dash.json create mode 100644 plugins/Trading212/v1/defaultContent/positionDashboard.dash.json create mode 100644 plugins/Trading212/v1/defaultContent/scopes.json create mode 100644 plugins/Trading212/v1/docs/README.md create mode 100644 plugins/Trading212/v1/icon.svg create mode 100644 plugins/Trading212/v1/indexDefinitions/default.json create mode 100644 plugins/Trading212/v1/metadata.json create mode 100644 plugins/Trading212/v1/ui.json diff --git a/plugins/Trading212/v1/configValidation.json b/plugins/Trading212/v1/configValidation.json new file mode 100644 index 0000000..943444c --- /dev/null +++ b/plugins/Trading212/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { "name": "accountSummary" }, + "required": true, + "error": "Could not authenticate. Check your API key, API secret and environment (Live/Demo) are correct and that the key hasn't been revoked.", + "success": "Connected successfully." + } + ] +} diff --git a/plugins/Trading212/v1/custom_types.json b/plugins/Trading212/v1/custom_types.json new file mode 100644 index 0000000..b296074 --- /dev/null +++ b/plugins/Trading212/v1/custom_types.json @@ -0,0 +1,23 @@ +[ + { + "name": "Trading212 Account", + "sourceType": "Trading212 Account", + "icon": "wallet", + "singular": "Account", + "plural": "Accounts" + }, + { + "name": "Trading212 Position", + "sourceType": "Trading212 Position", + "icon": "chart-line", + "singular": "Position", + "plural": "Positions" + }, + { + "name": "Trading212 Pie", + "sourceType": "Trading212 Pie", + "icon": "chart-pie", + "singular": "Pie", + "plural": "Pies" + } +] diff --git a/plugins/Trading212/v1/dataStreams/accountSummary.json b/plugins/Trading212/v1/dataStreams/accountSummary.json new file mode 100644 index 0000000..53e9cc8 --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/accountSummary.json @@ -0,0 +1,79 @@ +{ + "name": "accountSummary", + "displayName": "Account Summary", + "description": "Cash balance, invested value and total account value", + "tags": ["Account"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "equity/account/summary", + "expandInnerObjects": true, + "paging": { "mode": "none" }, + "getArgs": [], + "headers": [] + }, + "matches": "none", + "metadata": [ + { + "name": "id", + "displayName": "Account ID", + "shape": "string", + "visible": false + }, + { + "name": "currency", + "displayName": "Currency", + "shape": "string" + }, + { + "name": "accountName", + "displayName": "Name", + "computed": true, + "valueExpression": "{{ 'Trading 212 (' + $['currency'] + ')' }}", + "shape": "string", + "role": "label" + }, + { + "name": "totalValue", + "displayName": "Total Value", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }], + "role": "value" + }, + { + "name": "cash.availableToTrade", + "displayName": "Cash Available", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "cash.reservedForOrders", + "displayName": "Cash Reserved for Orders", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "cash.inPies", + "displayName": "Cash in Pies", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "investments.currentValue", + "displayName": "Invested Value", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "investments.totalCost", + "displayName": "Total Cost", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "investments.realizedProfitLoss", + "displayName": "Realized P/L", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "investments.unrealizedProfitLoss", + "displayName": "Unrealized P/L", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + } + ], + "timeframes": false +} diff --git a/plugins/Trading212/v1/dataStreams/dividendHistory.json b/plugins/Trading212/v1/dataStreams/dividendHistory.json new file mode 100644 index 0000000..a3d3bc7 --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/dividendHistory.json @@ -0,0 +1,74 @@ +{ + "name": "dividendHistory", + "displayName": "Dividend History", + "description": "Most recent dividend payments paid out to your account, one row per payment", + "tags": ["Dividends"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "equity/history/dividends", + "paging": { "mode": "none" }, + "getArgs": [{ "key": "limit", "value": "50" }], + "headers": [], + "postRequestScript": "dividendHistory.js" + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "position", + "label": "Position (optional)", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Trading212 Position"] } + } + } + ], + "metadata": [ + { + "name": "ticker", + "displayName": "Ticker", + "shape": "string", + "visible": false + }, + { + "name": "instrumentName", + "displayName": "Instrument", + "shape": "string", + "role": "label" + }, + { + "name": "amount", + "displayName": "Amount", + "shape": ["number", { "decimalPlaces": 2 }], + "role": "value" + }, + { + "name": "grossAmountPerShare", + "displayName": "Gross Amount Per Share", + "shape": ["number", { "decimalPlaces": 4 }] + }, + { + "name": "quantity", + "displayName": "Quantity", + "shape": ["number", { "decimalPlaces": 4 }] + }, + { + "name": "type", + "displayName": "Type", + "shape": "string" + }, + { + "name": "paidOn", + "displayName": "Paid On", + "shape": "date", + "role": "timestamp" + }, + { + "name": "reference", + "displayName": "Reference", + "shape": "string", + "visible": false + } + ], + "timeframes": false +} diff --git a/plugins/Trading212/v1/dataStreams/orderHistory.json b/plugins/Trading212/v1/dataStreams/orderHistory.json new file mode 100644 index 0000000..0c424bd --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/orderHistory.json @@ -0,0 +1,178 @@ +{ + "name": "orderHistory", + "displayName": "Order History", + "description": "Most recent completed and cancelled equity orders, one row per order", + "tags": ["Orders"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "equity/history/orders", + "paging": { "mode": "none" }, + "getArgs": [{ "key": "limit", "value": "50" }], + "headers": [], + "postRequestScript": "orderHistory.js" + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "position", + "label": "Position (optional)", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Trading212 Position"] } + } + } + ], + "metadata": [ + { + "name": "id", + "displayName": "Order ID", + "shape": "string", + "visible": false + }, + { + "name": "ticker", + "displayName": "Ticker", + "shape": "string", + "visible": false + }, + { + "name": "name", + "displayName": "Instrument", + "shape": "string", + "role": "label" + }, + { + "name": "isin", + "displayName": "ISIN", + "shape": "string", + "visible": false + }, + { + "name": "instrumentCurrency", + "displayName": "Instrument Currency", + "shape": "string", + "visible": false + }, + { + "name": "type", + "displayName": "Order Type", + "shape": "string" + }, + { + "name": "strategy", + "displayName": "Strategy", + "shape": "string", + "visible": false + }, + { + "name": "side", + "displayName": "Side", + "shape": "string" + }, + { + "name": "status", + "displayName": "Status", + "shape": "string" + }, + { + "name": "orderedQuantity", + "displayName": "Ordered Quantity", + "shape": ["number", { "decimalPlaces": 4 }], + "visible": false + }, + { + "name": "filledQuantity", + "displayName": "Filled Quantity", + "shape": ["number", { "decimalPlaces": 4 }] + }, + { + "name": "orderedValue", + "displayName": "Ordered Value", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }], + "visible": false + }, + { + "name": "filledValue", + "displayName": "Filled Value", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }], + "role": "value" + }, + { + "name": "fillPrice", + "displayName": "Fill Price", + "shape": ["number", { "decimalPlaces": 2 }] + }, + { + "name": "limitPrice", + "displayName": "Limit Price", + "shape": ["number", { "decimalPlaces": 2 }], + "visible": false + }, + { + "name": "stopPrice", + "displayName": "Stop Price", + "shape": ["number", { "decimalPlaces": 2 }], + "visible": false + }, + { + "name": "accountCurrency", + "displayName": "Account Currency", + "shape": "string", + "visible": false + }, + { + "name": "realisedProfitLoss", + "displayName": "Realised P/L", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "fees", + "displayName": "Fees", + "shape": ["number", { "decimalPlaces": 2 }], + "visible": false + }, + { + "name": "fxRate", + "displayName": "FX Rate", + "shape": ["number", { "decimalPlaces": 4 }], + "visible": false + }, + { + "name": "tradingMethod", + "displayName": "Trading Method", + "shape": "string", + "visible": false + }, + { + "name": "extendedHours", + "displayName": "Extended Hours", + "shape": "boolean", + "visible": false + }, + { + "name": "initiatedFrom", + "displayName": "Initiated From", + "shape": "string", + "visible": false + }, + { + "name": "dateCreated", + "displayName": "Created", + "shape": "date", + "visible": false + }, + { + "name": "dateExecuted", + "displayName": "Executed", + "shape": "date", + "role": "timestamp" + }, + { + "sourceId": "ticker", + "sourceType": "Trading212 Position", + "name": "name" + } + ], + "timeframes": false +} diff --git a/plugins/Trading212/v1/dataStreams/pendingOrders.json b/plugins/Trading212/v1/dataStreams/pendingOrders.json new file mode 100644 index 0000000..a437430 --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/pendingOrders.json @@ -0,0 +1,133 @@ +{ + "name": "pendingOrders", + "displayName": "Pending Orders", + "description": "Currently active or pending equity orders that have not yet been filled", + "tags": ["Orders"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "equity/orders", + "paging": { "mode": "none" }, + "getArgs": [], + "headers": [], + "postRequestScript": "pendingOrders.js" + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "position", + "label": "Position (optional)", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Trading212 Position"] } + } + } + ], + "metadata": [ + { + "name": "id", + "displayName": "Order ID", + "shape": ["number", { "decimalPlaces": 0 }], + "role": "id" + }, + { + "name": "ticker", + "displayName": "Ticker", + "shape": "string", + "visible": false + }, + { + "name": "name", + "displayName": "Instrument", + "shape": "string", + "role": "label" + }, + { + "name": "isin", + "displayName": "ISIN", + "shape": "string", + "visible": false + }, + { + "name": "instrumentCurrency", + "displayName": "Instrument Currency", + "shape": "string" + }, + { + "name": "type", + "displayName": "Order Type", + "shape": "string" + }, + { + "name": "side", + "displayName": "Side", + "shape": "string" + }, + { + "name": "status", + "displayName": "Status", + "shape": "string" + }, + { + "name": "quantity", + "displayName": "Quantity", + "shape": ["number", { "decimalPlaces": 4 }] + }, + { + "name": "filledQuantity", + "displayName": "Filled Quantity", + "shape": ["number", { "decimalPlaces": 4 }] + }, + { + "name": "limitPrice", + "displayName": "Limit Price", + "shape": ["number", { "decimalPlaces": 2 }] + }, + { + "name": "stopPrice", + "displayName": "Stop Price", + "shape": ["number", { "decimalPlaces": 2 }] + }, + { + "name": "currency", + "displayName": "Account Currency", + "shape": "string", + "visible": false + }, + { + "name": "extendedHours", + "displayName": "Extended Hours", + "shape": "boolean", + "visible": false + }, + { + "name": "initiatedFrom", + "displayName": "Initiated From", + "shape": "string", + "visible": false + }, + { + "name": "timeInForce", + "displayName": "Time In Force", + "shape": "string" + }, + { + "name": "strategy", + "displayName": "Strategy", + "shape": "string", + "visible": false + }, + { + "name": "createdAt", + "displayName": "Created", + "shape": "date", + "role": "timestamp" + }, + { + "sourceId": "ticker", + "sourceType": "Trading212 Position", + "name": "name" + } + ], + "timeframes": false +} diff --git a/plugins/Trading212/v1/dataStreams/pieDetail.json b/plugins/Trading212/v1/dataStreams/pieDetail.json new file mode 100644 index 0000000..257248f --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/pieDetail.json @@ -0,0 +1,87 @@ +{ + "name": "pieDetail", + "displayName": "Pie Detail", + "description": "Instrument holdings within a pie, one row per instrument with its share, quantity and result", + "tags": ["Portfolio"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "equity/pies/{{object.rawId}}", + "paging": { "mode": "none" }, + "getArgs": [], + "headers": [], + "postRequestScript": "pieDetail.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Trading212 Pie"] } }, + "metadata": [ + { + "name": "pieId", + "displayName": "Pie ID", + "shape": "string", + "visible": false + }, + { + "name": "pieName", + "displayName": "Pie Name", + "shape": "string" + }, + { + "name": "goal", + "displayName": "Goal", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "dividendCashAction", + "displayName": "Dividend Cash Action", + "shape": "string" + }, + { + "name": "ticker", + "displayName": "Ticker", + "shape": "string", + "role": "label" + }, + { + "name": "currentShare", + "displayName": "Current Share", + "shape": ["percent", { "asZeroToOne": true }] + }, + { + "name": "expectedShare", + "displayName": "Expected Share", + "shape": ["percent", { "asZeroToOne": true }] + }, + { + "name": "ownedQuantity", + "displayName": "Owned Quantity", + "shape": ["number", { "decimalPlaces": 4 }] + }, + { + "name": "investedValue", + "displayName": "Invested Value", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "currentValue", + "displayName": "Current Value", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }], + "role": "value" + }, + { + "name": "result", + "displayName": "Result", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "resultCoef", + "displayName": "Result (%)", + "shape": ["percent", { "asZeroToOne": true }] + }, + { + "sourceId": "ticker", + "sourceType": "Trading212 Position", + "name": "ticker" + } + ], + "timeframes": false +} diff --git a/plugins/Trading212/v1/dataStreams/pies.json b/plugins/Trading212/v1/dataStreams/pies.json new file mode 100644 index 0000000..9d1897f --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/pies.json @@ -0,0 +1,75 @@ +{ + "name": "pies", + "displayName": "Pies", + "description": "Investment pies, one row per pie with progress and overall result", + "tags": ["Portfolio"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "equity/pies", + "expandInnerObjects": true, + "paging": { "mode": "none" }, + "getArgs": [], + "headers": [] + }, + "matches": "none", + "metadata": [ + { + "name": "id", + "displayName": "Pie ID", + "shape": "string", + "visible": false + }, + { + "name": "pieName", + "displayName": "Name", + "computed": true, + "valueExpression": "{{ 'Pie #' + $['id'] }}", + "shape": "string", + "role": "label" + }, + { + "name": "status", + "displayName": "Status", + "shape": "string" + }, + { + "name": "progress", + "displayName": "Progress to Goal", + "shape": ["percent", { "asZeroToOne": true }] + }, + { + "name": "cash", + "displayName": "Cash", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "result.priceAvgInvestedValue", + "displayName": "Invested Value", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "result.priceAvgValue", + "displayName": "Current Value", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }], + "role": "value" + }, + { + "name": "result.priceAvgResult", + "displayName": "Result", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "result.priceAvgResultCoef", + "displayName": "Result (%)", + "shape": ["percent", { "asZeroToOne": true }] + }, + { + "sourceId": "id", + "sourceType": "Trading212 Pie", + "name": "pieName" + }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/Trading212/v1/dataStreams/positions.json b/plugins/Trading212/v1/dataStreams/positions.json new file mode 100644 index 0000000..7da0de0 --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/positions.json @@ -0,0 +1,118 @@ +{ + "name": "positions", + "displayName": "Positions", + "description": "Open equity, ETF and crypto holdings, one row per position", + "tags": ["Portfolio"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "equity/positions", + "paging": { "mode": "none" }, + "getArgs": [], + "headers": [], + "postRequestScript": "positions.js" + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "position", + "label": "Position (optional)", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Trading212 Position"] } + } + } + ], + "metadata": [ + { + "name": "ticker", + "displayName": "Ticker", + "shape": "string", + "visible": false + }, + { + "name": "name", + "displayName": "Instrument", + "shape": "string", + "role": "label" + }, + { + "name": "isin", + "displayName": "ISIN", + "shape": "string", + "visible": false + }, + { + "name": "instrumentCurrency", + "displayName": "Instrument Currency", + "shape": "string" + }, + { + "name": "quantity", + "displayName": "Quantity", + "shape": ["number", { "decimalPlaces": 4 }] + }, + { + "name": "quantityAvailableForTrading", + "displayName": "Quantity Available", + "shape": ["number", { "decimalPlaces": 4 }], + "visible": false + }, + { + "name": "quantityInPies", + "displayName": "Quantity in Pies", + "shape": ["number", { "decimalPlaces": 4 }], + "visible": false + }, + { + "name": "currentPrice", + "displayName": "Current Price", + "shape": ["number", { "decimalPlaces": 2 }] + }, + { + "name": "averagePricePaid", + "displayName": "Average Price Paid", + "shape": ["number", { "decimalPlaces": 2 }] + }, + { + "name": "walletCurrency", + "displayName": "Account Currency", + "shape": "string", + "visible": false + }, + { + "name": "totalCost", + "displayName": "Total Cost", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "currentValue", + "displayName": "Current Value", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }], + "role": "value" + }, + { + "name": "unrealizedProfitLoss", + "displayName": "Unrealized P/L", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }] + }, + { + "name": "fxImpact", + "displayName": "FX Impact", + "shape": ["number", { "decimalPlaces": 2 }], + "visible": false + }, + { + "name": "createdAt", + "displayName": "Opened", + "shape": "date", + "role": "timestamp" + }, + { + "sourceId": "ticker", + "sourceType": "Trading212 Position", + "name": "name" + } + ], + "timeframes": false +} diff --git a/plugins/Trading212/v1/dataStreams/scripts/dividendHistory.js b/plugins/Trading212/v1/dataStreams/scripts/dividendHistory.js new file mode 100644 index 0000000..66f1720 --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/scripts/dividendHistory.js @@ -0,0 +1,19 @@ +// Optional `position` object-picker parameter (stream `ui` name "position"). +// Selected objects arrive at context.config.position as an ARRAY, each rawId +// a single-element array. Empty/absent -> account-wide, no filter. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.position) || []; +const tickers = new Set(selected.map((o) => unwrap(o.rawId)).filter(Boolean)); + +const rows = ((data && data.items) || []).map((d) => ({ + ticker: d.ticker || (d.instrument && d.instrument.ticker), + instrumentName: d.instrument && d.instrument.name, + amount: d.amount, + grossAmountPerShare: d.grossAmountPerShare, + quantity: d.quantity, + type: d.type, + paidOn: d.paidOn, + reference: d.reference, +})); + +result = tickers.size ? rows.filter((r) => tickers.has(r.ticker)) : rows; diff --git a/plugins/Trading212/v1/dataStreams/scripts/orderHistory.js b/plugins/Trading212/v1/dataStreams/scripts/orderHistory.js new file mode 100644 index 0000000..e4850ea --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/scripts/orderHistory.js @@ -0,0 +1,63 @@ +// Optional `position` object-picker parameter (stream `ui` name "position"). +// Selected objects arrive at context.config.position as an ARRAY, each rawId +// a single-element array. Empty/absent -> account-wide, no filter. +// +// NOTE ON PAGING: the endpoint's `nextPagePath` is a root-relative path +// (e.g. "/api/v0/equity/history/orders?cursor=...&limit=50&instrumentCode"), +// not an absolute URL and not a bare token. Neither the platform's "nextUrl" +// mode (requires an absolute URL, throws ERR_INVALID_URL on a relative one) +// nor "token" mode (sends the whole payload value verbatim as the next +// request's query arg -> API rejects it: "parameter 'cursor' is invalid. +// Expected: Long") can consume this shape, and there is no separate bare +// cursor field elsewhere in the payload or headers to extract instead. So +// paging is disabled here and `limit` is pinned to the API's max (50) via a +// plain getArg -- this stream returns only the most recent 50 orders. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.position) || []; +const tickers = new Set(selected.map((o) => unwrap(o.rawId)).filter(Boolean)); + +const rows = (data.items || []).map((item) => { + const o = item.order || {}; + const f = item.fill || {}; + const wallet = f.walletImpact || {}; + const taxes = wallet.taxes || []; + const fees = taxes.length + ? -taxes.reduce((sum, t) => sum + (t.quantity || 0), 0) + : null; + // order.filledValue/order.value only exist for VALUE-strategy orders; + // fill.walletImpact.netValue is present for every fill (QUANTITY or + // VALUE strategy alike) and matches order.filledValue when both exist, + // so it's the reliable source for "how much this order was worth". + const filledValue = + wallet.netValue !== undefined ? wallet.netValue : o.filledValue; + + return { + id: o.id, + ticker: o.ticker, + name: o.instrument && o.instrument.name, + isin: o.instrument && o.instrument.isin, + instrumentCurrency: o.instrument && o.instrument.currency, + type: o.type, + strategy: o.strategy, + side: o.side, + status: o.status, + orderedQuantity: o.quantity, + filledQuantity: o.filledQuantity, + orderedValue: o.value, + filledValue: filledValue, + fillPrice: f.price, + limitPrice: o.limitPrice, + stopPrice: o.stopPrice, + accountCurrency: o.currency, + realisedProfitLoss: wallet.realisedProfitLoss, + fees: fees, + fxRate: wallet.fxRate, + tradingMethod: f.tradingMethod, + extendedHours: o.extendedHours, + initiatedFrom: o.initiatedFrom, + dateCreated: o.createdAt, + dateExecuted: f.filledAt || o.createdAt, + }; +}); + +result = tickers.size ? rows.filter((r) => tickers.has(r.ticker)) : rows; diff --git a/plugins/Trading212/v1/dataStreams/scripts/pendingOrders.js b/plugins/Trading212/v1/dataStreams/scripts/pendingOrders.js new file mode 100644 index 0000000..c50057e --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/scripts/pendingOrders.js @@ -0,0 +1,29 @@ +// Optional `position` object-picker parameter (stream `ui` name "position"). +// Selected objects arrive at context.config.position as an ARRAY, each rawId +// a single-element array. Empty/absent -> account-wide, no filter. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.position) || []; +const tickers = new Set(selected.map((o) => unwrap(o.rawId)).filter(Boolean)); + +const rows = (data || []).map((o) => ({ + id: o.id, + ticker: o.ticker, + name: o.instrument && o.instrument.name, + isin: o.instrument && o.instrument.isin, + instrumentCurrency: o.instrument && o.instrument.currency, + type: o.type, + side: o.side, + status: o.status, + quantity: o.quantity, + filledQuantity: o.filledQuantity, + limitPrice: o.limitPrice, + stopPrice: o.stopPrice, + currency: o.currency, + extendedHours: o.extendedHours, + initiatedFrom: o.initiatedFrom, + timeInForce: o.timeInForce, + strategy: o.strategy, + createdAt: o.createdAt, +})); + +result = tickers.size ? rows.filter((r) => tickers.has(r.ticker)) : rows; diff --git a/plugins/Trading212/v1/dataStreams/scripts/pieDetail.js b/plugins/Trading212/v1/dataStreams/scripts/pieDetail.js new file mode 100644 index 0000000..dea2001 --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/scripts/pieDetail.js @@ -0,0 +1,25 @@ +// Joins the pie-level `settings` object onto every row of the `instruments` +// array so each output row carries both the instrument's own fields and the +// pie's shared context (id, name, goal, dividend action). Needs a script +// because pathToData + expandInnerObjects can only reach one of the two +// sibling structures (the array OR the object), not both on the same row. +const settings = data.settings || {}; +const instruments = data.instruments || []; + +result = instruments.map((instrument) => { + const r = instrument.result || {}; + return { + pieId: settings.id, + pieName: settings.name, + goal: settings.goal, + dividendCashAction: settings.dividendCashAction, + ticker: instrument.ticker, + currentShare: instrument.currentShare, + expectedShare: instrument.expectedShare, + ownedQuantity: instrument.ownedQuantity, + investedValue: r.priceAvgInvestedValue, + currentValue: r.priceAvgValue, + result: r.priceAvgResult, + resultCoef: r.priceAvgResultCoef, + }; +}); diff --git a/plugins/Trading212/v1/dataStreams/scripts/positions.js b/plugins/Trading212/v1/dataStreams/scripts/positions.js new file mode 100644 index 0000000..6faf0f9 --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/scripts/positions.js @@ -0,0 +1,26 @@ +// Optional `position` object-picker parameter (stream `ui` name "position"). +// Selected objects arrive at context.config.position as an ARRAY, each rawId +// a single-element array. Empty/absent -> account-wide, no filter. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.position) || []; +const tickers = new Set(selected.map((o) => unwrap(o.rawId)).filter(Boolean)); + +const rows = (data || []).map((p) => ({ + ticker: p.instrument && p.instrument.ticker, + name: p.instrument && p.instrument.name, + isin: p.instrument && p.instrument.isin, + instrumentCurrency: p.instrument && p.instrument.currency, + quantity: p.quantity, + quantityAvailableForTrading: p.quantityAvailableForTrading, + quantityInPies: p.quantityInPies, + currentPrice: p.currentPrice, + averagePricePaid: p.averagePricePaid, + walletCurrency: p.walletImpact && p.walletImpact.currency, + totalCost: p.walletImpact && p.walletImpact.totalCost, + currentValue: p.walletImpact && p.walletImpact.currentValue, + unrealizedProfitLoss: p.walletImpact && p.walletImpact.unrealizedProfitLoss, + fxImpact: p.walletImpact && p.walletImpact.fxImpact, + createdAt: p.createdAt, +})); + +result = tickers.size ? rows.filter((r) => tickers.has(r.ticker)) : rows; diff --git a/plugins/Trading212/v1/dataStreams/transactionHistory.json b/plugins/Trading212/v1/dataStreams/transactionHistory.json new file mode 100644 index 0000000..b577ea7 --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/transactionHistory.json @@ -0,0 +1,47 @@ +{ + "name": "transactionHistory", + "displayName": "Transaction History", + "description": "Most recent account-wide deposits, withdrawals, fees and transfers", + "tags": ["Account"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "equity/history/transactions", + "pathToData": "items", + "paging": { "mode": "none" }, + "getArgs": [{ "key": "limit", "value": "50" }], + "headers": [] + }, + "matches": "none", + "metadata": [ + { + "name": "reference", + "displayName": "Reference", + "shape": "string", + "visible": false + }, + { + "name": "type", + "displayName": "Type", + "shape": "string" + }, + { + "name": "amount", + "displayName": "Amount", + "shape": ["number", { "decimalPlaces": 2, "thousandsSeparator": true }], + "role": "value" + }, + { + "name": "currency", + "displayName": "Currency", + "shape": "string" + }, + { + "name": "dateTime", + "displayName": "Date", + "shape": "date", + "role": "timestamp" + } + ], + "timeframes": false +} diff --git a/plugins/Trading212/v1/defaultContent/manifest.json b/plugins/Trading212/v1/defaultContent/manifest.json new file mode 100644 index 0000000..4699f41 --- /dev/null +++ b/plugins/Trading212/v1/defaultContent/manifest.json @@ -0,0 +1,7 @@ +{ + "items": [ + { "name": "overviewDashboard", "type": "dashboard" }, + { "name": "positionDashboard", "type": "dashboard" }, + { "name": "pieDashboard", "type": "dashboard" } + ] +} diff --git a/plugins/Trading212/v1/defaultContent/overviewDashboard.dash.json b/plugins/Trading212/v1/defaultContent/overviewDashboard.dash.json new file mode 100644 index 0000000..08870f3 --- /dev/null +++ b/plugins/Trading212/v1/defaultContent/overviewDashboard.dash.json @@ -0,0 +1,372 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last7days", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "98a86ed8-5ac0-4054-a543-006c233d1fd1", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Total Value", + "description": "Total account value: cash plus invested value", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[accountSummary]}}", + "name": "accountSummary", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "totalValue", + "comparisonColumn": "none", + "label": "Total Value" + } + } + } + } + }, + { + "i": "330ba79a-bd13-4dcd-82a4-aafdab66d7f1", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Cash Available", + "description": "Cash available to trade", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[accountSummary]}}", + "name": "accountSummary", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "cash.availableToTrade", + "comparisonColumn": "none", + "label": "Cash Available" + } + } + } + } + }, + { + "i": "1c19e872-704b-423f-97c5-dbc9b712072d", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Invested Value", + "description": "Current value of open positions", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[accountSummary]}}", + "name": "accountSummary", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "investments.currentValue", + "comparisonColumn": "none", + "label": "Invested Value" + } + } + } + } + }, + { + "i": "27cf6dd5-5a64-425b-b8d5-628899600397", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Unrealized P/L", + "description": "Unrealized profit/loss across open positions", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[accountSummary]}}", + "name": "accountSummary", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "investments.unrealizedProfitLoss", + "comparisonColumn": "none", + "label": "Unrealized P/L" + } + } + } + } + }, + { + "i": "bd43cf55-efbe-4c87-a086-9c3e8c153822", + "x": 0, + "y": 2, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Properties", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Accounts]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["name", "currency"], + "hiddenColumns": ["id", "links", "type", "label", "sourceId", "sourceType"] + } + } + } + } + }, + { + "i": "67271716-4c6d-413a-b5a0-b57b5989c709", + "x": 1, + "y": 2, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Positions", + "description": "All open positions", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[positions]}}", + "name": "positions", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["currentValue", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "quantity", + "currentPrice", + "averagePricePaid", + "currentValue", + "unrealizedProfitLoss", + "createdAt" + ] + } + } + } + } + }, + { + "i": "68f20726-acd0-4e7b-9cf9-78f8a29f49f9", + "x": 0, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pending Orders", + "description": "Orders that have not yet been filled", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pendingOrders]}}", + "name": "pendingOrders", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["createdAt", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "side", + "type", + "status", + "quantity", + "filledQuantity", + "limitPrice", + "stopPrice", + "createdAt" + ] + } + } + } + } + }, + { + "i": "4bc49722-1efa-41b1-a471-426f3bb5c8c3", + "x": 2, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pies", + "description": "Investment pies (empty until the account has pies)", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pies]}}", + "name": "pies", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["result.priceAvgValue", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "pieName", + "status", + "progress", + "cash", + "result.priceAvgInvestedValue", + "result.priceAvgValue", + "result.priceAvgResult", + "result.priceAvgResultCoef" + ] + } + } + } + } + }, + { + "i": "53fd3fab-abe4-49cd-b4a6-92c83835a20c", + "x": 0, + "y": 8, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Orders", + "description": "Most recently executed or cancelled orders", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[orderHistory]}}", + "name": "orderHistory", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["dateExecuted", "desc"]], "top": 10 } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "side", + "type", + "status", + "filledQuantity", + "fillPrice", + "filledValue", + "realisedProfitLoss", + "dateExecuted" + ] + } + } + } + } + }, + { + "i": "f9864f32-8544-4ad8-9614-dc34ffe9461d", + "x": 2, + "y": 8, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Transactions", + "description": "Most recent deposits, withdrawals, fees and transfers", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[transactionHistory]}}", + "name": "transactionHistory", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["dateTime", "desc"]], "top": 10 } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["type", "amount", "currency", "dateTime"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Trading212/v1/defaultContent/pieDashboard.dash.json b/plugins/Trading212/v1/defaultContent/pieDashboard.dash.json new file mode 100644 index 0000000..44f39b0 --- /dev/null +++ b/plugins/Trading212/v1/defaultContent/pieDashboard.dash.json @@ -0,0 +1,198 @@ +{ + "name": "Pie", + "schemaVersion": "1.5", + "timeframe": "last7days", + "variables": ["{{variables.[Pie]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "ece8fe71-cb6d-4ab4-a95f-81ea391c8f3c", + "x": 0, + "y": 0, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Properties", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Pie]}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Pies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pie]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["name", "status"], + "hiddenColumns": ["id", "links", "type", "label", "sourceId", "sourceType"] + } + } + } + } + }, + { + "i": "e7ad5a1c-8b4f-464d-b4e2-1c6d0de5e48d", + "x": 1, + "y": 0, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pie Summary", + "description": "Goal, dividend handling and total result across this pie's instruments", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Pie]}}"], + "dataStream": { + "id": "{{dataStreams.[pieDetail]}}", + "name": "pieDetail", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { "type": "max", "names": ["goal"] }, + { "type": "mode", "names": ["dividendCashAction"] }, + { "type": "sum", "names": ["investedValue"] }, + { "type": "sum", "names": ["currentValue"] }, + { "type": "sum", "names": ["result"] } + ] + } + }, + "scope": { + "scope": "{{scopes.[Pies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pie]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "goal_max", + "dividendCashAction_mode", + "investedValue_sum", + "currentValue_sum", + "result_sum" + ], + "columnDisplayNames": { + "goal_max": "Goal", + "dividendCashAction_mode": "Dividend Cash Action", + "investedValue_sum": "Invested Value", + "currentValue_sum": "Current Value", + "result_sum": "Result" + } + } + } + } + } + }, + { + "i": "f32d2391-4411-47a2-83ae-a85ba39907ca", + "x": 0, + "y": 3, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Composition", + "description": "Instruments held within this pie", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Pie]}}"], + "dataStream": { + "id": "{{dataStreams.[pieDetail]}}", + "name": "pieDetail", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["currentShare", "desc"]] } + }, + "scope": { + "scope": "{{scopes.[Pies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pie]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "ticker", + "currentShare", + "expectedShare", + "ownedQuantity", + "investedValue", + "currentValue", + "result", + "resultCoef" + ] + } + } + } + } + }, + { + "i": "86716308-203c-4ecc-8108-342652028359", + "x": 3, + "y": 3, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Value by Instrument", + "description": "Share of this pie's current value by instrument", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Pie]}}"], + "dataStream": { + "id": "{{dataStreams.[pieDetail]}}", + "name": "pieDetail", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Pies]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pie]}}" + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "currentValue", + "labelColumn": "ticker", + "hideCenterValue": false, + "showValuesAsPercentage": true, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + } + ] + } +} diff --git a/plugins/Trading212/v1/defaultContent/positionDashboard.dash.json b/plugins/Trading212/v1/defaultContent/positionDashboard.dash.json new file mode 100644 index 0000000..db1e438 --- /dev/null +++ b/plugins/Trading212/v1/defaultContent/positionDashboard.dash.json @@ -0,0 +1,354 @@ +{ + "name": "Position", + "schemaVersion": "1.5", + "timeframe": "last7days", + "variables": ["{{variables.[Position]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "db656a41-e8be-4f17-9a76-635388d11031", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Quantity", + "description": "Quantity currently held", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Position]}}"], + "dataStream": { + "id": "{{dataStreams.[positions]}}", + "name": "positions", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "position": { + "variable": "{{variables.[Position]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Positions]}}" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "quantity", + "comparisonColumn": "none", + "label": "Quantity" + } + } + } + } + }, + { + "i": "21d39438-8ea8-4d80-a835-af39c1598ab5", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Current Price", + "description": "Current price per share", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Position]}}"], + "dataStream": { + "id": "{{dataStreams.[positions]}}", + "name": "positions", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "position": { + "variable": "{{variables.[Position]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Positions]}}" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "currentPrice", + "comparisonColumn": "none", + "label": "Current Price" + } + } + } + } + }, + { + "i": "b880ed60-a80e-4dd8-a972-0f398292a61a", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Current Value", + "description": "Current market value of this position", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Position]}}"], + "dataStream": { + "id": "{{dataStreams.[positions]}}", + "name": "positions", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "position": { + "variable": "{{variables.[Position]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Positions]}}" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "currentValue", + "comparisonColumn": "none", + "label": "Current Value" + } + } + } + } + }, + { + "i": "a3d61706-d93d-4853-aa05-a24ef2e03b57", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Unrealized P/L", + "description": "Unrealized profit/loss on this position", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Position]}}"], + "dataStream": { + "id": "{{dataStreams.[positions]}}", + "name": "positions", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "position": { + "variable": "{{variables.[Position]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Positions]}}" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "unrealizedProfitLoss", + "comparisonColumn": "none", + "label": "Unrealized P/L" + } + } + } + } + }, + { + "i": "d4fc4189-5954-4a29-bbd3-f00d17936cc9", + "x": 0, + "y": 2, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Properties", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Position]}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Positions]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Position]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["name", "isin", "instrumentCurrency"], + "hiddenColumns": ["id", "links", "type", "label", "sourceId", "sourceType"] + } + } + } + } + }, + { + "i": "e57a0b5f-7236-406b-b4f4-dc645af824ae", + "x": 1, + "y": 2, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pending Orders", + "description": "Orders for this position that have not yet been filled", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Position]}}"], + "dataStream": { + "id": "{{dataStreams.[pendingOrders]}}", + "name": "pendingOrders", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "position": { + "variable": "{{variables.[Position]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Positions]}}" + } + }, + "sort": { "by": [["createdAt", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "side", + "type", + "status", + "quantity", + "filledQuantity", + "limitPrice", + "stopPrice", + "createdAt" + ] + } + } + } + } + }, + { + "i": "8259feea-9cda-4738-9918-c1909c53b657", + "x": 0, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Order History", + "description": "Most recent executed or cancelled orders for this position", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Position]}}"], + "dataStream": { + "id": "{{dataStreams.[orderHistory]}}", + "name": "orderHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "position": { + "variable": "{{variables.[Position]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Positions]}}" + } + }, + "sort": { "by": [["dateExecuted", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "side", + "type", + "status", + "filledQuantity", + "fillPrice", + "filledValue", + "realisedProfitLoss", + "dateExecuted" + ] + } + } + } + } + }, + { + "i": "1f155aa8-9b26-4963-a144-8346313fdc3e", + "x": 2, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Dividend History", + "description": "Most recent dividend payments for this position", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Position]}}"], + "dataStream": { + "id": "{{dataStreams.[dividendHistory]}}", + "name": "dividendHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "position": { + "variable": "{{variables.[Position]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Positions]}}" + } + }, + "sort": { "by": [["paidOn", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "amount", + "grossAmountPerShare", + "quantity", + "type", + "paidOn" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Trading212/v1/defaultContent/scopes.json b/plugins/Trading212/v1/defaultContent/scopes.json new file mode 100644 index 0000000..80439cc --- /dev/null +++ b/plugins/Trading212/v1/defaultContent/scopes.json @@ -0,0 +1,38 @@ +[ + { + "name": "Accounts", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Trading212 Account"] } + }, + "variable": { + "name": "Account", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Positions", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Trading212 Position"] } + }, + "variable": { + "name": "Position", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Pies", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Trading212 Pie"] } + }, + "variable": { + "name": "Pie", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/Trading212/v1/docs/README.md b/plugins/Trading212/v1/docs/README.md new file mode 100644 index 0000000..2444d8b --- /dev/null +++ b/plugins/Trading212/v1/docs/README.md @@ -0,0 +1,44 @@ +# Before you start + +To connect SquaredUp to Trading 212, you will need to generate an API key. + +The Trading 212 public API is currently in **beta** and is only available for **Invest** and **Stocks ISA** accounts (not SIPP). + +## Generate an API key + +1. Log in to the Trading 212 web app or mobile app. +2. Open **Settings** and select **API (Beta)**. +3. Accept the risk warning for using third-party applications. +4. Click **Generate API key**. +5. Tick **every** permission checkbox shown (Account data, Portfolio, Orders, and each History item such as Orders/Dividends/Transactions if listed separately). The permission list is more granular than it first appears — a key missing even one of these returns a `403 Forbidden` on the matching part of this plugin rather than a clear error. +6. Give the key a name and, optionally, restrict it to specific IP addresses. +7. Copy the **API Key** and the **API Secret** — the secret is only shown once. + +If you use a Trading 212 **practice/demo** account instead of a live account, generate the key from the demo app — a demo key only works against the Demo environment below. + +## Configure the plugin + +1. Add the **Trading 212** plugin in SquaredUp. +2. Enter the **API Key** and **API Secret** generated above. +3. Choose **Live** or **Demo** to match the account the key was generated against. +4. Save the configuration to begin importing your account, positions, and pies. + +| Field | Description | Required | +| --- | --- | --- | +| API Key | The API key from Trading 212 Settings > API (Beta). | Yes | +| API Secret | The API secret shown once when the key was generated. | Yes | +| Environment | `Live` for a real account, `Demo` for a practice account. Must match the account the key belongs to. | Yes | + +## What gets indexed + +- **Trading212 Account** — your account's currency, cash, invested and total value. One object per connected account. +- **Trading212 Position** — each open holding (equity, ETF or crypto instrument) in your account, with quantity, average price, current price and profit/loss. +- **Trading212 Pie** — each investment "pie" (basket of instruments) in your account, with progress toward its goal and overall result. + +## Known limitations + +- The Trading 212 public API has no price-history or portfolio-value-over-time endpoint, so dashboards show current state and recent activity rather than long-term trend charts. +- The **Pies** part of the API is marked deprecated by Trading 212 (no further updates planned) but remains operational at the time of writing. +- Rate limits are strict and vary per endpoint (as low as 1 request per 30–50 seconds for some endpoints), so large accounts with many pies or a long history may take longer to fully sync. +- Placing, modifying, or cancelling orders is not supported by this plugin — it is read-only monitoring. +- Order, dividend and transaction history show only the **most recent 50** records. Trading 212's history endpoints paginate via a relative path that SquaredUp's data streams can't follow, so this plugin requests the maximum single page instead of walking further back. diff --git a/plugins/Trading212/v1/icon.svg b/plugins/Trading212/v1/icon.svg new file mode 100644 index 0000000..475f475 --- /dev/null +++ b/plugins/Trading212/v1/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/Trading212/v1/indexDefinitions/default.json b/plugins/Trading212/v1/indexDefinitions/default.json new file mode 100644 index 0000000..5e26b6f --- /dev/null +++ b/plugins/Trading212/v1/indexDefinitions/default.json @@ -0,0 +1,37 @@ +{ + "steps": [ + { + "name": "account", + "dataStream": { "name": "accountSummary" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "accountName", + "type": { "value": "Trading212 Account" }, + "properties": ["currency"] + } + }, + { + "name": "positions", + "dataStream": { "name": "positions" }, + "timeframe": "none", + "objectMapping": { + "id": "ticker", + "name": "name", + "type": { "value": "Trading212 Position" }, + "properties": ["isin", "instrumentCurrency"] + } + }, + { + "name": "pies", + "dataStream": { "name": "pies" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "pieName", + "type": { "value": "Trading212 Pie" }, + "properties": ["status"] + } + } + ] +} diff --git a/plugins/Trading212/v1/metadata.json b/plugins/Trading212/v1/metadata.json new file mode 100644 index 0000000..7d444b0 --- /dev/null +++ b/plugins/Trading212/v1/metadata.json @@ -0,0 +1,41 @@ +{ + "name": "trading212", + "displayName": "Trading 212", + "version": "1.2.0", + "author": { + "name": "Ryan Welan", + "type": "community" + }, + "description": "Monitor your Trading 212 Invest or Stocks ISA account: cash and account value, open positions, pies, pending orders, and account history.", + "category": "Analytics", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": ["trading212", "trading 212", "investing", "stocks", "portfolio", "shares", "isa"], + "objectTypes": ["Trading212 Account", "Trading212 Position", "Trading212 Pie"], + "links": [ + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/Trading212/v1", + "label": "Repository" + }, + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Trading212/v1/docs/README.md", + "label": "Help adding this plugin" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "authMode": "basic", + "basicAuthUsername": "{{apiKey}}", + "basicAuthPassword": "{{apiSecret}}", + "baseUrl": "https://{{environment === 'demo' ? 'demo' : 'live'}}.trading212.com/api/v0", + "headers": [], + "queryArgs": [] + } + } +} diff --git a/plugins/Trading212/v1/ui.json b/plugins/Trading212/v1/ui.json new file mode 100644 index 0000000..09876f9 --- /dev/null +++ b/plugins/Trading212/v1/ui.json @@ -0,0 +1,27 @@ +[ + { + "type": "password", + "name": "apiKey", + "label": "API Key", + "help": "Generate an API key under Trading 212 **Settings > API (Beta)**. Requires the Account data, Portfolio, Orders and History scopes.", + "validation": { "required": true } + }, + { + "type": "password", + "name": "apiSecret", + "label": "API Secret", + "help": "Shown only once when the API key is generated — store it securely before saving.", + "validation": { "required": true } + }, + { + "type": "radio", + "name": "environment", + "label": "Environment", + "defaultValue": "live", + "options": [ + { "value": "live", "label": "Live (real account)" }, + { "value": "demo", "label": "Demo (practice account)" } + ], + "help": "The API key must belong to the same environment. Keys generated against a practice account only work against Demo." + } +] From 9d9ed3a27853a702b7f514b7b4ca067417f320f4 Mon Sep 17 00:00:00 2001 From: ryanwelan Date: Wed, 8 Jul 2026 13:09:39 +0100 Subject: [PATCH 2/3] Trading212: fix Pie Summary tile aggregation on Dividend Cash Action The mode aggregator was applied to dividendCashAction (a string column), but SquaredUp's mode aggregator is documented for numeric columns only. Verified against a real pie once one existed in a test account. Group by the column instead, since it's constant per pie. Co-Authored-By: Claude Sonnet 5 --- .../Trading212/v1/defaultContent/pieDashboard.dash.json | 7 +++---- plugins/Trading212/v1/metadata.json | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/Trading212/v1/defaultContent/pieDashboard.dash.json b/plugins/Trading212/v1/defaultContent/pieDashboard.dash.json index 44f39b0..eb2b6ea 100644 --- a/plugins/Trading212/v1/defaultContent/pieDashboard.dash.json +++ b/plugins/Trading212/v1/defaultContent/pieDashboard.dash.json @@ -66,10 +66,9 @@ "name": "pieDetail", "pluginConfigId": "{{configId}}", "group": { - "by": [], + "by": [["dividendCashAction", "uniqueValues"]], "aggregate": [ { "type": "max", "names": ["goal"] }, - { "type": "mode", "names": ["dividendCashAction"] }, { "type": "sum", "names": ["investedValue"] }, { "type": "sum", "names": ["currentValue"] }, { "type": "sum", "names": ["result"] } @@ -88,14 +87,14 @@ "transpose": true, "columnOrder": [ "goal_max", - "dividendCashAction_mode", + "dividendCashAction_uniqueValues", "investedValue_sum", "currentValue_sum", "result_sum" ], "columnDisplayNames": { "goal_max": "Goal", - "dividendCashAction_mode": "Dividend Cash Action", + "dividendCashAction_uniqueValues": "Dividend Cash Action", "investedValue_sum": "Invested Value", "currentValue_sum": "Current Value", "result_sum": "Result" diff --git a/plugins/Trading212/v1/metadata.json b/plugins/Trading212/v1/metadata.json index 7d444b0..c607ce2 100644 --- a/plugins/Trading212/v1/metadata.json +++ b/plugins/Trading212/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "trading212", "displayName": "Trading 212", - "version": "1.2.0", + "version": "1.2.1", "author": { "name": "Ryan Welan", "type": "community" From fe5911ca64c24f62951bc792e2492b63a022a634 Mon Sep 17 00:00:00 2001 From: ryanwelan Date: Wed, 8 Jul 2026 13:16:34 +0100 Subject: [PATCH 3/3] Trading212: address CodeRabbit review findings - orderHistory: order id is an integer per the Trading212 API, not a string; align its shape/role with pendingOrders.json so row identity and type handling work consistently across both order streams. - orderHistory: rename the "fees" column's display label from "Fees" to "Taxes" - it sums walletImpact.taxes (stamp duty, transaction tax, etc), not broker fees, and Trading 212 is commission-free. Co-Authored-By: Claude Sonnet 5 --- plugins/Trading212/v1/dataStreams/orderHistory.json | 5 +++-- plugins/Trading212/v1/metadata.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/Trading212/v1/dataStreams/orderHistory.json b/plugins/Trading212/v1/dataStreams/orderHistory.json index 0c424bd..d0e1365 100644 --- a/plugins/Trading212/v1/dataStreams/orderHistory.json +++ b/plugins/Trading212/v1/dataStreams/orderHistory.json @@ -27,7 +27,8 @@ { "name": "id", "displayName": "Order ID", - "shape": "string", + "shape": ["number", { "decimalPlaces": 0 }], + "role": "id", "visible": false }, { @@ -128,7 +129,7 @@ }, { "name": "fees", - "displayName": "Fees", + "displayName": "Taxes", "shape": ["number", { "decimalPlaces": 2 }], "visible": false }, diff --git a/plugins/Trading212/v1/metadata.json b/plugins/Trading212/v1/metadata.json index c607ce2..d7e269d 100644 --- a/plugins/Trading212/v1/metadata.json +++ b/plugins/Trading212/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "trading212", "displayName": "Trading 212", - "version": "1.2.1", + "version": "1.2.2", "author": { "name": "Ryan Welan", "type": "community"