Skip to content

Commit

Permalink
feat: Extend candle info with base, counter and size (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
bennycode authored May 4, 2020
1 parent a41d0bb commit ea0f8cc
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 26 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## Motivation

This project was created to continue an active **Coinbase Pro API** after Coinbase deprecated the official Node.js library on [January, 16 2020](https://github.com/coinbase/coinbase-node/issues/140#issuecomment-574990136). The official predecessor was also deprecated on [July, 19th 2016](https://github.com/coinbase/coinbase-exchange-node/commit/b8347efdb4e2589367c1395b646d283c9c391681).
The purpose of "coinbase-pro-node" is to continue an active **Coinbase Pro API** after Coinbase deprecated the official Node.js library on [January, 16 2020](https://github.com/coinbase/coinbase-node/issues/140#issuecomment-574990136). The official predecessor got deprecated on [July, 19th 2016](https://github.com/coinbase/coinbase-exchange-node/commit/b8347efdb4e2589367c1395b646d283c9c391681).

## Features

Expand Down
4 changes: 2 additions & 2 deletions src/demo/rest-watch-candles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ async function main(): Promise<void> {
const client = initClient();

client.rest.on(ProductEvent.NEW_CANDLE, (productId: string, granularity: CandleGranularity, candle: Candle) => {
console.info('Recent candle', productId, granularity, candle.openTimeString);
console.info('Recent candle', productId, granularity, candle.openTimeInISO);
});

// 3. Get latest candle
const candles = await client.rest.product.getCandles(productId, {
granularity,
});
const latestCandle = candles[candles.length - 1];
const latestOpen = latestCandle.openTimeString;
const latestOpen = latestCandle.openTimeInISO;
console.info('Initial candle', productId, granularity, latestOpen);

// 4. Subscribe to upcoming candles
Expand Down
28 changes: 18 additions & 10 deletions src/product/ProductAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,17 +190,19 @@ describe('ProductAPI', () => {
});

expect(candles.length).toEqual(3);
expect(candles[0].openTime).toEqual(1558261140000);
expect(candles[1].openTime).toEqual(1558261200000);
expect(candles[2].openTime).toEqual(1558261260000);
expect(candles[0].openTimeInMillis).toEqual(1558261140000);
expect(candles[1].openTimeInMillis).toEqual(1558261200000);
expect(candles[2].openTimeInMillis).toEqual(1558261260000);
expect(candles[2].sizeInMillis).toEqual(60000);
});

it('sorts candles ascending by timestamp', async () => {
const from = '2020-03-09T00:00:00.000Z';
const to = '2020-03-15T23:59:59.999Z';
const productId = 'BTC-USD';

nock(global.REST_URL)
.get(`${ProductAPI.URL.PRODUCTS}/BTC-USD/candles`)
.get(`${ProductAPI.URL.PRODUCTS}/${productId}/candles`)
.query(true)
.reply(() => {
const min = new Date(from).getTime();
Expand All @@ -214,15 +216,21 @@ describe('ProductAPI', () => {
return [200, JSON.stringify(data)];
});

const candles = await global.client.rest.product.getCandles('BTC-USD', {
const candles = await global.client.rest.product.getCandles(productId, {
end: to,
granularity: CandleGranularity.ONE_HOUR,
start: from,
});

const firstCandle = candles[0];

expect(candles.length).withContext('7 days * 24 hours = 168 hours / candles').toBe(168);
expect(candles[0].openTimeString).withContext('Starting time of first time slice').toBe(from);
expect(candles[candles.length - 1].openTimeString)
expect(firstCandle.sizeInMillis).withContext('Candle size').toBe(3600000);
expect(firstCandle.base).withContext('Base asset').toBe('BTC');
expect(firstCandle.counter).withContext('Quote asset').toBe('USD');
expect(firstCandle.productId).withContext('Product ID').toBe(productId);
expect(firstCandle.openTimeInISO).withContext('Starting time of first time slice').toBe(from);
expect(candles[candles.length - 1].openTimeInISO)
.withContext('Starting time of last time slice')
.toBe('2020-03-15T23:00:00.000Z');
});
Expand Down Expand Up @@ -317,11 +325,11 @@ describe('ProductAPI', () => {
.and.callFake((emittedProductId: string, emittedGranularity: CandleGranularity, candle: Candle) => {
expect(emittedProductId).toBe(productId);
expect(emittedGranularity).toBe(granularity);
const {openTimeString} = candle;
if (openTimeString === expectedISO) {
const {openTimeInISO} = candle;
if (openTimeInISO === expectedISO) {
done();
} else {
done.fail(`Received "${openTimeString}" but expected "${expectedISO}".`);
done.fail(`Received "${openTimeInISO}" but expected "${expectedISO}".`);
}
});

Expand Down
42 changes: 29 additions & 13 deletions src/product/ProductAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,26 @@ type Timestamp = number;
type Volume = number;

export interface Candle {
/** ID of base asset */
base: string;
/** Closing price (last trade) in the bucket interval */
close: Close;
/** ID of quote asset */
counter: string;
/** Highest price during the bucket interval */
high: High;
/** Lowest price during the bucket interval */
low: Low;
/** Opening price (first trade) in the bucket interval */
open: Open;
/** Bucket start time converted to milliseconds (note: Coinbase Pro actually uses seconds) */
openTime: Timestamp;
/** Bucket start time in simplified extended ISO 8601 format */
openTimeString: ISO_8601_MS_UTC;
openTimeInISO: ISO_8601_MS_UTC;
/** Bucket start time converted to milliseconds (note: Coinbase Pro actually uses seconds) */
openTimeInMillis: number;
/** Product ID / Symbol */
productId: string;
/** Candle size in milliseconds */
sizeInMillis: number;
/** Volume of trading activity during the bucket interval */
volume: Volume;
}
Expand Down Expand Up @@ -195,13 +203,15 @@ export class ProductAPI {
*/
async getCandles(productId: string, params: HistoricRateRequest): Promise<Candle[]> {
const resource = `${ProductAPI.URL.PRODUCTS}/${productId}/candles`;
let rawCandles: RawCandle[] = [];

const candleSizeInMillis = params.granularity * 1000;
const potentialParams = params as HistoricRateRequestWithTimeSpan;

let rawCandles: RawCandle[] = [];

if (potentialParams.start && potentialParams.end) {
const fromInMillis = new Date(potentialParams.start).getTime();
const toInMillis = new Date(potentialParams.end).getTime();
const candleSizeInMillis = params.granularity * 1000;

const bucketsInMillis = CandleBucketUtil.getBucketsInMillis(fromInMillis, toInMillis, candleSizeInMillis);
const bucketsInISO = CandleBucketUtil.getBucketsInISO(bucketsInMillis);
Expand All @@ -222,7 +232,9 @@ export class ProductAPI {
rawCandles = response.data;
}

return rawCandles.map(this.mapCandle).sort((a, b) => a.openTime - b.openTime);
return rawCandles
.map(candle => this.mapCandle(candle, candleSizeInMillis, productId))
.sort((a, b) => a.openTimeInMillis - b.openTimeInMillis);
}

/**
Expand Down Expand Up @@ -363,16 +375,21 @@ export class ProductAPI {
return response.data;
}

private mapCandle(payload: number[]): Candle {
private mapCandle(payload: number[], sizeInMillis: number, productId: string): Candle {
const [time, low, high, open, close, volume] = payload;
const openTime = time * 1000; // Map seconds to milliseconds
const [base, counter] = productId.split('-');
const openTimeInMillis = time * 1000; // Map seconds to milliseconds
return {
base,
close,
counter,
high,
low,
open,
openTime,
openTimeString: new Date(openTime).toISOString(),
openTimeInISO: new Date(openTimeInMillis).toISOString(),
openTimeInMillis,
productId: productId,
sizeInMillis,
volume,
};
}
Expand All @@ -381,8 +398,7 @@ export class ProductAPI {
// Emit matched candle
this.restClient.emit(ProductEvent.NEW_CANDLE, productId, granularity, candle);
// Cache timestamp of upcoming candle
const {openTime} = candle;
const nextOpenTime = CandleBucketUtil.addUnitISO(openTime, granularity, 1);
const nextOpenTime = CandleBucketUtil.addUnitISO(candle.openTimeInMillis, granularity, 1);
this.watchCandlesConfig[productId][granularity].expectedISO = nextOpenTime;
}

Expand All @@ -394,7 +410,7 @@ export class ProductAPI {
start: expectedTimestampISO,
});

const matches = candles.filter(candle => candle.openTimeString === expectedTimestampISO);
const matches = candles.filter(candle => candle.openTimeInISO === expectedTimestampISO);
if (matches.length > 0) {
const matchedCandle = matches[0];
this.emitCandle(productId, granularity, matchedCandle);
Expand Down

0 comments on commit ea0f8cc

Please sign in to comment.