-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
209 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) | ||
}) |