├── .gitignore ├── lib └── endpoints │ ├── pricing.js │ ├── transactions.js │ ├── positions.js │ ├── instruments.js │ ├── trades.js │ ├── accounts.js │ └── orders.js ├── package.json ├── Oanda.js ├── tests └── formatter.js ├── examples.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/config.js 2 | /lib/setup.js -------------------------------------------------------------------------------- /lib/endpoints/pricing.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const Pricing = {} 3 | 4 | /* GET REQUEST for pricing of a instrument between a given time 5 | options: { 6 | instruments: array (e.g ['USD_CAD', 'EUR_USD']) 7 | time: dateTime 8 | } 9 | */ 10 | Pricing.getPricing = function (options) { 11 | var url = this.accountURL + '/pricing' + this.formatQuery(options) 12 | return axios.get(url, this.header) 13 | } 14 | 15 | module.exports = Pricing -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oanda-node-api", 3 | "version": "0.1.0", 4 | "description": "A NodeJS Promise based API wrapper for Oanda trading platform", 5 | "main": "Oanda.js", 6 | "scripts": { 7 | "test": "mocha tests --recursive --watch" 8 | }, 9 | "author": "Eric Abrahams", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^0.21.1", 13 | "chai": "^4.1.2" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/escwdev/oanda-node-api.git" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/endpoints/transactions.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const Transactions = {} 3 | 4 | 5 | /* GET REQUEST for transactions 6 | options = { 7 | from: dateTime, 8 | to: dateTime, 9 | pageSize: integer, [100-1000] 10 | type: array 11 | } 12 | */ 13 | Transactions.getTransactions = function(options) { 14 | let url = this.accountURL + '/transactions/' + this.formatQuery(options) 15 | return axios.get(url, this.header) 16 | } 17 | 18 | // GET REQUEST transaction data by id 19 | // TradeID = integer 20 | Transactions.getTransactionByID = function(transactionID) { 21 | let url = this.accountURL + `/transactions/${transactionID}` 22 | return axios.get(url, this.header) 23 | } 24 | 25 | /* GET REQUEST for range of transactions between two trade IDs 26 | options = { 27 | to: integer, 28 | from: integer, 29 | type: array 30 | } 31 | */ 32 | Transactions.getTransactionsByIDRange = function(options) { 33 | let url = this.accountURL + `/transactions/idrange` + this.formatQuery(options) 34 | return axios.get(url, this.header) 35 | } 36 | 37 | // GET REQUEST for range of transactions since a trade ID 38 | // tradeID = integer 39 | Transactions.getTransactionsSinceID= function(transactionID) { 40 | let url = this.accountURL + `/transactions/sinceid?id=${transactionID}` 41 | return axios.get(url, this.header) 42 | } 43 | 44 | module.exports = Transactions -------------------------------------------------------------------------------- /lib/endpoints/positions.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const Positions = {} 3 | 4 | // GET REQUEST for posistions 5 | Positions.getPositions = function () { 6 | let url = this.accountURL + '/positions' 7 | return axios.get(url, this.header) 8 | } 9 | 10 | // GET REQUEST for open positions 11 | Positions.getOpenPositions = function () { 12 | let url = this.accountURL + '/openPositions' 13 | return axios.get(url, this.header) 14 | } 15 | 16 | // GET REQUEST for a specific instrument positions 17 | // instrument = 'string' (e.g. 'USD_CAD') 18 | Positions.getInstrumentPosition = function (instrument) { 19 | let url = this.accountURL + `/positions/${instrument}` 20 | return axios.get(url, this.header) 21 | } 22 | 23 | /* PUT REQUEST to close position 24 | options = { 25 | instrument: string, 26 | longUnits: integer, 27 | longClientExtensions: object, 28 | shortUnits: integer, 29 | shortClientExtensions: object 30 | } 31 | */ 32 | Positions.closeInstrumentPosition = function (options) { 33 | let url = this.accountURL + `/positions/${options.instrument}/close` 34 | if (!!options.longUnits) { 35 | options.longUnits = options.longUnits.toString() 36 | } else { 37 | options.shortUnits = options.shortUnits.toString() 38 | } 39 | return axios.put(url, 40 | { 41 | longUnits: options.longUnits, 42 | longClientExtensions: options.longClientExtensions, 43 | shortUnits: options.shortUnits, 44 | shortClientExtensions: options.shortClientExtensions 45 | }, 46 | this.header) 47 | } 48 | 49 | module.exports = Positions -------------------------------------------------------------------------------- /lib/endpoints/instruments.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const Instruments = {} 3 | 4 | /* GET REQUEST for instrument candles 5 | instrument = 'string' (e.g. 'USD_CAD') 6 | options = { 7 | price: stringFloat, 8 | granularity: string, 9 | count: int, 10 | smooth: boolean, 11 | includeFirst: boolean, 12 | from: RFC3339 || UNIX, 13 | to: RFC3339 || UNIX, 14 | dailyAlignment: int, 15 | alignmentTimezone: string, 16 | weeklyAlignment: string 17 | } 18 | */ 19 | Instruments.getCandles = function (instrument, options) { 20 | let url = this.baseURL + `instruments/${instrument}/candles` + this.formatQuery(options) 21 | return axios.get(url, this.header) 22 | } 23 | 24 | /* GET REQUEST for instrument's orderbook 25 | options = { 26 | instrument: 'string' (e.g. 'USD_CAD') 27 | time: 'dateTime' DEFAULT: most recent time (e.g. 2016-06-22T18: 41:36.201836422Z) or as UNIX depending on config 28 | } 29 | */ 30 | Instruments.getOrderBook = function (options) { 31 | var url = this.baseURL + `instruments/${options.instrument}/orderBook` 32 | url += this.formatTime(options.time) 33 | return axios.get(url, this.header) 34 | } 35 | 36 | /*GET REQUEST for instrument's positionbook 37 | options = { 38 | instrument: 'string' (e.g. 'USD_CAD') 39 | time: 'dateTime' DEFAULT: most recent time (e.g. 2016-06-22T18:41:36.201836422Z) or as UNIX depending on config 40 | } 41 | */ 42 | Instruments.getPositionBook = function (options) { 43 | let url = this.baseURL + `instruments/${options.instrument}/positionBook` 44 | url += this.formatQuery(options.time) 45 | return axios.get(url, this.header) 46 | } 47 | 48 | module.exports = Instruments 49 | -------------------------------------------------------------------------------- /lib/endpoints/trades.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const Trades = {} 3 | 4 | /* GET REQUEST get trades 5 | options: { 6 | ids: array 7 | state: string 8 | instrument: string 9 | count: integer 10 | beforeID: integer 11 | } 12 | */ 13 | Trades.getTrades = function(options) { 14 | let url = this.accountURL + '/trades' + this.formatQuery(options) 15 | return axios.get(url, this.header) 16 | } 17 | 18 | // GET REQUEST get all open trades 19 | Trades.getOpenTrades = function() { 20 | let url = this.accountURL + '/openTrades' 21 | return axios.get(url, this.header) 22 | } 23 | 24 | // GET REQUEST get a trade by trade ID 25 | // tradeID = integer 26 | Trades.getTradeByID = function(tradeID) { 27 | let url = this.accountURL + `/trades/${tradeID}` 28 | return axios.get(url, this.header) 29 | } 30 | 31 | // PUT REQUEST close a trade by ID and number of units 32 | // tradeID = integer 33 | // units = integer || 'ALL' [set to 'ALL' by default] 34 | Trades.closeTrade = function(tradeID, units = 'ALL') { 35 | let url = this.accountURL + `/trades/${tradeID}/close` 36 | return axios.put(url, 37 | { 38 | units: units.toString() 39 | }, 40 | this.header) 41 | } 42 | 43 | 44 | /* PUT REQUEST modify a trade's takeProfit, stopLoss or trailingStopLoss 45 | tradeID = integer 46 | changeObject = { 47 | takeProfit/stopLoss/trailingStopLoss: 48 | { 49 | timeInForce: 'GTC'(DEFAULT) OPTIONS: GTC GTD GFD FOK IOC 50 | price: 'stringFloat', 51 | gtd: dateTime 52 | } 53 | } 54 | */ 55 | Trades.modifyTrade = function(tradeID, changeObject) { 56 | let url = this.accountURL + `/trades/${tradeID}/orders` 57 | return axios.put(url, changeObject, this.header) 58 | } 59 | 60 | module.exports = Trades -------------------------------------------------------------------------------- /Oanda.js: -------------------------------------------------------------------------------- 1 | const qs = require('querystring') 2 | 3 | const Oanda = { 4 | // Initalizes the API-WRAPPER Object with config inputs 5 | init: function(config) { 6 | const Accounts = require('./lib/endpoints/accounts') 7 | const Instruments = require('./lib/endpoints/instruments') 8 | const Positions = require('./lib/endpoints/positions') 9 | const Pricing = require('./lib/endpoints/pricing') 10 | const Orders = require('./lib/endpoints/orders') 11 | const Transactions = require('./lib/endpoints/transactions') 12 | const Trades = require('./lib/endpoints/trades') 13 | Object.assign(this, Accounts, Instruments, Positions, Transactions, Trades, Orders, Pricing) 14 | this.baseURL = (function() { 15 | if (config.env === 'fxPractice') { 16 | return 'https://api-fxpractice.oanda.com/v3/' 17 | } else { 18 | return 'https://api-fxtrade.oanda.com/v3/' 19 | } 20 | })() 21 | this.accountURL = this.baseURL + 'accounts/' + config.accountID 22 | this.accountID = config.accountID 23 | this.header = { 24 | headers: { 25 | 'Authorization': config.auth, 26 | 'Accept-Datetime-Format': config.dateFormat 27 | } 28 | } 29 | }, 30 | formatQuery: function(query) { 31 | if (Object.values(this.header.headers).includes('RFC3339')) { 32 | if (query.time) query.time = query.time.toJSON() 33 | if (query.from) query.from = query.from.toJSON() 34 | if (query.to) query.to = query.to.toJSON() 35 | } 36 | for (let prop in query) { 37 | if (!!Array.isArray(query[prop])) query[prop] = query[prop].join() 38 | } 39 | return '?' + qs.stringify(query) 40 | }, 41 | formatTime: function(time) { 42 | if (time !== undefined) { 43 | if (Object.values(this.header.headers).includes('RFC3339')) { 44 | return '?time=' + time.toJSON().replace(/[:]/g, "%3A") 45 | } else { 46 | return '?time=' + time 47 | } 48 | } 49 | return '' 50 | } 51 | } 52 | 53 | module.exports = Oanda 54 | -------------------------------------------------------------------------------- /lib/endpoints/accounts.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const Accounts = {} 3 | 4 | // GET REQUEST for accounts (including all IDs if config requires change) 5 | Accounts.getAccounts = function() { 6 | let url = this.baseURL + 'accounts' 7 | return axios.get(url, this.header) 8 | } 9 | 10 | // GET REQUEST for specific account details based on Account ID 11 | Accounts.getAccountDetails = function () { 12 | return axios.get(this.accountURL, this.header) 13 | } 14 | 15 | // GET REQUEST for account summary information (similar to account details) 16 | Accounts.getAccountSummary = function () { 17 | let url = this.accountURL + '/summary' 18 | return axios.get(url, this.header) 19 | } 20 | 21 | /* GET REQUEST for specific account related instrument/instrument information 22 | options = { 23 | instruments: array || string (for single instrument) 24 | } 25 | */ 26 | Accounts.getAccountInstrument = function (options) { 27 | let url = this.accountURL + '/instruments' 28 | if (!!options) url += this.formatQuery(options) 29 | return axios.get(url, this.header) 30 | } 31 | 32 | // PATCH REQUEST for changing account alias 33 | // alias = 'string' and is for new alias for account use 34 | Accounts.updateAccountAlias = function (alias) { 35 | let url = this.accountURL + '/configuration' 36 | return axios.patch(url, 37 | { 38 | 'alias': alias, 39 | }, 40 | this.header) 41 | } 42 | 43 | // PATCH REQUEST for changing account margin rate 44 | // marginRate = 'stringFloat' is new margin rate for account use (e.g. '0.05') 45 | Accounts.updateAccountMarginRate = function (marginRate) { 46 | let url = this.accountURL + '/configuration' 47 | return axios.patch(url, 48 | { 49 | 'marginRate': marginRate, 50 | }, 51 | this.header) 52 | } 53 | 54 | // GET REQUEST for listing account changes since a specific transaction 55 | // sinceTransactionID = 'stringInteger' is an account based transaction ID that augments based on account actions 56 | Accounts.getAccountChanges = function (sinceTransactionID) { 57 | let url = this.accountURL + `/changes?sinceTransactionID=${sinceTransactionID}` 58 | return axios.get(url, this.header) 59 | } 60 | 61 | module.exports = Accounts -------------------------------------------------------------------------------- /tests/formatter.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const should = chai.should() 3 | const Oanda = require('./../Oanda') 4 | const api = Object.create(Oanda) 5 | 6 | const config = { 7 | env: 'fxPractice', 8 | auth: 'Bearer e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 9 | accountID: '101-002-7777777-001', 10 | dateFormat: 'RFC3339' 11 | } 12 | 13 | const configUnix = { 14 | env: 'fxPractice', 15 | auth: 'Bearer e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 16 | accountID: '101-002-7777777-001', 17 | dateFormat: 'UNIX' 18 | } 19 | 20 | api.init(config) 21 | 22 | describe('api.init(config)', function() { 23 | it('should create api wrapper object config variables inserted', function() { 24 | let env = 'https://api-fxpractice.oanda.com/v3/' 25 | let auth = 'Bearer e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' 26 | let accountID = '101-002-7777777-001' 27 | env.should.eql(api.baseURL) 28 | auth.should.eql(api.header.headers.Authorization) 29 | accountID.should.eql(api.accountID) 30 | }) 31 | }) 32 | 33 | describe('api.formatQuery(query)', function() { 34 | it('should stringify an object to be suitable for API url query in RFC3339 dateTime format', function() { 35 | let queryUrl = api.formatQuery({ 36 | instrument: 'USD_CAD', 37 | count: 100, 38 | time: new Date(2017, 10, 14) 39 | }) 40 | queryUrl.should.eql('?instrument=USD_CAD&count=100&time=2017-11-14T04%3A00%3A00.000Z') 41 | }) 42 | }) 43 | 44 | describe('api.formatQuery(query)', function() { 45 | it('should stringify options for candle data suitable for API call', function() { 46 | let queryUrl = api.formatQuery({ 47 | granularity: 'M5', 48 | smooth: true, 49 | from: new Date(2017, 10, 11, 0), 50 | to: new Date(2017, 10, 12, 0) 51 | }) 52 | let testUrl = 'https://api-fxpractice.oanda.com/v3/instruments/USD_CAD/candles' + queryUrl 53 | let url = 'https://api-fxpractice.oanda.com/v3/instruments/USD_CAD/candles?granularity=M5&smooth=true&from=2017-11-11T04%3A00%3A00.000Z&to=2017-11-12T04%3A00%3A00.000Z' 54 | testUrl.should.eql(url) 55 | }) 56 | }) 57 | 58 | describe('api.formatTime(time)', function() { 59 | it('should stringify time to be suitable to API call', function() { 60 | let time = api.formatTime(new Date(2017, 10, 11, 0)) 61 | time.should.eql('?time=2017-11-11T04%3A00%3A00.000Z') 62 | }) 63 | }) 64 | 65 | apiUnix = Object.create(Oanda) 66 | apiUnix.init(configUnix) 67 | 68 | describe('apiUnix.formatQuery(query)', function() { 69 | it('should stringify an object to be suitable for API url query in UNIX dateTime format', function() { 70 | let queryUrl = apiUnix.formatQuery({ 71 | instrument: 'USD_CAD', 72 | count: 100, 73 | time: 1508174396789 74 | }) 75 | queryUrl.should.eql('?instrument=USD_CAD&count=100&time=1508174396789') 76 | }) 77 | }) 78 | 79 | -------------------------------------------------------------------------------- /examples.js: -------------------------------------------------------------------------------- 1 | // Get account details and log to console 2 | api.getAccountDetails().then((resp) => { 3 | console.log(resp.data) 4 | }).catch((err) => { 5 | console.log(err) 6 | }) 7 | 8 | // Get transactions and log to console 9 | api.getTransactions({from: new Date(2017, 9, 1)}).then((resp) => { 10 | console.log(resp.data) 11 | }).catch((err) => { 12 | console.log(err) 13 | }) 14 | 15 | // Get candle data of M5 granulairy for 500 periods and log to console 16 | api.getCandles('USD_CAD', {count: 500}).then((resp) => { 17 | console.log(resp.data) 18 | }).catch((err) => { 19 | console.log(err) 20 | }) 21 | 22 | // Place an order 23 | api.placeOrder({ 24 | type: 'MARKET', 25 | instrument: 'USD_CAD', 26 | units: 10000, 27 | takeProfitOnFill: { 28 | price: '1.31013', 29 | timeInForce: 'GTC' 30 | }, 31 | stopLossOnFill: { 32 | price: '1.28013', 33 | timeInForce: 'GTC' 34 | } 35 | }).then((resp) => { 36 | console.log(resp.data) 37 | }).catch ((err) => { 38 | console.log(err) 39 | }) 40 | 41 | // Modify a trade 42 | api.modifyTrade(1355, { 43 | stopLoss: { 44 | price: '1.27013', 45 | timeInForce: 'GTC' 46 | } 47 | }).then((resp) => { 48 | console.log(resp.data) 49 | }).catch((err) => { 50 | console.log(err) 51 | }) 52 | 53 | // Close trade 54 | api.closeTrade(1355).then((resp) => { 55 | console.log(resp.data) 56 | }).catch((err) => { 57 | console.log(err) 58 | }) 59 | 60 | // Calculate a simple moving average over a given period and return it as an array 61 | function simpleMovingAverage(period, length) { 62 | let ma = [] 63 | for (let i = 0; i < length - period; i++) { 64 | ma.push(data.candles.close.slice(i, period + i).reduce((x,y) => Number(x) + Number(y))/period) 65 | } 66 | data.ma = ma 67 | } 68 | 69 | /* Async Function to store candle data and caclulate Moving Average while updating account alias 70 | and getting open positions */ 71 | let data = {} 72 | 73 | function multiAxiosData() { 74 | data.candles = { 75 | full: [], 76 | open: [], 77 | high: [], 78 | low: [], 79 | close: [] 80 | } 81 | data.details = [] 82 | data.positions = [] 83 | Promise.all([ 84 | api.getCandles('USD_CAD', {count: 200}), 85 | api.updateAccountAlias('escdev'), 86 | api.getOpenPositions() 87 | ]) 88 | .then((resp) => { 89 | data.candles.full = resp[0].data.candles.map(i => i) 90 | resp[0].data.candles.map((i) => i.mid).forEach(i => { 91 | data.candles.open.push(i.o) 92 | data.candles.high.push(i.h) 93 | data.candles.low.push(i.l) 94 | data.candles.close.push(i.c) 95 | }) 96 | simpleMovingAverage(5, data.candles.open.length) 97 | console.log(resp[1].data) 98 | data.positions = resp[2].data 99 | api.getAccountDetails().then(r => console.log(r.data)).catch(e => console.log(e)) 100 | }) 101 | } 102 | 103 | 104 | /* Get candle data for specific instrument over a given period with default period length 105 | Create function scope variable from response 106 | Test and log to console the larger of index 1 and 2 candle's opening price */ 107 | api.getCandles('USD_CAD', {count: 20}).then((response) => { 108 | data = response.data.candles.map(i => i.mid) 109 | if (data[1].o > data[2].o) { 110 | data = data[1].o 111 | } else { 112 | data = data[2].o 113 | } 114 | }) -------------------------------------------------------------------------------- /lib/endpoints/orders.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const Orders = {} 3 | 4 | /* 5 | POST REQUEST to place an order 6 | orderObject = 7 | { 8 | type: 'string' DEFAULT: 'MARKET' LIMIT STOP MARKET_IF_TOUCHED TAKE_PROFIT STOP_LOSS TRAILING_STOP_LOSS 9 | instrument: 'string' (e.g. 'USD_CAD') 10 | units: integer (e.g. 100 || -100) 11 | timeInForce: 'string' DEFAULT:'FOK' GTC GTD DFD IOC 12 | priceBound: 'stringFloat' 13 | positionFill: 'string' DEFAULT: 'DEFAULT' OPEN_ONLY REDUCE_FIRST REDUCE_ONLY 14 | triggerCondition: 'string' DEFAULT 'DEFAULT' INVERSE BID ASK MID 15 | takeProfitonFill || stopLossOnFill: { 16 | price: 'stringFloat' 17 | timeInForce: 'string' DEFAULT: 'GTC' GTD GFD 18 | gdtTime: 'DateTime' used if on GTD for timeInForce 19 | } 20 | trailingStopLossOnFill: { 21 | distance: 'stringFloat' like price but amount of PIPs vs static price point 22 | timeInForce: 'string' DEFAULT: 'GTC' GTD GFD 23 | gdtTime: 'DateTime' used if on GTD for timeInForce 24 | } 25 | distance: 'stringFloat' amount of PIPs for trailingStopLossOrders 26 | } 27 | */ 28 | Orders.placeOrder = function(orderObject) { 29 | let url = this.accountURL + '/orders' 30 | return axios.post(url, 31 | { 32 | order: orderObject 33 | }, 34 | this.header) 35 | } 36 | 37 | /* GET REQUEST for orders 38 | options = { 39 | ids: array 40 | state: string 41 | instrument: string 42 | count: integer 43 | beforeID: integer 44 | } 45 | */ 46 | Orders.getOrders = function(options) { 47 | let url = this.accountURL + '/orders' 48 | if (!!options) url += this.formatQuery(options) 49 | return axios.get(url, this.header) 50 | } 51 | 52 | // GET REQUEST for pending orders 53 | Orders.getPendingOrders = function() { 54 | let url = this.accountURL + '/pendingOrders' 55 | return axios.get(url, this.header) 56 | } 57 | 58 | // GET REQUEST by order ID 59 | // orderID = 'stringInteger' based on transaction ID 60 | Orders.getOrderByID = function(orderID) { 61 | let url = this.accountURL + `/orders/${orderID}` 62 | return axios.get(url, this.header) 63 | } 64 | 65 | /* 66 | PUT REQUEST to replace an order 67 | orderObject = 68 | { 69 | type: 'string' DEFAULT: 'MARKET' LIMIT STOP MARKET_IF_TOUCHED TAKE_PROFIT STOP_LOSS TRAILING_STOP_LOSS 70 | instrument: 'string' (e.g. 'USD_CAD') 71 | units: 'stringInteger' (e.g. '100' || '-100') 72 | timeInForce: 'string' DEFAULT:'FOK' GTC GTD DFD IOC 73 | priceBound: 'stringFloat' 74 | positionFill: 'string' DEFAULT: 'DEFAULT' OPEN_ONLY REDUCE_FIRST REDUCE_ONLY 75 | triggerCondition: 'string' DEFAULT 'DEFAULT' INVERSE BID ASK MID 76 | takeProfitonFill || stopLossOnFill: { 77 | price: 'stringFloat' 78 | timeInForce: 'string' DEFAULT: 'GTC' GTD GFD 79 | gdtTime: 'DateTime' used if on GTD for timeInForce 80 | } 81 | trailingStopLossOnFill: { 82 | distance: 'stringFloat' like price but amount of PIPs vs static price point 83 | timeInForce: 'string' DEFAULT: 'GTC' GTD GFD 84 | gdtTime: 'DateTime' used if on GTD for timeInForce 85 | } 86 | distance: 'stringFloat' amount of PIPs for trailingStopLossOrders 87 | } 88 | */ 89 | Orders.replaceOrder = function(orderID, orderObject) { 90 | let url = this.accountURL + `/orders/${orderID}` 91 | return axios.put(url, 92 | { 93 | order: orderObject 94 | }, 95 | this.header) 96 | } 97 | 98 | // PUT REQUEST to cancel order 99 | // orderID = 'stringInteger' 100 | Orders.cancelOrder = function(orderID) { 101 | let url = this.accountURL + `/orders/${orderID}/cancel` 102 | return axios.put(url, {}, this.header) 103 | } 104 | 105 | module.exports = Orders -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oanda-node-api 2 | 3 | This is a simple promise based api-wrapper for Oanda's REST v20 API. The sole required dependency outside of Node.js 8.x modules is axios which performs the HTTP requests. Mocha/chai has also been included for basic testing. 4 | 5 | This is a hobby project and no guarantees can be made towards its reliability. If you would like to support the project, please feel free to contact me or star it. 6 | 7 | ## Getting Started 8 | 9 | Install with NodeJS and Node Package Manager (npm) in your root project folder. 10 | 11 | `npm install oanda-node-api` 12 | 13 | Once the npm install is complete, require the package in your chosen JavaScript file. You will also need to declare a configuration object and initialize the API wrapper. 14 | 15 | The configuration object requires the environment, your API authentication key provided by Oanda and your account ID. This project is only compatible with Oanda's newer v20 API. 16 | 17 | The environments available are fxPractice and fxTrade. 18 | 19 | It supports both RFC3399 and UNIX formats for datetime and auto formats for calls accordingly. 20 | 21 | Example code is as follows: 22 | 23 | ```javascript 24 | const Oanda = require('oanda-node-api') 25 | 26 | const config = { 27 | env: 'fxPractice', 28 | auth: 'Bearer xxxxxxxxxxxxxxxxxxxxxxXXXXXXXXXXXX', 29 | accountID: 'xxx-xxx-xxxxxxx-xxx', 30 | dateFormat: 'RFC3339' 31 | } 32 | 33 | const api = Object.create(Oanda) 34 | api.init(config) 35 | ``` 36 | 37 | If you have multiple account IDs, which Oanda supports, you will need to initialize more than one API core object. 38 | 39 | ## Using the API Wrapper 40 | 41 | Once the API wrapper object is created, you can call any of the associated functions. The function calls will return a Promise which can be utilized with .then and .catch for errors. 42 | 43 | Simple example: 44 | 45 | ```javascript 46 | api.getAccountDetails().then((resp) => { 47 | console.log(resp.data) 48 | }).catch((err) => { 49 | console.log(err) 50 | }) 51 | 52 | // will provide a console log of: 53 | { account: 54 | { id: 'xxx-xxx-xxxxxx-xxx', 55 | createdTime: '2017-10-01T20:46:35.053478376Z', 56 | currency: 'CAD', 57 | createdByUserID: xxxxxxx, 58 | alias: 'xxxxx', 59 | ... 60 | ... 61 | } 62 | } 63 | ``` 64 | 65 | To initalize a trade: 66 | 67 | ```javascript 68 | api.placeOrder(options).then((resp) => { 69 | console.log(resp.data) 70 | }).catch((err) => { 71 | console.log(err) 72 | }) 73 | ``` 74 | 75 | Where options is an object containing the chosen trade options. A list of all options is included below as well as documented in the appropriate sections of code. They can also be seen on the Oanda REST v20 API documentation site http://developer.oanda.com/rest-live-v20/order-df/#OrderRequest. 76 | 77 | More examples can be found in the example.js file. 78 | 79 | ## Rest Endpoints and Associated Functions 80 | 81 | Where options are an object, or otherwise stated, and only the chosen options should be included. If you are unfamiliar with which option parameters are required, please review the Oanda API documentation or see below. 82 | 83 | #### Accounts 84 | 85 | `api.getAccounts()` 86 | `api.getAccountDetails()` 87 | `api.getAccountSummary()` 88 | `api.getAccountInstrument(options)` 89 | > options = array || string (if single instrument) If no instrument is provided, ALL pairs are returned 90 | > 91 | 92 | `api.updateAccountAlias(alias)` 93 | > alias = 'string' 94 | 95 | `api.updateAccountMarginRate(marginRate)` 96 | > marginRate = 'stringFloat' < 0.99 97 | > 98 | 99 | `api.getAccountChanges(sinceTransactionID)` 100 | > sinceTransactionID = integer 101 | > 102 | 103 | #### Instruments 104 | 105 | `api.getCandles(instrument, options)` 106 | ```javascript 107 | instrument = string 108 | options = { 109 | price: stringFloat, 110 | granularity: string, 111 | count: integer, 112 | smooth: boolean, 113 | to: dateTime, 114 | from: dateTime, 115 | includeFirst: boolean, 116 | dailyAlignment: integer, 117 | alignmentTimezone: string, 118 | weeklyAlignment: string 119 | } 120 | ``` 121 | `api.getOrderBook(options)` 122 | ```javascript 123 | options = { 124 | instrument: string, 125 | time: dateTime 126 | } 127 | ``` 128 | `api.getPostionBook(options)` 129 | ```javascript 130 | options = { 131 | instrument: string, 132 | time: dateTime 133 | } 134 | ``` 135 | 136 | #### Orders 137 | 138 | `api.placeOrder(orderObject)` 139 | ```javascript 140 | orderObject = { 141 | type: 'string' DEFAULT: 'MARKET' LIMIT STOP MARKET_IF_TOUCHED TAKE_PROFIT STOP_LOSS TRAILING_STOP_LOSS 142 | instrument: 'string' (e.g. 'USD_CAD') 143 | units: integer (e.g. 100 || -100) 144 | timeInForce: 'string' DEFAULT:'FOK' GTC GTD DFD IOC 145 | priceBound: 'stringFloat' 146 | positionFill: 'string' DEFAULT: 'DEFAULT' OPEN_ONLY REDUCE_FIRST REDUCE_ONLY 147 | triggerCondition: 'string' DEFAULT 'DEFAULT' INVERSE BID ASK MID 148 | takeProfitOnFill || stopLossOnFill: { 149 | price: 'stringFloat' 150 | timeInForce: 'string' DEFAULT: 'GTC' GTD GFD 151 | gtdTime: 'DateTime' used if on GTD for timeInForce 152 | } 153 | trailingStopLossOnFill: { 154 | distance: 'stringFloat' like price but amount of PIPs vs static price point 155 | timeInForce: 'string' DEFAULT: 'GTC' GTD GFD 156 | gtdTime: 'DateTime' used if on GTD for timeInForce 157 | } 158 | distance: 'stringFloat' amount of PIPs for trailingStopLossOrders 159 | } 160 | ``` 161 | `api.getOrders(options)` 162 | ```javascript 163 | options = { 164 | ids: array 165 | state: string 166 | instrument: string 167 | count: integer 168 | beforeID: integer 169 | } 170 | ``` 171 | 172 | `api.getPendingOrders()` 173 | `api.getOrderByID(orderID)` 174 | > orderID = 'stringInteger' 175 | > 176 | 177 | `api.replaceOrder(orderID, orderObject)` 178 | 179 | ```javascript 180 | orderObject = { 181 | type: 'string' DEFAULT: 'MARKET' LIMIT STOP MARKET_IF_TOUCHED TAKE_PROFIT STOP_LOSS TRAILING_STOP_LOSS 182 | instrument: 'string' (e.g. 'USD_CAD') 183 | units: 'stringInteger' (e.g. '100' || '-100') 184 | timeInForce: 'string' DEFAULT:'FOK' GTC GTD DFD IOC 185 | priceBound: 'stringFloat' 186 | positionFill: 'string' DEFAULT: 'DEFAULT' OPEN_ONLY REDUCE_FIRST REDUCE_ONLY 187 | triggerCondition: 'string' DEFAULT 'DEFAULT' INVERSE BID ASK MID 188 | takeProfitOnFill || stopLossOnFill: { 189 | price: 'stringFloat' 190 | timeInForce: 'string' DEFAULT: 'GTC' GTD GFD 191 | gtdTime: 'DateTime' used if on GTD for timeInForce 192 | } 193 | trailingStopLossOnFill: { 194 | distance: 'stringFloat' like price but amount of PIPs vs static price point 195 | timeInForce: 'string' DEFAULT: 'GTC' GTD GFD 196 | gtdTime: 'DateTime' used if on GTD for timeInForce 197 | } 198 | distance: 'stringFloat' amount of PIPs for trailingStopLossOrders 199 | } 200 | ``` 201 | `api.cancelOrder(orderID)` 202 | > orderID = integer 203 | > 204 | 205 | #### Positions 206 | 207 | `api.getPositions()` 208 | `api.getOpenPositions()` 209 | `api.getInstrumentPositions(instrument)` 210 | > instrument = 'string' (e.g. 'USD_CAD') 211 | > 212 | 213 | `api.closeInstrumentPosition(options)` 214 | ```javascript 215 | options = { 216 | instrument: string, 217 | longUnits: integer, 218 | longClientExtensions: object, 219 | shortUnits: integer, 220 | shortClientExtensions: object 221 | } 222 | ``` 223 | 224 | #### Pricing 225 | 226 | `api.getPricing(options)` 227 | ```javascript 228 | options: { 229 | instruments: array (e.g ['USD_CAD', 'EUR_USD']) 230 | time: dateTime 231 | } 232 | ``` 233 | 234 | #### Trades 235 | 236 | `api.getTrades(options)` 237 | ```javascript 238 | options: { 239 | ids: array 240 | state: string 241 | instrument: string 242 | count: integer 243 | beforeID: integer 244 | } 245 | ``` 246 | 247 | `api.getOpenTrades()` 248 | `api.getTradeByID(tradeID)` 249 | > tradeID = integer 250 | > 251 | 252 | `api.closeTrade(tradeID, units = 'ALL')` 253 | > tradeID = integer 254 | > units = integer || 'ALL' (e.g. 100) DEFAULT = 'ALL' 255 | 256 | `api.modifyTrade(tradeID, changeObject)` 257 | ```javascript 258 | tradeID = integer 259 | changeObject = { 260 | takeProfit || stopLoss || trailingStopLoss: 261 | { 262 | price: 'stringFloat', 263 | timeInForce: 'GTC'(DEFAULT) OPTIONS: GTC GTD GFD FOK IOC 264 | gtd: dateTime 265 | } 266 | } 267 | ``` 268 | 269 | #### Transactions 270 | 271 | `api.getTransactions(options = [])` 272 | ```javascript 273 | options = { 274 | from: dateTime, 275 | to: dateTime, 276 | pageSize: integer, [100-1000] 277 | type: array 278 | } 279 | ``` 280 | 281 | `api.getTransactionsByID(transactionID)` 282 | > transactionID = integer 283 | > 284 | 285 | `api.getTransactionByIDRange(options)` 286 | ```javascript 287 | options = { 288 | to: integer, 289 | from: integer, 290 | type: array 291 | } 292 | ``` 293 | 294 | `api.getTransactionSinceID(transactionID)` 295 | > transactionID = integer 296 | > --------------------------------------------------------------------------------