Skip to content

Commit

Permalink
feat: add xignite fx adapter (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
nvtaveras authored Oct 3, 2023
1 parent 0c0b8b0 commit ed72b6e
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/data_aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { OKCoinAdapter } from './exchange_adapters/okcoin'
import { OKXAdapter } from './exchange_adapters/okx'
import { OracleApplicationConfig } from './app'
import { WhitebitAdapter } from './exchange_adapters/whitebit'
import { XigniteAdapter } from './exchange_adapters/xignite'
import { strict as assert } from 'assert'

function adapterFromExchangeName(name: Exchange, config: ExchangeAdapterConfig): ExchangeAdapter {
Expand Down Expand Up @@ -80,6 +81,8 @@ function adapterFromExchangeName(name: Exchange, config: ExchangeAdapterConfig):
return new AlphavantageAdapter(config)
case Exchange.CURRENCYAPI:
return new CurrencyApiAdapter(config)
case Exchange.XIGNITE:
return new XigniteAdapter(config)
}
}

Expand Down
93 changes: 93 additions & 0 deletions src/exchange_adapters/xignite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { BaseExchangeAdapter, ExchangeAdapter, ExchangeDataType, Ticker } from './base'

import BigNumber from 'bignumber.js'
import { Exchange } from '../utils'
import { strict as assert } from 'assert'

export class XigniteAdapter extends BaseExchangeAdapter implements ExchangeAdapter {
baseApiUrl = 'https://globalcurrencies.xignite.com/xGlobalCurrencies.json'
readonly _exchangeName: Exchange = Exchange.XIGNITE
// *.xignite.com - validity not after: 30/01/2024, 19:59:59 GMT-4
readonly _certFingerprint256 =
'AC:3B:21:EB:EE:92:8B:81:85:EF:85:DF:76:DE:9A:A0:2C:06:3D:D0:48:89:F2:29:76:9F:AB:E1:69:3A:D4:F4'

protected generatePairSymbol(): string {
const base = XigniteAdapter.standardTokenSymbolMap.get(this.config.baseCurrency)
const quote = XigniteAdapter.standardTokenSymbolMap.get(this.config.quoteCurrency)

return `${base}${quote}`
}

async fetchTicker(): Promise<Ticker> {
assert(this.config.apiKey !== undefined, 'XigniteAdapter API key was not set')

const tickerJson = await this.fetchFromApi(
ExchangeDataType.TICKER,
`GetRealTimeRate?Symbol=${this.pairSymbol}&_token=${this.config.apiKey}`
)
return this.parseTicker(tickerJson)
}

/**
*
* @param json parsed response from Xignite's rate endpoint
* {
* "BaseCurrency": "EUR",
* "QuoteCurrency": "XOF",
* "Symbol": "EURXOF",
* "Date": "09/29/2023",
* "Time": "9:59:50 PM",
* "QuoteType": "Calculated",
* "Bid": 653.626,
* "Mid": 654.993,
* "Ask": 656.36,
* "Spread": 2.734,
* "Text": "1 European Union euro = 654.993 West African CFA francs",
* "Source": "Rates calculated by crossing via ZAR(Morningstar).",
* "Outcome": "Success",
* "Message": null,
* "Identity": "Request",
* "Delay": 0.0032363
* }
*/
parseTicker(json: any): Ticker {
assert(
json.BaseCurrency === this.config.baseCurrency,
`Base currency mismatch in response: ${json.BaseCurrency} != ${this.config.baseCurrency}`
)
assert(
json.QuoteCurrency === this.config.quoteCurrency,
`Quote currency mismatch in response: ${json.QuoteCurrency} != ${this.config.quoteCurrency}`
)

const ticker = {
...this.priceObjectMetadata,
ask: this.safeBigNumberParse(json.Ask)!,
bid: this.safeBigNumberParse(json.Bid)!,
lastPrice: this.safeBigNumberParse(json.Mid)!,
timestamp: this.toUnixTimestamp(json.Date, json.Time),
// These FX API's do not provide volume data,
// therefore we set all of them to 1 to weight them equally
baseVolume: new BigNumber(1),
quoteVolume: new BigNumber(1),
}
this.verifyTicker(ticker)
return ticker
}

toUnixTimestamp(date: string, time: string): number {
const [month, day, year] = date.split('/').map(Number) // date format: MM/DD/YYYY
const [hours, minutes, seconds] = time.split(' ')[0].split(':').map(Number) // time format: HH:MM:SS AM/PM

let adjustedHours = hours
if (time.includes('PM') && hours !== 12) adjustedHours += 12
if (time.includes('AM') && hours === 12) adjustedHours = 0

// month should be 0-indexed
return Date.UTC(year, month - 1, day, adjustedHours, minutes, seconds) / 1000
}

async isOrderbookLive(): Promise<boolean> {
return !BaseExchangeAdapter.fxMarketsClosed(Date.now())
}
}
1 change: 1 addition & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export enum Exchange {
BITMART = 'BITMART',
ALPHAVANTAGE = 'ALPHAVANTAGE',
CURRENCYAPI = 'CURRENCYAPI',
XIGNITE = 'XIGNITE',
}

export enum ExternalCurrency {
Expand Down
112 changes: 112 additions & 0 deletions test/exchange_adapters/xignite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Exchange, ExternalCurrency } from '../../src/utils'

import BigNumber from 'bignumber.js'
import { ExchangeAdapterConfig } from '../../src/exchange_adapters/base'
import { XigniteAdapter } from '../../src/exchange_adapters/xignite'
import { baseLogger } from '../../src/default_config'

describe('Xignite adapter', () => {
let adapter: XigniteAdapter

const config: ExchangeAdapterConfig = {
baseCurrency: ExternalCurrency.EUR,
baseLogger,
quoteCurrency: ExternalCurrency.XOF,
}

beforeEach(() => {
adapter = new XigniteAdapter(config)
})

const validMockTickerJson = {
BaseCurrency: 'EUR',
QuoteCurrency: 'XOF',
Symbol: 'EURXOF',
Date: '09/29/2023',
Time: '9:59:50 PM',
QuoteType: 'Calculated',
Bid: 653.626,
Mid: 654.993,
Ask: 656.36,
Spread: 2.734,
Text: '1 European Union euro = 654.993 West African CFA francs',
Source: 'Rates calculated by crossing via ZAR(Morningstar).',
Outcome: 'Success',
Message: null,
Identity: 'Request',
Delay: 0.0032363,
}

const invalidJsonWithBaseCurrencyMissmatch = {
...validMockTickerJson,
BaseCurrency: 'USD',
}

const invalidJsonWithQuoteCurrencyMissmatch = {
...validMockTickerJson,
QuoteCurrency: 'USD',
}

const invalidJsonWithMissingFields = {
Spread: 0.000001459666,
Mid: 0.001524435453,
Delay: 0.0570077,
Time: '2:36:48 PM',
Date: '07/26/2023',
Symbol: 'EURXOF',
QuoteCurrency: 'XOF',
BaseCurrency: 'EUR',
}

describe('parseTicker', () => {
it('handles a response that matches the documentation', () => {
const ticker = adapter.parseTicker(validMockTickerJson)

expect(ticker).toEqual({
source: Exchange.XIGNITE,
symbol: adapter.standardPairSymbol,
ask: new BigNumber(656.36),
bid: new BigNumber(653.626),
lastPrice: new BigNumber(654.993),
timestamp: 1696024790,
baseVolume: new BigNumber(1),
quoteVolume: new BigNumber(1),
})
})

it('throws an error when the base currency does not match', () => {
expect(() => {
adapter.parseTicker(invalidJsonWithBaseCurrencyMissmatch)
}).toThrowError('Base currency mismatch in response: USD != EUR')
})

it('throws an error when the quote currency does not match', () => {
expect(() => {
adapter.parseTicker(invalidJsonWithQuoteCurrencyMissmatch)
}).toThrowError('Quote currency mismatch in response: USD != XOF')
})

it('throws an error when some required fields are missing', () => {
expect(() => {
adapter.parseTicker(invalidJsonWithMissingFields)
}).toThrowError('bid, ask not defined')
})
})

describe('toUnixTimestamp', () => {
it('handles date strings with AM time', () => {
expect(adapter.toUnixTimestamp('07/26/2023', '10:00:00 AM')).toEqual(1690365600)
expect(adapter.toUnixTimestamp('01/01/2023', '4:29:03 AM')).toEqual(1672547343)
})
it('handles date strins with PM time', () => {
expect(adapter.toUnixTimestamp('03/15/2023', '4:53:27 PM')).toEqual(1678899207)
expect(adapter.toUnixTimestamp('07/26/2023', '8:29:37 PM')).toEqual(1690403377)
})
it('handles 12 PM edge case', () => {
expect(adapter.toUnixTimestamp('07/20/2023', '12:53:15 PM')).toEqual(1689857595)
})
it('handles 12 AM edge case', () => {
expect(adapter.toUnixTimestamp('07/20/2023', '12:53:15 AM')).toEqual(1689814395)
})
})
})

0 comments on commit ed72b6e

Please sign in to comment.