Skip to content

Commit

Permalink
Add FailoverCalculator
Browse files Browse the repository at this point in the history
  • Loading branch information
Cayle Sharrock committed Oct 26, 2017
1 parent 7e2339a commit 747e4e0
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 7 deletions.
5 changes: 5 additions & 0 deletions src/FXService/FXRateCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@
import { CurrencyPair, FXObject } from './FXProvider';

export abstract class FXRateCalculator {
/**
* Makes a request for the calculator to calculate and return the most recent exchange rate for the given currency pairs.
* If the Calculator is unable to complete a request for any of the pairs, it should return `null` for that pair and the other rates
* will still be accepted. However, it can also reject the entire Promise if it is unable to calculate rates for any of the given pairs
*/
abstract calculateRatesFor(pairs: CurrencyPair[]): Promise<FXObject[]>;
}
66 changes: 66 additions & 0 deletions src/FXService/calculators/FailoverCalculator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**********************************************************************************************************************
* @license *
* Copyright 2017 Coinbase, Inc. *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on*
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the *
* License for the specific language governing permissions and limitations under the License. *
**********************************************************************************************************************/

import { FXRateCalculator } from '../FXRateCalculator';
import { CurrencyPair, FXObject } from '../FXProvider';
import { ConsoleLoggerFactory, Logger } from '../../utils/Logger';
import { tryUntil } from '../../utils/promises';

export interface FailoverCalculatorConfig {
calculators: FXRateCalculator[];
logger?: Logger;
}

/**
* A simple FX rate calculator that uses a single FXProvider and return the current exchange rate from it directly.
* If the pair is unavailable, or some other error occurs, the calculator returns null for that pair
*/
export default class FailoverCalculator extends FXRateCalculator {
logger: Logger;
calculators: FXRateCalculator[];

constructor(config: FailoverCalculatorConfig) {
super();
this.calculators = config.calculators;
this.logger = config.logger || ConsoleLoggerFactory();
}

calculateRatesFor(pairs: CurrencyPair[]): Promise<FXObject[]> {
const promises: Promise<FXObject>[] = pairs.map((pair: CurrencyPair) => {
return this.requestRateFor(pair);
});
// Wait for all promises to resolve before sending results back
return Promise.all(promises);
}

private requestRateFor(pair: CurrencyPair): Promise<FXObject> {
return tryUntil<FXRateCalculator, FXObject>(this.calculators, (calculator: FXRateCalculator) => {
return calculator.calculateRatesFor([pair])
.then((result: FXObject[]) => {
if (result[0] === null || result[0].rate === null) {
return false;
}
return result[0];
})
.catch(() => {
return false;
});
}).then((result: FXObject | false) => {
if (result === false) {
return null;
}
return result;
});
}
}
8 changes: 1 addition & 7 deletions src/FXService/calculators/SimpleRateCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,7 @@ export default class SimpleRateCalculator extends FXRateCalculator {
return this.provider.fetchCurrentRate(pair)
.catch((err: Error) => {
this.logger.log('warn', err.message, (err as any).details || null);
return Promise.resolve({
from: pair.from,
to: pair.to,
rate: null,
change: null,
time: new Date()
});
return null;
});
});
// Wait for all promises to resolve before sending results back
Expand Down
123 changes: 123 additions & 0 deletions test/FXService/FailoverCalculatorTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**********************************************************************************************************************
* @license *
* Copyright 2017 Coinbase, Inc. *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on*
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the *
* License for the specific language governing permissions and limitations under the License. *
**********************************************************************************************************************/

import { CryptoProvider } from '../../src/FXService/providers/CryptoProvider';
import { NullLogger } from '../../src/utils/Logger';
import { DefaultAPI as GDAX } from '../../src/factories/gdaxFactories';
import { DefaultAPI as Bitfinex } from '../../src/factories/bitfinexFactories';
import { FXObject } from '../../src/FXService/FXProvider';
import FailoverCalculator from '../../src/FXService/calculators/FailoverCalculator';
import SimpleRateCalculator from '../../src/FXService/calculators/SimpleRateCalculator';

const assert = require('assert');
const nock = require('nock');

describe('FailoverCalculator', () => {
let calculator: FailoverCalculator;

before(() => {
const provider1 = new CryptoProvider({ logger: NullLogger, exchange: GDAX(NullLogger) });
const provider2 = new CryptoProvider({ logger: NullLogger, exchange: Bitfinex(NullLogger) });
const calc1 = new SimpleRateCalculator(provider1, NullLogger);
const calc2 = new SimpleRateCalculator(provider2, NullLogger);
calculator = new FailoverCalculator({
logger: NullLogger,
calculators: [calc1, calc2]
});
});

it('returns spot rate from first calculator is supported', () => {
nock('https://api.gdax.com:443')
.get('/products')
.reply(200, [{
id: 'BTC-USD',
base_currency: 'BTC',
quote_currency: 'USD',
base_min_size: '0.01',
base_max_size: '1000000',
quote_increment: '0.01',
display_name: 'BTC/USD'
}])
.get('/products/BTC-USD/ticker')
.reply(200, {
trade_id: 10000,
price: '10500.00',
bid: '10400.00',
ask: '10600.00',
time: '2017-10-26T06:17:41.579000Z'
});
return calculator.calculateRatesFor([{ from: 'BTC', to: 'USD' }]).then((results: FXObject[]) => {
assert.equal(results[0].rate.toNumber(), 10500);
assert.equal(results[0].from, 'BTC');
assert.equal(results[0].to, 'USD');
});
});

it('returns spot rate from 2nd calculator is first is not supported', () => {
nock('https://api.bitfinex.com:443')
.get('/v1/symbols_details')
.reply(200, [{ pair: 'ethusd' }, { pair: 'btcusd' }])
.get('/v1/pubticker/ethusd')
.reply(200, {
mid: '500.00',
bid: '499.00',
ask: '501.00',
last_price: '495.49'
});
return calculator.calculateRatesFor([{ from: 'ETH', to: 'USD' }]).then((results: FXObject[]) => {
assert.equal(results[0].rate.toNumber(), 500);
assert.equal(results[0].from, 'ETH');
assert.equal(results[0].to, 'USD');
});
});

it('returns null for unsupported currencies', () => {
nock('https://api.gdax.com:443')
.get('/products/BTC-XYZ/ticker')
.reply(404, { message: 'NotFound' });
return calculator.calculateRatesFor([{ from: 'BTC', to: 'XYZ' }]).then((results: FXObject[]) => {
assert.equal(results[0], null);
});
});

it('returns spot rate from both calculators', () => {
nock('https://api.gdax.com:443')
.get('/products/BTC-USD/ticker')
.reply(200, {
trade_id: 10000,
price: '10500.00',
bid: '10400.00',
ask: '10500.00',
time: '2017-10-26T06:17:41.579000Z'
});
nock('https://api.bitfinex.com:443')
.get('/v1/pubticker/ethusd')
.reply(200, {
bid: '400.00',
ask: '410.00',
last_price: '410.0'
});
return calculator.calculateRatesFor([{ from: 'BTC', to: 'USD' }, { from: 'ETH', to: 'USD' }]).then((results: FXObject[]) => {
assert.equal(results.length, 2);

assert.equal(results[0].rate.toNumber(), 10450);
assert.equal(results[0].from, 'BTC');
assert.equal(results[0].to, 'USD');

assert.equal(results[1].rate.toNumber(), 405);
assert.equal(results[1].from, 'ETH');
assert.equal(results[1].to, 'USD');
});
});
});

0 comments on commit 747e4e0

Please sign in to comment.