From d07e07fdfead942393269c8926f9934fb395701a Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Thu, 1 May 2025 10:29:29 -0400 Subject: [PATCH 1/4] wip --- components/oanda/oanda.app.mjs | 262 ++++++++++++++++++++++++++++++++- components/oanda/package.json | 2 +- 2 files changed, 259 insertions(+), 5 deletions(-) diff --git a/components/oanda/oanda.app.mjs b/components/oanda/oanda.app.mjs index 0089dc3b7e639..7a7bcce3f2e21 100644 --- a/components/oanda/oanda.app.mjs +++ b/components/oanda/oanda.app.mjs @@ -1,11 +1,265 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "oanda", - propDefinitions: {}, + propDefinitions: { + baseCurrency: { + type: "string", + label: "Base Currency", + description: "The base currency of the currency pair.", + }, + quoteCurrency: { + type: "string", + label: "Quote Currency", + description: "The quote currency of the currency pair.", + }, + changeThreshold: { + type: "number", + label: "Change Threshold", + description: "The threshold for significant change in forex rate to trigger the event.", + }, + instrumentFilter: { + type: "string", + label: "Instrument Filter", + description: "Filter by instrument.", + optional: true, + }, + tradeTypeFilter: { + type: "string", + label: "Trade Type Filter", + description: "Filter by trade type.", + optional: true, + options: [ + { + label: "Buy", + value: "BUY", + }, + { + label: "Sell", + value: "SELL", + }, + ], + }, + orderTypeFilter: { + type: "string", + label: "Order Type Filter", + description: "Filter by order type.", + optional: true, + options: [ + { + label: "Market", + value: "MARKET", + }, + { + label: "Limit", + value: "LIMIT", + }, + { + label: "Stop", + value: "STOP", + }, + ], + }, + instrument: { + type: "string", + label: "Instrument", + description: "The financial instrument to be used.", + }, + orderType: { + type: "string", + label: "Order Type", + description: "Type of the order.", + options: [ + { + label: "Market", + value: "MARKET", + }, + { + label: "Limit", + value: "LIMIT", + }, + { + label: "Stop", + value: "STOP", + }, + ], + }, + units: { + type: "integer", + label: "Units", + description: "Number of units to trade.", + }, + priceThreshold: { + type: "number", + label: "Price Threshold", + description: "Optional price threshold for the order.", + optional: true, + }, + timeInForce: { + type: "string", + label: "Time In Force", + description: "Optional time in force for the order.", + optional: true, + options: [ + { + label: "Good Till Cancelled (GTC)", + value: "GTC", + }, + { + label: "Immediate Or Cancel (IOC)", + value: "IOC", + }, + { + label: "Fill Or Kill (FOK)", + value: "FOK", + }, + ], + }, + granularity: { + type: "string", + label: "Granularity", + description: "The granularity of the historical price data.", + options: [ + { + label: "Minute", + value: "M1", + }, + { + label: "Hour", + value: "H1", + }, + { + label: "Daily", + value: "D", + }, + ], + }, + startTime: { + type: "string", + label: "Start Time", + description: "The start time for historical price data (ISO 8601 format).", + }, + endTime: { + type: "string", + label: "End Time", + description: "The end time for historical price data (ISO 8601 format).", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl(isDemo = false) { + return isDemo + ? "https://api-fxpractice.oanda.com/v3" + : "https://api-fxtrade.oanda.com/v3"; + }, + _makeRequest({ + $ = this, + path, + isDemo, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl(isDemo)}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.personal_token}`, + }, + }); + }, + async listTrades(opts = {}) { + const { + instrumentFilter, tradeTypeFilter, ...otherOpts + } = opts; + const params = {}; + if (tradeTypeFilter) { + params.type = tradeTypeFilter; + } + if (instrumentFilter) { + params.instrument = instrumentFilter; + } + return this._makeRequest({ + path: `/accounts/${this.$auth.account_id}/trades`, + params, + ...otherOpts, + }); + }, + async createOrder(opts = {}) { + const { + instrument, orderType, units, priceThreshold, timeInForce, ...otherOpts + } = opts; + const data = { + order: { + type: orderType, + instrument: instrument, + units: units, + timeInForce: timeInForce || "FOK", + }, + }; + if (priceThreshold) { + data.order.price = priceThreshold; + } + return this._makeRequest({ + method: "POST", + path: `/accounts/${this.$auth.account_id}/orders`, + data, + ...otherOpts, + }); + }, + async getHistoricalPrices(opts = {}) { + const { + instrument, granularity, startTime, endTime, ...otherOpts + } = opts; + const params = { + granularity: granularity, + from: startTime, + to: endTime, + }; + return this._makeRequest({ + path: `/instruments/${instrument}/candles`, + params, + ...otherOpts, + }); + }, + async getOrderStatus(opts = {}) { + const { + orderTypeFilter, ...otherOpts + } = opts; + const params = {}; + if (orderTypeFilter) { + params.type = orderTypeFilter; + } + return this._makeRequest({ + path: `/accounts/${this.$auth.account_id}/orders`, + params, + ...otherOpts, + }); + }, + async getPricingData(opts = {}) { + const { + baseCurrency, quoteCurrency, ...otherOpts + } = opts; + const instrument = `${baseCurrency}_${quoteCurrency}`; + const params = { + instruments: instrument, + }; + return this._makeRequest({ + path: "/pricing", + params, + ...otherOpts, + }); + }, + async paginate(fn, ...opts) { + let results = []; + let response = await fn(...opts); + results = results.concat(response.trades || response.orders || []); + while (response.nextPage) { + response = await fn({ + page: response.nextPage, + ...opts, + }); + results = results.concat(response.trades || response.orders || []); + } + return results; }, }, }; diff --git a/components/oanda/package.json b/components/oanda/package.json index 20ec6ae68b4a1..20ee1ffa005f8 100644 --- a/components/oanda/package.json +++ b/components/oanda/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/oanda", - "version": "0.6.0", + "version": "0.7.0", "description": "Pipedream oanda Components", "main": "oanda.app.mjs", "keywords": [ From 80192362dd71f2023e5fe21bc45b79fee4b17e43 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Fri, 2 May 2025 12:28:26 -0400 Subject: [PATCH 2/4] new components --- .../actions/create-order/create-order.mjs | 152 ++++++ .../get-historical-prices.mjs | 125 +++++ .../oanda/actions/list-trades/list-trades.mjs | 71 +++ components/oanda/common/constants.mjs | 445 ++++++++++++++++++ components/oanda/oanda.app.mjs | 290 +++--------- components/oanda/package.json | 3 +- components/oanda/sources/common/base.mjs | 56 +++ .../new-open-position/new-open-position.mjs | 37 ++ .../sources/new-open-position/test-event.mjs | 36 ++ .../sources/new-open-trade/new-open-trade.mjs | 50 ++ .../sources/new-open-trade/test-event.mjs | 15 + .../new-transaction/new-transaction.mjs | 67 +++ .../sources/new-transaction/test-event.mjs | 14 + 13 files changed, 1148 insertions(+), 213 deletions(-) create mode 100644 components/oanda/actions/create-order/create-order.mjs create mode 100644 components/oanda/actions/get-historical-prices/get-historical-prices.mjs create mode 100644 components/oanda/actions/list-trades/list-trades.mjs create mode 100644 components/oanda/common/constants.mjs create mode 100644 components/oanda/sources/common/base.mjs create mode 100644 components/oanda/sources/new-open-position/new-open-position.mjs create mode 100644 components/oanda/sources/new-open-position/test-event.mjs create mode 100644 components/oanda/sources/new-open-trade/new-open-trade.mjs create mode 100644 components/oanda/sources/new-open-trade/test-event.mjs create mode 100644 components/oanda/sources/new-transaction/new-transaction.mjs create mode 100644 components/oanda/sources/new-transaction/test-event.mjs diff --git a/components/oanda/actions/create-order/create-order.mjs b/components/oanda/actions/create-order/create-order.mjs new file mode 100644 index 0000000000000..bc2acf160640f --- /dev/null +++ b/components/oanda/actions/create-order/create-order.mjs @@ -0,0 +1,152 @@ +import oanda from "../../oanda.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "oanda-create-order", + name: "Create Order", + description: "Create a new trading order. [See the documentation](https://developer.oanda.com/rest-live-v20/order-ep/)", + version: "0.0.1", + type: "action", + props: { + oanda, + isDemo: { + propDefinition: [ + oanda, + "isDemo", + ], + }, + accountId: { + propDefinition: [ + oanda, + "accountId", + (c) => ({ + isDemo: c.isDemo, + }), + ], + }, + type: { + type: "string", + label: "Type", + description: "The type of the order to create", + options: constants.ORDER_TYPES, + reloadProps: true, + }, + tradeID: { + propDefinition: [ + oanda, + "tradeId", + (c) => ({ + isDemo: c.isDemo, + accountId: c.accountId, + }), + ], + optional: true, + hidden: true, + }, + instrument: { + propDefinition: [ + oanda, + "instrument", + ], + description: "The instrument for the order. E.g. `AUD_USD`", + optional: true, + hidden: true, + }, + units: { + type: "integer", + label: "Units", + description: "The quantity requested to be filled by the Order. A positive number of units results in a long Order, and a negative number of units results in a short Order.", + optional: true, + hidden: true, + }, + price: { + type: "string", + label: "Price", + description: "The price threshold specified for the Limit Order. The Order will only be filled by a market price that is equal to or better than this price.", + optional: true, + hidden: true, + }, + distance: { + type: "string", + label: "Distance", + description: "Specifies the distance (in price units) from the Account’s current price to use as the Stop Loss Order price. If the Trade is short the Instrument’s bid price is used, and for long Trades the ask is used", + optional: true, + hidden: true, + }, + timeInForce: { + type: "string", + label: "Time in Force", + description: "The time-in-force requested for the Order. Restricted to FOK or IOC for a MarketOrder.", + options: constants.TIME_IN_FORCE_OPTIONS, + optional: true, + hidden: true, + }, + gtdTime: { + type: "string", + label: "GTD Time", + description: "The date/time when the Stop Order will be cancelled if its timeInForce is \"GTD\".", + optional: true, + hidden: true, + }, + priceBound: { + type: "string", + label: "Price Bound", + description: "The worst price that the client is willing to have the Order filled at", + optional: true, + hidden: true, + }, + positionFill: { + type: "string", + label: "Position Fill", + description: "Specification of how Positions in the Account are modified when the Order is filled", + options: constants.POSITION_FILL_OPTIONS, + optional: true, + hidden: true, + }, + triggerCondition: { + type: "string", + label: "Trigger Condition", + description: "Specification of which price component should be used when determining if an Order should be triggered and filled", + options: constants.ORDER_TRIGGER_CONDITIONS, + optional: true, + hidden: true, + }, + }, + additionalProps(props) { + if (!this.type) { + return {}; + } + const fields = constants.REQUIRED_FIELDS_FOR_ORDER_TYPES[this.type]; + for (const [ + key, + value, + ] of Object.entries(fields)) { + props[key].hidden = false; + props[key].optional = !value; + } + return {}; + }, + async run({ $ }) { + const { + oanda, + isDemo, + accountId, + type, + ...fields + } = this; + + const response = await oanda.createOrder({ + $, + isDemo, + accountId, + data: { + order: { + type, + ...fields, + }, + }, + }); + $.export("$summary", "Successfully created order"); + return response; + }, +}; diff --git a/components/oanda/actions/get-historical-prices/get-historical-prices.mjs b/components/oanda/actions/get-historical-prices/get-historical-prices.mjs new file mode 100644 index 0000000000000..ce1012fcdfa83 --- /dev/null +++ b/components/oanda/actions/get-historical-prices/get-historical-prices.mjs @@ -0,0 +1,125 @@ +import oanda from "../../oanda.app.mjs"; +import constants from "../../common/constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "oanda-get-historical-prices", + name: "Get Historical Prices", + description: "Retrieve historical price data for a specified currency pair or instrument within a given time range. [See the documentation](https://developer.oanda.com/rest-live-v20/pricing-ep/)", + version: "0.0.1", + type: "action", + props: { + oanda, + isDemo: { + propDefinition: [ + oanda, + "isDemo", + ], + }, + accountId: { + propDefinition: [ + oanda, + "accountId", + (c) => ({ + isDemo: c.isDemo, + }), + ], + }, + instrument: { + propDefinition: [ + oanda, + "instrument", + ], + }, + startTime: { + type: "string", + label: "Start Time", + description: "The start time for historical price data (ISO 8601 format). E.g. `2025-04-01T04:00:00.000000000Z`", + }, + endTime: { + type: "string", + label: "End Time", + description: "The end time for historical price data (ISO 8601 format). E.g. `2025-04-01T04:00:00.000000000Z`", + }, + price: { + type: "string", + label: "Price", + description: "The Price component(s) to get candlestick data for. Can contain any combination of the characters “M” (midpoint candles) “B” (bid candles) and “A” (ask candles).", + optional: true, + }, + granularity: { + type: "string", + label: "Granularity", + description: "The granularity of the candlesticks to fetch", + options: constants.CANDLE_GRANULARITIES, + optional: true, + }, + smooth: { + type: "boolean", + label: "Smooth", + description: "A flag that controls whether the candlestick is “smoothed” or not. A smoothed candlestick uses the previous candle’s close price as its open price, while an unsmoothed candlestick uses the first price from its time range as its open price.", + optional: true, + }, + includeFirst: { + type: "boolean", + label: "Include First", + description: "A flag that controls whether the candlestick that is covered by the from time should be included in the results. This flag enables clients to use the timestamp of the last completed candlestick received to poll for future candlesticks but avoid receiving the previous candlestick repeatedly.", + optional: true, + }, + dailyAlignment: { + type: "integer", + label: "Daily Alignment", + description: "The hour of the day (in the specified timezone) to use for granularities that have daily alignments. minimum=0, maximum=23", + optional: true, + }, + alignmentTimezone: { + type: "string", + label: "Alignment Timezone", + description: "The timezone to use for the dailyAlignment parameter. Candlesticks with daily alignment will be aligned to the dailyAlignment hour within the alignmentTimezone. Note that the returned times will still be represented in UTC. [default=America/New_York]", + optional: true, + }, + weeklyAlignment: { + type: "string", + label: "Weekly Alignment", + description: "The day of the week used for granularities that have weekly alignment. [default=Friday]", + options: constants.WEEKLY_ALIGNMENT_DAYS, + optional: true, + }, + units: { + type: "integer", + label: "Units", + description: "The number of units used to calculate the volume-weighted average bid and ask prices in the returned candles. [default=1]", + optional: true, + }, + }, + async run({ $ }) { + try { + const response = await this.oanda.getHistoricalPrices({ + $, + isDemo: this.isDemo, + accountId: this.accountId, + instrument: this.instrument, + params: { + price: this.price, + granularity: this.granularity, + from: this.startTime, + to: this.endTime, + smooth: this.smooth, + includeFirst: this.includeFirst, + dailyAlignment: this.dailyAlignment, + alignmentTimezone: this.alignmentTimezone, + weeklyAlignment: this.weeklyAlignment, + units: this.units, + }, + }); + $.export("$summary", `Successfully retrieved ${response.candles.length} trade${response.candles.length === 1 + ? "" + : "s"}`); + return response; + } catch (error) { + if (error?.message.includes("Maximum value for 'count' exceeded")) { + throw new ConfigurationError("Maximum results exceeded. Update the time range or granularity to return fewer results."); + } + } + }, +}; diff --git a/components/oanda/actions/list-trades/list-trades.mjs b/components/oanda/actions/list-trades/list-trades.mjs new file mode 100644 index 0000000000000..06268cac8eed0 --- /dev/null +++ b/components/oanda/actions/list-trades/list-trades.mjs @@ -0,0 +1,71 @@ +import oanda from "../../oanda.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "oanda-list-trades", + name: "List Trades", + description: "Retrieve a list of trades for an account. [See the documentation](https://developer.oanda.com/rest-live-v20/trade-ep/)", + version: "0.0.1", + type: "action", + props: { + oanda, + isDemo: { + propDefinition: [ + oanda, + "isDemo", + ], + }, + accountId: { + propDefinition: [ + oanda, + "accountId", + (c) => ({ + isDemo: c.isDemo, + }), + ], + }, + state: { + type: "string", + label: "State", + description: "Filter trades by state", + options: constants.TRADE_STATES, + optional: true, + }, + instrument: { + propDefinition: [ + oanda, + "instrument", + ], + optional: true, + }, + count: { + type: "integer", + label: "Count", + description: "The maximum number of Trades to return. [default=50, maximum=500]", + optional: true, + }, + beforeId: { + type: "string", + label: "Before ID", + description: "The maximum Trade ID to return. If not provided the most recent Trades in the Account are returned.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.oanda.listTrades({ + $, + isDemo: this.isDemo, + accountId: this.accountId, + params: { + state: this.state, + instrument: this.instrument, + count: this.count, + beforeID: this.beforeId, + }, + }); + $.export("$summary", `Successfully retrieved ${response.trades.length} trade${response.trades.length === 1 + ? "" + : "s"}`); + return response; + }, +}; diff --git a/components/oanda/common/constants.mjs b/components/oanda/common/constants.mjs new file mode 100644 index 0000000000000..1ef6cee265097 --- /dev/null +++ b/components/oanda/common/constants.mjs @@ -0,0 +1,445 @@ +const DEFAULT_LIMIT = 1000; + +const REQUIRED_FIELDS_FOR_ORDER_TYPES = { + MARKET: { + instrument: true, + units: true, + timeInForce: true, + positionFill: true, + priceBound: false, + }, + LIMIT: { + instrument: true, + units: true, + price: true, + timeInForce: true, + positionFill: true, + triggerCondition: true, + gtdTime: false, + }, + STOP: { + instrument: true, + units: true, + price: true, + timeInForce: true, + positionFill: true, + triggerCondition: true, + gtdTime: false, + }, + MARKET_IF_TOUCHED: { + instrument: true, + units: true, + price: true, + timeInForce: true, + positionFill: true, + triggerCondition: true, + priceBound: false, + gtdTime: false, + }, + TAKE_PROFIT: { + tradeID: true, + price: true, + timeInForce: true, + triggerCondition: true, + gtdTime: false, + }, + STOP_LOSS: { + tradeID: true, + price: true, + timeInForce: true, + triggerCondition: true, + distance: false, + gtdTime: false, + }, + GUARANTEED_STOP_LOSS: { + tradeID: true, + price: true, + timeInForce: true, + triggerCondition: true, + distance: false, + gtdTime: false, + }, + TRAILING_STOP_LOSS: { + tradeID: true, + distance: true, + timeInForce: true, + triggerCondition: true, + gtdTime: false, + }, +}; + +const TRADE_STATES = [ + { + label: "The Trades that are currently open", + value: "OPEN", + }, + { + label: "The Trades that have been fully closed", + value: "CLOSED", + }, + { + label: "The Trades that will be closed as soon as the trades’ instrument becomes tradeable", + value: "CLOSE_WHEN_TRADEABLE", + }, + { + label: "The Trades that are in any of the possible states listed above", + value: "ALL", + }, +]; + +const CANDLE_GRANULARITIES = [ + { + value: "S5", + label: "5 second candlesticks, minute alignment", + }, + { + value: "S10", + label: "10 second candlesticks, minute alignment", + }, + { + value: "S15", + label: "15 second candlesticks, minute alignment", + }, + { + value: "S30", + label: "30 second candlesticks, minute alignment", + }, + { + value: "M1", + label: "1 minute candlesticks, minute alignment", + }, + { + value: "M2", + label: "2 minute candlesticks, hour alignment", + }, + { + value: "M4", + label: "4 minute candlesticks, hour alignment", + }, + { + value: "M5", + label: "5 minute candlesticks, hour alignment", + }, + { + value: "M10", + label: "10 minute candlesticks, hour alignment", + }, + { + value: "M15", + label: "15 minute candlesticks, hour alignment", + }, + { + value: "M30", + label: "30 minute candlesticks, hour alignment", + }, + { + value: "H1", + label: "1 hour candlesticks, hour alignment", + }, + { + value: "H2", + label: "2 hour candlesticks, day alignment", + }, + { + value: "H3", + label: "3 hour candlesticks, day alignment", + }, + { + value: "H4", + label: "4 hour candlesticks, day alignment", + }, + { + value: "H6", + label: "6 hour candlesticks, day alignment", + }, + { + value: "H8", + label: "8 hour candlesticks, day alignment", + }, + { + value: "H12", + label: "12 hour candlesticks, day alignment", + }, + { + value: "D", + label: "1 day candlesticks, day alignment", + }, + { + value: "W", + label: "1 week candlesticks, aligned to start of week", + }, + { + value: "M", + label: "1 month candlesticks, aligned to first day of the month", + }, +]; + +const WEEKLY_ALIGNMENT_DAYS = [ + "Monday", + "Tuesday", + "Wednesday", + "Thrusday", + "Friday", + "Saturday", + "Sunday", +]; + +const ORDER_TYPES = [ + "MARKET", + "LIMIT", + "STOP", + "MARKET_IF_TOUCHED", + "TAKE_PROFIT", + "STOP_LOSS", + "GUARANTEED_STOP_LOSS", + "TRAILING_STOP_LOSS", +]; + +const TIME_IN_FORCE_OPTIONS = [ + { + value: "GTC", + label: "The Order is \"Good until Cancelled\"", + }, + { + value: "GTD", + label: "The Order is \"Good until Date\" and will be cancelled at the provided time", + }, + { + value: "GFD", + label: "The Order is \"Good For Day\" and will be cancelled at 5pm New York time", + }, + { + value: "FOK", + label: "The Order must be immediately \"Filled Or Killed\"", + }, + { + value: "IOC", + label: "The Order must be \"Immediately partially filled Or Cancelled\"", + }, +]; + +const POSITION_FILL_OPTIONS = [ + { + value: "OPEN_ONLY", + label: "When the Order is filled, only allow Positions to be opened or extended", + }, + { + value: "REDUCE_FIRST", + label: "When the Order is filled, always fully reduce an existing Position before opening a new Position", + }, + { + value: "REDUCE_ONLY", + label: "When the Order is filled, only reduce an existing Position", + }, + { + value: "DEFAULT", + label: "When the Order is filled, use REDUCE_FIRST behaviour for non-client hedging Accounts, and OPEN_ONLY behaviour for client hedging Accounts", + }, +]; + +const ORDER_TRIGGER_CONDITIONS = [ + { + value: "DEFAULT", + label: "Trigger an Order the \"natural\" way: compare its price to the ask for long Orders and bid for short Orders.", + }, + { + value: "INVERSE", + label: "Trigger an Order the opposite of the \"natural\" way: compare its price the bid for long Orders and ask for short Orders.", + }, + { + value: "BID", + label: "Trigger an Order by comparing its price to the bid regardless of whether it is long or short.", + }, + { + value: "ASK", + label: "Trigger an Order by comparing its price to the ask regardless of whether it is long or short.", + }, + { + value: "MID", + label: "Trigger an Order by comparing its price to the midpoint regardless of whether it is long or short.", + }, +]; + +const TRANSACTION_TYPES = [ + { + value: "ORDER", + label: "Order-related Transactions. These are the Transactions that create, cancel, fill or trigger Orders", + }, + { + value: "FUNDING", + label: "Funding-related Transactions", + }, + { + value: "ADMIN", + label: "Administrative Transactions", + }, + { + value: "CREATE", + label: "Account Create Transaction", + }, + { + value: "CLOSE", + label: "Account Close Transaction", + }, + { + value: "REOPEN", + label: "Account Reopen Transaction", + }, + { + value: "CLIENT_CONFIGURE", + label: "Client Configuration Transaction", + }, + { + value: "CLIENT_CONFIGURE_REJECT", + label: "Client Configuration Reject Transaction", + }, + { + value: "TRANSFER_FUNDS", + label: "Transfer Funds Transaction", + }, + { + value: "TRANSFER_FUNDS_REJECT", + label: "Transfer Funds Reject Transaction", + }, + { + value: "MARKET_ORDER", + label: "Market Order Transaction", + }, + { + value: "MARKET_ORDER_REJECT", + label: "Market Order Reject Transaction", + }, + { + value: "LIMIT_ORDER", + label: "Limit Order Transaction", + }, + { + value: "LIMIT_ORDER_REJECT", + label: "Limit Order Reject Transaction", + }, + { + value: "STOP_ORDER", + label: "Stop Order Transaction", + }, + { + value: "STOP_ORDER_REJECT", + label: "Stop Order Reject Transaction", + }, + { + value: "MARKET_IF_TOUCHED_ORDER", + label: "Market if Touched Order Transaction", + }, + { + value: "MARKET_IF_TOUCHED_ORDER_REJECT", + label: "Market if Touched Order Reject Transaction", + }, + { + value: "TAKE_PROFIT_ORDER", + label: "Take Profit Order Transaction", + }, + { + value: "TAKE_PROFIT_ORDER_REJECT", + label: "Take Profit Order Reject Transaction", + }, + { + value: "STOP_LOSS_ORDER", + label: "Stop Loss Order Transaction", + }, + { + value: "STOP_LOSS_ORDER_REJECT", + label: "Stop Loss Order Reject Transaction", + }, + { + value: "GUARANTEED_STOP_LOSS_ORDER", + label: "Guaranteed Stop Loss Order Transaction", + }, + { + value: "GUARANTEED_STOP_LOSS_ORDER_REJECT", + label: "Guaranteed Stop Loss Order Reject Transaction", + }, + { + value: "TRAILING_STOP_LOSS_ORDER", + label: "Trailing Stop Loss Order Transaction", + }, + { + value: "TRAILING_STOP_LOSS_ORDER_REJECT", + label: "Trailing Stop Loss Order Reject Transaction", + }, + { + value: "ONE_CANCELS_ALL_ORDER", + label: "One Cancels All Order Transaction", + }, + { + value: "ONE_CANCELS_ALL_ORDER_REJECT", + label: "One Cancels All Order Reject Transaction", + }, + { + value: "ONE_CANCELS_ALL_ORDER_TRIGGERED", + label: "One Cancels All Order Trigger Transaction", + }, + { + value: "ORDER_FILL", + label: "Order Fill Transaction", + }, + { + value: "ORDER_CANCEL", + label: "Order Cancel Transaction", + }, + { + value: "ORDER_CANCEL_REJECT", + label: "Order Cancel Reject Transaction", + }, + { + value: "ORDER_CLIENT_EXTENSIONS_MODIFY", + label: "Order Client Extensions Modify Transaction", + }, + { + value: "ORDER_CLIENT_EXTENSIONS_MODIFY_REJECT", + label: "Order Client Extensions Modify Reject Transaction", + }, + { + value: "TRADE_CLIENT_EXTENSIONS_MODIFY", + label: "Trade Client Extensions Modify Transaction", + }, + { + value: "TRADE_CLIENT_EXTENSIONS_MODIFY_REJECT", + label: "Trade Client Extensions Modify Reject Transaction", + }, + { + value: "MARGIN_CALL_ENTER", + label: "Margin Call Enter Transaction", + }, + { + value: "MARGIN_CALL_EXTEND", + label: "Margin Call Extend Transaction", + }, + { + value: "MARGIN_CALL_EXIT", + label: "Margin Call Exit Transaction", + }, + { + value: "DELAYED_TRADE_CLOSURE", + label: "Delayed Trade Closure Transaction", + }, + { + value: "DAILY_FINANCING", + label: "Daily Financing Transaction", + }, + { + value: "RESET_RESETTABLE_PL", + label: "Reset Resettable PL Transaction", + }, +]; + +export default { + DEFAULT_LIMIT, + REQUIRED_FIELDS_FOR_ORDER_TYPES, + TRADE_STATES, + CANDLE_GRANULARITIES, + WEEKLY_ALIGNMENT_DAYS, + ORDER_TYPES, + TIME_IN_FORCE_OPTIONS, + POSITION_FILL_OPTIONS, + ORDER_TRIGGER_CONDITIONS, + TRANSACTION_TYPES, +}; diff --git a/components/oanda/oanda.app.mjs b/components/oanda/oanda.app.mjs index 7a7bcce3f2e21..8406b8f69bf1c 100644 --- a/components/oanda/oanda.app.mjs +++ b/components/oanda/oanda.app.mjs @@ -4,146 +4,52 @@ export default { type: "app", app: "oanda", propDefinitions: { - baseCurrency: { - type: "string", - label: "Base Currency", - description: "The base currency of the currency pair.", - }, - quoteCurrency: { - type: "string", - label: "Quote Currency", - description: "The quote currency of the currency pair.", - }, - changeThreshold: { - type: "number", - label: "Change Threshold", - description: "The threshold for significant change in forex rate to trigger the event.", + isDemo: { + type: "boolean", + label: "Is Demo", + description: "Set to `true` if using a demo/practice account", }, - instrumentFilter: { + accountId: { type: "string", - label: "Instrument Filter", - description: "Filter by instrument.", - optional: true, - }, - tradeTypeFilter: { - type: "string", - label: "Trade Type Filter", - description: "Filter by trade type.", - optional: true, - options: [ - { - label: "Buy", - value: "BUY", - }, - { - label: "Sell", - value: "SELL", - }, - ], + label: "Account ID", + description: "The identifier of an account", + async options({ isDemo }) { + const { accounts } = await this.listAccounts({ + isDemo, + }); + return accounts?.map(({ id }) => id) || []; + }, }, - orderTypeFilter: { + tradeId: { type: "string", - label: "Order Type Filter", - description: "Filter by order type.", - optional: true, - options: [ - { - label: "Market", - value: "MARKET", - }, - { - label: "Limit", - value: "LIMIT", - }, - { - label: "Stop", - value: "STOP", - }, - ], + label: "Trade ID", + description: "The ID of the Trade to close when the price threshold is breached", + async options({ + isDemo, accountId, prevContext, + }) { + const { + trades, lastTransactionID, + } = await this.listTrades({ + isDemo, + accountId, + params: prevContext?.beforeID + ? { + beforeID: prevContext.beforeID, + } + : {}, + }); + return { + options: trades?.map(({ id }) => id), + context: { + beforeID: lastTransactionID, + }, + }; + }, }, instrument: { type: "string", - label: "Instrument", - description: "The financial instrument to be used.", - }, - orderType: { - type: "string", - label: "Order Type", - description: "Type of the order.", - options: [ - { - label: "Market", - value: "MARKET", - }, - { - label: "Limit", - value: "LIMIT", - }, - { - label: "Stop", - value: "STOP", - }, - ], - }, - units: { - type: "integer", - label: "Units", - description: "Number of units to trade.", - }, - priceThreshold: { - type: "number", - label: "Price Threshold", - description: "Optional price threshold for the order.", - optional: true, - }, - timeInForce: { - type: "string", - label: "Time In Force", - description: "Optional time in force for the order.", - optional: true, - options: [ - { - label: "Good Till Cancelled (GTC)", - value: "GTC", - }, - { - label: "Immediate Or Cancel (IOC)", - value: "IOC", - }, - { - label: "Fill Or Kill (FOK)", - value: "FOK", - }, - ], - }, - granularity: { - type: "string", - label: "Granularity", - description: "The granularity of the historical price data.", - options: [ - { - label: "Minute", - value: "M1", - }, - { - label: "Hour", - value: "H1", - }, - { - label: "Daily", - value: "D", - }, - ], - }, - startTime: { - type: "string", - label: "Start Time", - description: "The start time for historical price data (ISO 8601 format).", - }, - endTime: { - type: "string", - label: "End Time", - description: "The end time for historical price data (ISO 8601 format).", + label: "Instrument Name", + description: "The instrument to filter the requested Trades by. E.g. `AUD_USD`", }, }, methods: { @@ -166,100 +72,60 @@ export default { }, }); }, - async listTrades(opts = {}) { - const { - instrumentFilter, tradeTypeFilter, ...otherOpts - } = opts; - const params = {}; - if (tradeTypeFilter) { - params.type = tradeTypeFilter; - } - if (instrumentFilter) { - params.instrument = instrumentFilter; - } + listAccounts(opts = {}) { return this._makeRequest({ - path: `/accounts/${this.$auth.account_id}/trades`, - params, - ...otherOpts, + path: "/accounts", + ...opts, }); }, - async createOrder(opts = {}) { - const { - instrument, orderType, units, priceThreshold, timeInForce, ...otherOpts - } = opts; - const data = { - order: { - type: orderType, - instrument: instrument, - units: units, - timeInForce: timeInForce || "FOK", - }, - }; - if (priceThreshold) { - data.order.price = priceThreshold; - } + listTrades({ + accountId, ...opts + }) { return this._makeRequest({ - method: "POST", - path: `/accounts/${this.$auth.account_id}/orders`, - data, - ...otherOpts, + path: `/accounts/${accountId}/trades`, + ...opts, }); }, - async getHistoricalPrices(opts = {}) { - const { - instrument, granularity, startTime, endTime, ...otherOpts - } = opts; - const params = { - granularity: granularity, - from: startTime, - to: endTime, - }; + listOpenTrades({ + accountId, ...opts + }) { return this._makeRequest({ - path: `/instruments/${instrument}/candles`, - params, - ...otherOpts, + path: `/accounts/${accountId}/openTrades`, + ...opts, }); }, - async getOrderStatus(opts = {}) { - const { - orderTypeFilter, ...otherOpts - } = opts; - const params = {}; - if (orderTypeFilter) { - params.type = orderTypeFilter; - } + listOpenPositions({ + accountId, ...opts + }) { return this._makeRequest({ - path: `/accounts/${this.$auth.account_id}/orders`, - params, - ...otherOpts, + path: `/accounts/${accountId}/openPositions`, + ...opts, }); }, - async getPricingData(opts = {}) { - const { - baseCurrency, quoteCurrency, ...otherOpts - } = opts; - const instrument = `${baseCurrency}_${quoteCurrency}`; - const params = { - instruments: instrument, - }; + listTransactions({ + accountId, ...opts + }) { return this._makeRequest({ - path: "/pricing", - params, - ...otherOpts, + path: `/accounts/${accountId}/transactions/idrange`, + ...opts, }); }, - async paginate(fn, ...opts) { - let results = []; - let response = await fn(...opts); - results = results.concat(response.trades || response.orders || []); - while (response.nextPage) { - response = await fn({ - page: response.nextPage, - ...opts, - }); - results = results.concat(response.trades || response.orders || []); - } - return results; + getHistoricalPrices({ + accountId, instrument, ...opts + }) { + return this._makeRequest({ + path: `/accounts/${accountId}/instruments/${instrument}/candles`, + ...opts, + }); + }, + createOrder({ + accountId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/accounts/${accountId}/orders`, + ...opts, + }); }, }, }; diff --git a/components/oanda/package.json b/components/oanda/package.json index 20ee1ffa005f8..4c8204d4f4e80 100644 --- a/components/oanda/package.json +++ b/components/oanda/package.json @@ -13,6 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^3.0.0" + "@pipedream/platform": "^3.0.3", + "md5": "^2.3.0" } } diff --git a/components/oanda/sources/common/base.mjs b/components/oanda/sources/common/base.mjs new file mode 100644 index 0000000000000..5d810aee0df3c --- /dev/null +++ b/components/oanda/sources/common/base.mjs @@ -0,0 +1,56 @@ +import oanda from "../../oanda.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + oanda, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + isDemo: { + propDefinition: [ + oanda, + "isDemo", + ], + }, + accountId: { + propDefinition: [ + oanda, + "accountId", + (c) => ({ + isDemo: c.isDemo, + }), + ], + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 1; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + emitItem(item) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }, + processEvent() { + throw new Error("processEvent is not iplemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/oanda/sources/new-open-position/new-open-position.mjs b/components/oanda/sources/new-open-position/new-open-position.mjs new file mode 100644 index 0000000000000..0a87299f99b4e --- /dev/null +++ b/components/oanda/sources/new-open-position/new-open-position.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; +import md5 from "md5"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "oanda-new-open-position", + name: "New Open Position", + description: "Emit new event when a new open position is created or updated in an Oanda account. [See the documentation](https://developer.oanda.com/rest-live-v20/position-ep/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + async processEvent() { + const { positions } = await this.oanda.listOpenPositions({ + isDemo: this.isDemo, + accountId: this.accountId, + }); + + if (!positions.length) { + return; + } + + positions.reverse().forEach((position) => this.emitItem(position)); + }, + generateMeta(item) { + const hash = md5(JSON.stringify(item)); + return { + id: hash, + summary: `New or Updated Position for instrument ${item.instrument}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/oanda/sources/new-open-position/test-event.mjs b/components/oanda/sources/new-open-position/test-event.mjs new file mode 100644 index 0000000000000..cfa4779caa877 --- /dev/null +++ b/components/oanda/sources/new-open-position/test-event.mjs @@ -0,0 +1,36 @@ +export default { + "instrument": "AUD_USD", + "long": { + "units": "4", + "averagePrice": "0.64333", + "pl": "0.0000", + "resettablePL": "0.0000", + "financing": "0.0000", + "dividendAdjustment": "0.0000", + "guaranteedExecutionFees": "0.0000", + "tradeIDs": [ + "6", + "9", + "19", + "21" + ], + "unrealizedPL": "0.0074" + }, + "short": { + "units": "0", + "pl": "0.0000", + "resettablePL": "0.0000", + "financing": "0.0000", + "dividendAdjustment": "0.0000", + "guaranteedExecutionFees": "0.0000", + "unrealizedPL": "0.0000" + }, + "pl": "0.0000", + "resettablePL": "0.0000", + "financing": "0.0000", + "commission": "0.0000", + "dividendAdjustment": "0.0000", + "guaranteedExecutionFees": "0.0000", + "unrealizedPL": "0.0074", + "marginUsed": "0.0774" +} \ No newline at end of file diff --git a/components/oanda/sources/new-open-trade/new-open-trade.mjs b/components/oanda/sources/new-open-trade/new-open-trade.mjs new file mode 100644 index 0000000000000..e3dba84892f67 --- /dev/null +++ b/components/oanda/sources/new-open-trade/new-open-trade.mjs @@ -0,0 +1,50 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "oanda-new-open-trade", + name: "New Open Trade", + description: "Emit new event when a new trade is opened in an Oanda account. [See the documentation](https://developer.oanda.com/rest-live-v20/trade-ep/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + async processEvent(max) { + const lastId = this._getLastId(); + let count = 0; + + const { + trades, lastTransactionID, + } = await this.oanda.listOpenTrades({ + isDemo: this.isDemo, + accountId: this.accountId, + }); + + if (!trades.length) { + return; + } + + const newTrades = trades.filter(({ id }) => id > lastId); + + for (const trade of newTrades.reverse()) { + this.emitItem(trade); + count++; + if (max && count >= max) { + break; + } + } + + this._setLastId(lastTransactionID); + }, + generateMeta(item) { + return { + id: item.id, + summary: `New Open Trade with ID: ${item.id}`, + ts: Date.parse(item.openTime), + }; + }, + }, + sampleEmit, +}; diff --git a/components/oanda/sources/new-open-trade/test-event.mjs b/components/oanda/sources/new-open-trade/test-event.mjs new file mode 100644 index 0000000000000..64058bb33f48a --- /dev/null +++ b/components/oanda/sources/new-open-trade/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "id": "6", + "instrument": "AUD_USD", + "price": "0.63813", + "openTime": "2025-05-01T20:29:23.781003632Z", + "initialUnits": "1", + "initialMarginRequired": "0.0191", + "state": "OPEN", + "currentUnits": "1", + "realizedPL": "0.0000", + "financing": "0.0000", + "dividendAdjustment": "0.0000", + "unrealizedPL": "0.0062", + "marginUsed": "0.0193" +} \ No newline at end of file diff --git a/components/oanda/sources/new-transaction/new-transaction.mjs b/components/oanda/sources/new-transaction/new-transaction.mjs new file mode 100644 index 0000000000000..27af847a0c602 --- /dev/null +++ b/components/oanda/sources/new-transaction/new-transaction.mjs @@ -0,0 +1,67 @@ +import common from "../common/base.mjs"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "oanda-new-transaction", + name: "New Transaction", + description: "Emit new event whenever a trade is executed in the user's account. [See the documentation](https://developer.oanda.com/rest-live-v20/transaction-ep/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + type: { + type: "string", + label: "Type", + description: "Filter results by transaction type", + options: constants.TRANSACTION_TYPES, + optional: true, + }, + }, + methods: { + ...common.methods, + async processEvent(max) { + const lastId = this._getLastId(); + const params = { + type: this.type, + from: lastId, + to: lastId + constants.DEFAULT_LIMIT, + }; + let total, results = []; + + do { + const { transactions } = await this.oanda.listTransactions({ + isDemo: this.isDemo, + accountId: this.accountId, + params, + }); + results.push(...transactions); + total = transactions?.length; + params.from = params.to + 1; + params.to += constants.DEFAULT_LIMIT; + } while (total === constants.DEFAULT_LIMIT); + + if (!results.length) { + return; + } + + if (max && results.length > max) { + results = results.slice(-1 * max); + } + + this._setLastId(+results[results.length - 1].id); + + results.forEach((item) => this.emitItem(item)); + }, + generateMeta(item) { + return { + id: item.id, + summary: `New ${item.type} transaction`, + ts: Date.parse(item.time), + }; + }, + }, + sampleEmit, +}; diff --git a/components/oanda/sources/new-transaction/test-event.mjs b/components/oanda/sources/new-transaction/test-event.mjs new file mode 100644 index 0000000000000..527561eb8a68e --- /dev/null +++ b/components/oanda/sources/new-transaction/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "id": "1", + "accountID": "101-001-31625911-001", + "userID": 31625911, + "batchID": "1", + "requestID": "1592590512907732642", + "time": "2025-04-30T20:32:10.750971031Z", + "type": "CREATE", + "divisionID": 1, + "siteID": 101, + "accountUserID": 31625911, + "accountNumber": 1, + "homeCurrency": "USD" +} \ No newline at end of file From 6770547459f32302754defc68cdd670325510847 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Fri, 2 May 2025 12:30:25 -0400 Subject: [PATCH 3/4] pnpm-lock.yaml --- pnpm-lock.yaml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5f890e57c6ac..97fe117cab148 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4095,8 +4095,7 @@ importers: specifier: ^1.3.0 version: 1.6.6 - components/emailverify_io: - specifiers: {} + components/emailverify_io: {} components/emelia: {} @@ -8814,8 +8813,11 @@ importers: components/oanda: dependencies: '@pipedream/platform': - specifier: ^3.0.0 + specifier: ^3.0.3 version: 3.0.3 + md5: + specifier: ^2.3.0 + version: 2.3.0 components/oauth_app_demo: {} @@ -12796,8 +12798,7 @@ importers: specifier: ^1.2.0 version: 1.6.6 - components/swarmnode: - specifiers: {} + components/swarmnode: {} components/swell: dependencies: @@ -34753,6 +34754,8 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) + transitivePeerDependencies: + - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: From f4f2f001aae81d110401cd8db6cbbffd0439a233 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Fri, 2 May 2025 12:45:05 -0400 Subject: [PATCH 4/4] update --- .../actions/get-historical-prices/get-historical-prices.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/oanda/actions/get-historical-prices/get-historical-prices.mjs b/components/oanda/actions/get-historical-prices/get-historical-prices.mjs index ce1012fcdfa83..569c36dd9b6d0 100644 --- a/components/oanda/actions/get-historical-prices/get-historical-prices.mjs +++ b/components/oanda/actions/get-historical-prices/get-historical-prices.mjs @@ -119,6 +119,9 @@ export default { } catch (error) { if (error?.message.includes("Maximum value for 'count' exceeded")) { throw new ConfigurationError("Maximum results exceeded. Update the time range or granularity to return fewer results."); + } else { + console.error("Error retrieving historical prices:", error); + throw error; } } },