Skip to content

Commit

Permalink
Fix and refactor price managers and providers
Browse files Browse the repository at this point in the history
  • Loading branch information
maxima-net committed Aug 29, 2022
1 parent a98035b commit 279f9d8
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 58 deletions.
2 changes: 1 addition & 1 deletion src/atomex/atomex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export class Atomex implements AtomexService {
}

async convertCurrency(fromAmount: BigNumber.Value, fromCurrency: Currency['id'], toCurrency: Currency['id']): Promise<BigNumber | undefined> {
const price = await this.priceManager.getAveragePrice({ baseCurrency: fromCurrency, quoteCurrency: toCurrency });
const price = await this.priceManager.getAveragePrice({ baseCurrencyOrIdOrSymbol: fromCurrency, quoteCurrencyOrIdOrSymbol: toCurrency });
if (!price)
return undefined;

Expand Down
12 changes: 7 additions & 5 deletions src/atomexBuilder/atomexBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,12 @@ export class AtomexBuilder {
}

protected createPriceManager(): PriceManager {
return new MixedPriceManager(new Map<string, PriceProvider>([
['binance', new BinancePriceProvider()],
['kraken', new KrakenPriceProvider()],
['atomex', new AtomexPriceProvider(this.atomexContext.services.exchangeService)]
]));
return new MixedPriceManager(
this.atomexContext.providers.currenciesProvider,
new Map<string, PriceProvider>([
['binance', new BinancePriceProvider()],
['kraken', new KrakenPriceProvider()],
['atomex', new AtomexPriceProvider(this.atomexContext.services.exchangeService)]
]));
}
}
26 changes: 13 additions & 13 deletions src/blockchain/atomexProtocolV1/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import type { AtomexBlockchainProvider } from '../atomexBlockchainProvider';
import type { FeesInfo } from '../models/index';

export const getRedeemRewardInNativeCurrency = async (
currencyId: Currency['id'],
currencyOrId: Currency | Currency['id'],
redeemFee: FeesInfo,
priceManager: PriceManager
): Promise<FeesInfo> => {
const nativeTokenPriceInUsd = await priceManager.getAveragePrice({ baseCurrency: currencyId, quoteCurrency: 'USD' });
const nativeTokenPriceInUsd = await priceManager.getAveragePrice({ baseCurrencyOrIdOrSymbol: currencyOrId, quoteCurrencyOrIdOrSymbol: 'USD' });
if (!nativeTokenPriceInUsd)
throw new Error(`Price for ${currencyId} in USD not found`);
throw new Error(`Price for ${currencyOrId} in USD not found`);

const maxRewardPercentValue = 30;
const maxRewardPercent = 0.15;
Expand All @@ -27,25 +27,25 @@ export const getRedeemRewardInNativeCurrency = async (
};

export const getRedeemRewardInToken = async (
currencyId: string,
currencyOrId: Currency | Currency['id'],
redeemFee: FeesInfo,
priceManager: PriceManager,
blockchainProvider: AtomexBlockchainProvider
): Promise<FeesInfo> => {
const currencyInfo = blockchainProvider.getCurrency(currencyId);
if (!currencyInfo)
throw new Error(`Currency info not found for ${currencyId}`);
const currency = typeof currencyOrId === 'string' ? blockchainProvider.getCurrency(currencyOrId) : currencyOrId;
if (!currency)
throw new Error(`Currency info not found for ${currencyOrId}`);

const nativeCurrencyId = blockchainProvider.getNativeCurrencyInfo(currencyInfo.blockchain)?.currency.id;
if (!nativeCurrencyId)
throw new Error(`Native currency not found fir ${currencyInfo.blockchain}`);
const nativeCurrency = blockchainProvider.getNativeCurrencyInfo(currency.blockchain)?.currency;
if (!nativeCurrency)
throw new Error(`Native currency not found fir ${currency.blockchain}`);

const nativeTokenPriceInCurrency = await priceManager.getAveragePrice({ baseCurrency: nativeCurrencyId, quoteCurrency: currencyId });
const nativeTokenPriceInCurrency = await priceManager.getAveragePrice({ baseCurrencyOrIdOrSymbol: nativeCurrency, quoteCurrencyOrIdOrSymbol: currencyOrId });

if (!nativeTokenPriceInCurrency)
throw new Error(`Price for ${nativeCurrencyId} in ${currencyId} not found`);
throw new Error(`Price for ${nativeCurrency.id} in ${currencyOrId} not found`);

const inNativeToken = await getRedeemRewardInNativeCurrency(nativeCurrencyId, redeemFee, priceManager);
const inNativeToken = await getRedeemRewardInNativeCurrency(nativeCurrency.id, redeemFee, priceManager);

return {
estimated: inNativeToken.estimated.multipliedBy(nativeTokenPriceInCurrency),
Expand Down
56 changes: 37 additions & 19 deletions src/exchange/priceManager/mixedPriceManager/mixedPriceManager.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import BigNumber from 'bignumber.js';

import { Currency, DataSource } from '../../../common';
import { CurrenciesProvider, Currency, DataSource } from '../../../common';
import { Cache, InMemoryCache } from '../../../core/index';
import type { PriceProvider } from '../../priceProvider/index';
import type { GetAveragePriceParameters, GetPriceParameters, PriceManager } from '../priceManager';

interface GetCacheKeyParameters {
isAverage: boolean;
baseCurrency: Currency['id'];
quoteCurrency: Currency['id'];
baseCurrencyOrSymbol: Currency | Currency['id'];
quoteCurrencyOrSymbol: Currency | Currency['id'];
provider?: string;
}

export class MixedPriceManager implements PriceManager {
private static readonly cacheExpirationTime = 1000 * 60 * 1;
private readonly cache: Cache = new InMemoryCache({ absoluteExpirationMs: 1000 * 30 });

private readonly cache: Cache;
constructor(
private readonly currenciesProvider: CurrenciesProvider,
private readonly providersMap: Map<string, PriceProvider>
) {
this.cache = new InMemoryCache({ absoluteExpirationMs: MixedPriceManager.cacheExpirationTime });
}
) { }

async getAveragePrice({ baseCurrencyOrIdOrSymbol, quoteCurrencyOrIdOrSymbol, dataSource = DataSource.All }: GetAveragePriceParameters): Promise<BigNumber | undefined> {
const baseCurrencyOrSymbol = this.tryFindCurrency(baseCurrencyOrIdOrSymbol);
const quoteCurrencyOrSymbol = this.tryFindCurrency(quoteCurrencyOrIdOrSymbol);

async getAveragePrice({ baseCurrency, quoteCurrency, dataSource = DataSource.All }: GetAveragePriceParameters): Promise<BigNumber | undefined> {
const key = this.getCacheKey({ isAverage: true, baseCurrency, quoteCurrency });
const key = this.getCacheKey({ isAverage: true, baseCurrencyOrSymbol, quoteCurrencyOrSymbol });
if ((dataSource & DataSource.Local) === DataSource.Local) {
const cachedAveragePrice = this.cache.get<BigNumber>(key);
if (cachedAveragePrice)
Expand All @@ -32,7 +33,12 @@ export class MixedPriceManager implements PriceManager {

if ((dataSource & DataSource.Remote) === DataSource.Remote) {
const providers = this.getAvailableProviders();
const pricePromises = providers.map(provider => this.getPrice({ baseCurrency, quoteCurrency, provider }));
const pricePromises = providers.map(provider => this.getPrice({
baseCurrencyOrIdOrSymbol: baseCurrencyOrSymbol,
quoteCurrencyOrIdOrSymbol: quoteCurrencyOrSymbol,
provider,
dataSource
}));
const pricePromiseResults = await Promise.allSettled(pricePromises);

const prices: BigNumber[] = [];
Expand All @@ -50,18 +56,21 @@ export class MixedPriceManager implements PriceManager {
return undefined;
}

async getPrice({ baseCurrency, quoteCurrency, provider, dataSource = DataSource.All }: GetPriceParameters): Promise<BigNumber | undefined> {
const key = this.getCacheKey({ isAverage: false, baseCurrency, quoteCurrency, provider });
async getPrice({ baseCurrencyOrIdOrSymbol, quoteCurrencyOrIdOrSymbol, provider, dataSource = DataSource.All }: GetPriceParameters): Promise<BigNumber | undefined> {
const baseCurrencyOrSymbol = this.tryFindCurrency(baseCurrencyOrIdOrSymbol);
const quoteCurrencyOrSymbol = this.tryFindCurrency(quoteCurrencyOrIdOrSymbol);

const key = this.getCacheKey({ isAverage: false, baseCurrencyOrSymbol, quoteCurrencyOrSymbol, provider });
if ((dataSource & DataSource.Local) === DataSource.Local) {
const cachedPrice = this.cache.get<BigNumber>(key);
if (cachedPrice)
return cachedPrice;
}

if ((dataSource & DataSource.Remote) === DataSource.Remote) {
let price = await this.getPriceCore(baseCurrency, quoteCurrency, provider);
let price = await this.getPriceCore(baseCurrencyOrSymbol, quoteCurrencyOrSymbol, provider);
if (!price) {
const reversedPrice = await this.getPriceCore(quoteCurrency, baseCurrency, provider);
const reversedPrice = await this.getPriceCore(quoteCurrencyOrSymbol, baseCurrencyOrSymbol, provider);
if (reversedPrice)
price = reversedPrice.pow(-1);
}
Expand All @@ -83,16 +92,25 @@ export class MixedPriceManager implements PriceManager {
this.cache.clear();
}

private getCacheKey({ isAverage, baseCurrency, quoteCurrency, provider }: GetCacheKeyParameters) {
private tryFindCurrency(baseCurrencyOrIdOrSymbol: Currency | Currency['id']): Currency | string {
if (typeof baseCurrencyOrIdOrSymbol !== 'string')
return baseCurrencyOrIdOrSymbol;

return this.currenciesProvider.getCurrency(baseCurrencyOrIdOrSymbol) || baseCurrencyOrIdOrSymbol;
}

private getCacheKey({ isAverage, baseCurrencyOrSymbol, quoteCurrencyOrSymbol, provider }: GetCacheKeyParameters) {
const prefix = isAverage ? 'average' : 'actual';
const baseCurrencySymbol = typeof baseCurrencyOrSymbol === 'string' ? baseCurrencyOrSymbol : baseCurrencyOrSymbol.id;
const quoteCurrencySymbol = typeof quoteCurrencyOrSymbol === 'string' ? quoteCurrencyOrSymbol : quoteCurrencyOrSymbol.id;
const postfix = provider ? provider : '';

return `${prefix}_${baseCurrency}_${quoteCurrency}_${postfix}`;
return `${prefix}_${baseCurrencySymbol}_${quoteCurrencySymbol}_${postfix}`;
}

private async getPriceCore(baseCurrency: Currency['id'], quoteCurrency: Currency['id'], provider?: string): Promise<BigNumber | undefined> {
private async getPriceCore(baseCurrencyOrSymbol: Currency | string, quoteCurrencyOrSymbol: Currency | string, provider?: string): Promise<BigNumber | undefined> {
const providers = this.getSelectedProviders(provider);
const pricePromises = providers.map(provider => provider.getPrice(baseCurrency, quoteCurrency));
const pricePromises = providers.map(provider => provider.getPrice(baseCurrencyOrSymbol, quoteCurrencyOrSymbol));
const pricePromiseResults = await Promise.allSettled(pricePromises);

for (const result of pricePromiseResults)
Expand Down
8 changes: 4 additions & 4 deletions src/exchange/priceManager/priceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import type BigNumber from 'bignumber.js';
import type { Currency, DataSource, Disposable } from '../../common/index';

export interface GetPriceParameters {
baseCurrency: Currency['id'];
quoteCurrency: Currency['id'];
baseCurrencyOrIdOrSymbol: Currency | Currency['id'];
quoteCurrencyOrIdOrSymbol: Currency | Currency['id'];
provider?: string;
dataSource?: DataSource;
}

export interface GetAveragePriceParameters {
baseCurrency: Currency['id'];
quoteCurrency: Currency['id'];
baseCurrencyOrIdOrSymbol: Currency | Currency['id'];
quoteCurrencyOrIdOrSymbol: Currency | Currency['id'];
dataSource?: DataSource;
}

Expand Down
13 changes: 10 additions & 3 deletions src/exchange/priceProvider/atomex/atomexPriceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@ export class AtomexPriceProvider implements PriceProvider {
private readonly exchangeService: ExchangeService
) { }

async getPrice(baseCurrency: Currency['id'], quoteCurrency: Currency['id']): Promise<BigNumber | undefined> {
const symbol = `${baseCurrency}/${quoteCurrency}`;
async getPrice(baseCurrencyOrSymbol: Currency | string, quoteCurrencyOrSymbol: Currency | string): Promise<BigNumber | undefined> {
const baseCurrency = this.getSymbol(baseCurrencyOrSymbol);
const quoteCurrency = this.getSymbol(quoteCurrencyOrSymbol);
const pairSymbol = `${baseCurrency}/${quoteCurrency}`;

const quote = (await this.exchangeService.getTopOfBook([{ from: baseCurrency, to: quoteCurrency }]))?.[0];

return quote && quote.symbol == symbol ? this.getMiddlePrice(quote) : undefined;
return quote && quote.symbol === pairSymbol ? this.getMiddlePrice(quote) : undefined;
}

private getSymbol(currencyOrSymbol: Currency | string): string {
return typeof currencyOrSymbol === 'string' ? currencyOrSymbol : currencyOrSymbol.id;
}

private getMiddlePrice(quote: Quote): BigNumber {
Expand Down
17 changes: 13 additions & 4 deletions src/exchange/priceProvider/binance/binancePriceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,27 @@ export class BinancePriceProvider implements PriceProvider {
this.httpClient = new HttpClient(BinancePriceProvider.baseUrl);
}

async getPrice(baseCurrency: Currency['id'], quoteCurrency: Currency['id']): Promise<BigNumber | undefined> {
const symbol = `${baseCurrency}${quoteCurrency}`;
async getPrice(baseCurrencyOrSymbol: Currency | string, quoteCurrencyOrSymbol: Currency | string): Promise<BigNumber | undefined> {
const baseCurrency = this.getSymbol(baseCurrencyOrSymbol);
const quoteCurrency = this.getSymbol(quoteCurrencyOrSymbol);

const pairSymbol = `${baseCurrency}${quoteCurrency}`;
const allSymbols = await this.getAllSymbols();
if (!allSymbols.has(symbol))
if (!allSymbols.has(pairSymbol))
return undefined;

const urlPath = `${BinancePriceProvider.priceUrlPath}?symbol=${symbol}`;
const urlPath = `${BinancePriceProvider.priceUrlPath}?symbol=${pairSymbol}`;
const responseDto = await this.httpClient.request<BinanceRatesDto | BinanceErrorDto>({ urlPath }, false);

return this.mapRatesDtoToPrice(responseDto);
}

private getSymbol(currencyOrSymbol: Currency | string): string {
const symbol = typeof currencyOrSymbol === 'string' ? currencyOrSymbol : currencyOrSymbol.symbol;

return symbol.toUpperCase();
}

private mapRatesDtoToPrice(dto: BinanceRatesDto | BinanceErrorDto): BigNumber | undefined {
if (isErrorDto(dto))
return undefined;
Expand Down
15 changes: 12 additions & 3 deletions src/exchange/priceProvider/kraken/krakenPriceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@ export class KrakenPriceProvider implements PriceProvider {
this.httpClient = new HttpClient(KrakenPriceProvider.baseUrl);
}

async getPrice(baseCurrency: Currency['id'], quoteCurrency: Currency['id']): Promise<BigNumber | undefined> {
const symbol = `${baseCurrency}${quoteCurrency}`;
const urlPath = `/0/public/Ticker?pair=${symbol}`;
async getPrice(baseCurrencyOrSymbol: Currency | string, quoteCurrencyOrSymbol: Currency | string): Promise<BigNumber | undefined> {
const baseCurrency = this.getSymbol(baseCurrencyOrSymbol);
const quoteCurrency = this.getSymbol(quoteCurrencyOrSymbol);

const pairSymbol = `${baseCurrency}${quoteCurrency}`;
const urlPath = `/0/public/Ticker?pair=${pairSymbol}`;
const responseDto = await this.httpClient.request<KrakenRatesDto>({ urlPath }, false);

return this.mapRatesDtoToPrice(responseDto);
}

private getSymbol(currencyOrSymbol: Currency | string): string {
const symbol = typeof currencyOrSymbol === 'string' ? currencyOrSymbol : currencyOrSymbol.symbol;

return symbol.toUpperCase();
}

private mapRatesDtoToPrice(dto: KrakenRatesDto): BigNumber | undefined {
if (dto.error.length)
return undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/exchange/priceProvider/priceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import type BigNumber from 'bignumber.js';
import type { Currency } from '../../common';

export interface PriceProvider {
getPrice(baseCurrency: Currency['id'], quoteCurrency: Currency['id']): Promise<BigNumber | undefined>;
getPrice(baseCurrencyOrSymbol: Currency | string, quoteCurrencyOrSymbol: Currency | string): Promise<BigNumber | undefined>;
}
12 changes: 7 additions & 5 deletions tests/atomex/atomexProtocolV1/atomexProtocolV1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ describe('Atomex Protocol V1 utils', () => {
const createPriceManager = (prices: Record<string, BigNumber>) => {
const priceManager = new MockPriceManager();

const getPriceImplementation = (baseCurrency: string, quoteCurrency: string) => {
const getPriceImplementation = (baseCurrencyOrIdOrSymbol: Currency | Currency['id'], quoteCurrencyOrIdOrSymbol: Currency | Currency['id']) => {
const baseCurrency = typeof baseCurrencyOrIdOrSymbol === 'string' ? baseCurrencyOrIdOrSymbol : baseCurrencyOrIdOrSymbol.symbol;
const quoteCurrency = typeof quoteCurrencyOrIdOrSymbol === 'string' ? quoteCurrencyOrIdOrSymbol : quoteCurrencyOrIdOrSymbol.symbol;
const symbol = `${baseCurrency}/${quoteCurrency}`;

return prices[symbol];
};

priceManager.getPrice.mockImplementation(async ({ baseCurrency, quoteCurrency }) => {
return getPriceImplementation(baseCurrency, quoteCurrency);
priceManager.getPrice.mockImplementation(async ({ baseCurrencyOrIdOrSymbol, quoteCurrencyOrIdOrSymbol }) => {
return getPriceImplementation(baseCurrencyOrIdOrSymbol, quoteCurrencyOrIdOrSymbol);
});

priceManager.getAveragePrice.mockImplementation(async ({ baseCurrency, quoteCurrency }) => {
return getPriceImplementation(baseCurrency, quoteCurrency);
priceManager.getAveragePrice.mockImplementation(async ({ baseCurrencyOrIdOrSymbol, quoteCurrencyOrIdOrSymbol }) => {
return getPriceImplementation(baseCurrencyOrIdOrSymbol, quoteCurrencyOrIdOrSymbol);
});

return priceManager;
Expand Down

0 comments on commit 279f9d8

Please sign in to comment.