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..d0e1365 --- /dev/null +++ b/plugins/Trading212/v1/dataStreams/orderHistory.json @@ -0,0 +1,179 @@ +{ + "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": ["number", { "decimalPlaces": 0 }], + "role": "id", + "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": "Taxes", + "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..eb2b6ea --- /dev/null +++ b/plugins/Trading212/v1/defaultContent/pieDashboard.dash.json @@ -0,0 +1,197 @@ +{ + "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": [["dividendCashAction", "uniqueValues"]], + "aggregate": [ + { "type": "max", "names": ["goal"] }, + { "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_uniqueValues", + "investedValue_sum", + "currentValue_sum", + "result_sum" + ], + "columnDisplayNames": { + "goal_max": "Goal", + "dividendCashAction_uniqueValues": "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..d7e269d --- /dev/null +++ b/plugins/Trading212/v1/metadata.json @@ -0,0 +1,41 @@ +{ + "name": "trading212", + "displayName": "Trading 212", + "version": "1.2.2", + "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." + } +]