@drift-labs/common
Version:
Common functions for Drift
365 lines • 15.1 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.DriftTvFeed = void 0;
const sdk_1 = require("@drift-labs/sdk");
const types_1 = require("../types");
const Candle_1 = require("../utils/candles/Candle");
const pollingSequenceGuard_1 = require("../utils/pollingSequenceGuard");
const candleClient_1 = require("./candleClient");
const market_1 = require("../common-ui-utils/market");
const DRIFT_V2_START_TS = 1668470400; // 15th November 2022 ... 2022-11-15T00:00:00.000Z
const resolutions = [
'1',
'3',
'5',
'15',
'30',
'60',
'240',
'6H',
'8H',
'1D',
'3D',
'1W',
'1M',
];
const tvResolutionStringToStandardResolutionString = (tvResolutionString) => {
switch (tvResolutionString) {
case '1':
return '1';
case '5':
return '5';
case '15':
return '15';
case '60':
return '60';
case '240':
return '240';
case 'D':
case '1D':
return 'D';
case 'W':
case '1W':
return 'W';
case 'M':
case '1M':
return 'M';
}
};
const DATAFEED_CONFIG = {
exchanges: [],
supported_resolutions: [...resolutions],
currency_codes: [],
supports_marks: true,
supports_time: false,
supports_timescale_marks: false,
symbols_types: [],
};
const findMarketBySymbol = (symbol, perpMarketConfigs, spotMarketConfigs) => {
if (!symbol) {
throw new Error(`TVFeed::No symbol provided`);
}
const sanitisedSymbol = symbol.toLowerCase().replace('/usdc', ''); // Lowercase and replace /usdc (for spot markets) to santise symbol for lookup
const isPerp = sanitisedSymbol.toLowerCase().includes('perp');
const matchingMarketConfig = isPerp
? perpMarketConfigs.find((mkt) => mkt.symbol.toLowerCase().includes(sanitisedSymbol.toLowerCase()))
: spotMarketConfigs.find((mkt) => mkt.symbol.toLowerCase().includes(sanitisedSymbol.toLowerCase()));
if (!matchingMarketConfig) {
throw new Error(`TVFeed::No market found for symbol ${symbol}`);
}
if (isPerp) {
return {
type: 'perp',
config: matchingMarketConfig,
};
}
return {
type: 'spot',
config: matchingMarketConfig,
};
};
const candleFetchingPollKey = Symbol('candleFetchingPollKey');
const candleToTvBar = (candle, candleType) => {
const useOraclePrice = candleType === types_1.CandleType.ORACLE_PRICE;
return {
time: candle.ts * 1000,
open: useOraclePrice ? candle.oracleOpen : candle.fillOpen,
high: useOraclePrice ? candle.oracleHigh : candle.fillHigh,
low: useOraclePrice ? candle.oracleLow : candle.fillLow,
close: useOraclePrice ? candle.oracleClose : candle.fillClose,
volume: candle.quoteVolume,
};
};
const PerpMarketConfigToTVMarketInfo = (marketConfig) => {
return {
symbol: marketConfig.symbol,
full_name: marketConfig.fullName,
description: marketConfig.fullName,
exchange: 'Drift',
ticker: marketConfig.symbol,
type: 'crypto',
};
};
const SpotMarketConfigToTVMarketInfo = (marketConfig) => {
return {
symbol: marketConfig.symbol,
full_name: marketConfig.symbol,
description: marketConfig.symbol,
exchange: 'Drift',
ticker: marketConfig.symbol,
type: 'crypto',
};
};
class DriftTvFeed {
constructor(env, candleType, driftClient, perpMarketConfigs, spotMarketConfigs, tvAppTradeDataManager, marketDecimalConfig) {
this.searchMarkets = (symbol) => {
const res = [];
const currentPerpMarkets = this.perpMarketConfigs;
const currentSpotMarkets = this.spotMarketConfigs;
if (!symbol) {
res.push(PerpMarketConfigToTVMarketInfo(currentPerpMarkets[0]));
}
else {
for (const market of currentPerpMarkets) {
const lowerCaseMarket = market.symbol.toLowerCase();
if (lowerCaseMarket.includes(symbol.toLowerCase())) {
res.push(PerpMarketConfigToTVMarketInfo(market));
}
}
for (const market of currentSpotMarkets) {
const lowerCaseMarket = market.symbol.toLowerCase();
if (lowerCaseMarket.includes(symbol.toLowerCase())) {
res.push(SpotMarketConfigToTVMarketInfo(market));
}
}
}
return res.map((mkt) => {
return {
...mkt,
exchange: 'Drift',
type: 'crypto',
};
});
};
/**
* TradingView sometimes asks for a FROM timestamp halfway between two candles, this isn't compatible with the new candles API so we round these down the nearest candle - which should be the exact same candle!!
* @param timestamp
* @param resolution
* @returns
*/
this.roundFromTimestampToExactCandleTs = (timestamp, resolution) => {
const ROUND_UP = true;
const timestampMs = timestamp * 1000;
const candleLengthMs = Candle_1.Candle.resolutionStringToCandleLengthMs(resolution);
const remainderMs = timestampMs % candleLengthMs;
if (remainderMs === 0) {
return timestamp;
}
const roundedDownTimestampMs = timestampMs - remainderMs;
const roundedTimestampMs = ROUND_UP
? roundedDownTimestampMs + candleLengthMs
: roundedDownTimestampMs;
return roundedTimestampMs / 1000;
};
this.formatTVRequestedRange = (fromTs, toTs, resolution) => {
const formattedFromTs = this.roundFromTimestampToExactCandleTs(fromTs, resolution); // TradingView sometimes asks for a FROM timestamp halfway between two candles, so we round down to the nearest candle
const formattedToTs = Math.floor(Math.min(toTs, Date.now() / 1000)); // TradingView sometimes asks for a TO timestamp in the future, so we cap it at the current timestamp
return {
from: formattedFromTs,
to: formattedToTs,
};
};
this.env = env;
this.candleType = candleType;
this.candleClient = new candleClient_1.CandleClient();
this.driftClient = driftClient;
this.perpMarketConfigs = perpMarketConfigs;
this.spotMarketConfigs = spotMarketConfigs;
this.tvAppTradeDataManager = tvAppTradeDataManager;
this.marketDecimalConfig = marketDecimalConfig;
}
resetCache() {
var _a;
(_a = this.onResetCache) === null || _a === void 0 ? void 0 : _a.call(this);
}
// IExternalDatafeed implementation
onReady(callback) {
// using setTimeout because tradingview wants this to resolve asynchronously
setTimeout(() => callback(DATAFEED_CONFIG), 0);
}
// IDatafeedChartApi implementation
searchSymbols(userInput, _exchange, _symbolType, onResult) {
if (!userInput)
return onResult([]);
const res = this.searchMarkets(userInput);
onResult(res);
}
resolveSymbol(symbolName, onResolve, onError) {
var _a;
const targetMarket = findMarketBySymbol(symbolName, this.perpMarketConfigs, this.spotMarketConfigs);
if (targetMarket) {
const tvMarketName = targetMarket.config.symbol;
// Use market-specific decimal precision from configuration
const baseAssetSymbol = market_1.MARKET_UTILS.getBaseAssetSymbol(symbolName);
const marketDecimals = (_a = this.marketDecimalConfig) === null || _a === void 0 ? void 0 : _a[baseAssetSymbol];
let priceScale;
if (marketDecimals !== undefined) {
// Use configured market decimals
priceScale = 10 ** marketDecimals;
}
else {
// Fall back to original tick size calculation
let tickSize;
if (targetMarket.type === 'perp') {
tickSize = this.driftClient
.getPerpMarketAccount(targetMarket.config.marketIndex)
.amm.orderTickSize.toNumber();
}
else {
tickSize = this.driftClient
.getSpotMarketAccount(targetMarket.config.marketIndex)
.orderTickSize.toNumber();
}
const pricePrecisionExp = sdk_1.PRICE_PRECISION_EXP.toNumber();
const tickSizeExp = Math.ceil(Math.log10(tickSize));
const priceScaleExponent = Math.max(0, pricePrecisionExp - tickSizeExp);
priceScale = 10 ** priceScaleExponent;
}
onResolve({
name: tvMarketName,
full_name: tvMarketName,
description: tvMarketName,
exchange: 'Drift',
ticker: targetMarket.config.symbol,
type: 'crypto',
session: '24x7',
timezone: 'Etc/UTC',
listed_exchange: 'Drift',
format: 'price',
pricescale: priceScale,
minmov: 1,
supported_resolutions: [...resolutions],
has_intraday: true,
intraday_multipliers: ['1', '5', '15', '60', '240'],
});
return;
}
onError(`Couldn't find market for symbol ${symbolName}`);
}
// https://www.tradingview.com/charting-library-docs/latest/api/interfaces/Charting_Library.Mark/ reference for marks type
async getMarks(_symbolInfo, startDate, endDate, onDataCallback, _resolution) {
if (!this.tvAppTradeDataManager) {
return;
}
const orderHistory = await this.tvAppTradeDataManager.getFilledOrdersData(startDate, endDate);
const currentUserAccount = this.tvAppTradeDataManager.getCurrentSubAccountAddress();
const tradeMarks = orderHistory.map((trade) => {
const currentUserIsMaker = trade.maker === currentUserAccount;
const currentUserIsTaker = trade.taker === currentUserAccount;
let isLong;
if (currentUserIsMaker) {
isLong = trade.makerOrderDirection === 'long';
}
else if (currentUserIsTaker) {
isLong = trade.takerOrderDirection === 'long';
}
const color = isLong ? '#5DD5A0' : '#FF615C';
const baseAmount = Number(trade.baseAssetAmountFilled);
const quoteAmount = Number(trade.quoteAssetAmountFilled);
const avgPrice = quoteAmount / baseAmount;
const formatPrice = (price) => {
if (price >= 1) {
return price.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
else {
if (price === 0)
return '0.0000';
if (price < 0.00001)
return '<0.00001';
return price.toFixed(4);
}
};
return {
id: trade.txSig,
time: trade.ts,
color: {
background: color,
border: '#152A44',
},
borderWidth: 1,
hoveredBorderWidth: 1,
text: `${isLong ? 'Long' : 'Short'} at $${formatPrice(avgPrice)}`,
label: isLong ? 'B' : 'S',
labelFontColor: '#000000',
minSize: 16,
};
});
onDataCallback(tradeMarks);
}
async getBars(symbolInfo, resolution, periodParams, onResult, _onError) {
var _a;
// Can automatically return no data if the requested range is before the Drift V2 launch
if (periodParams.to < DRIFT_V2_START_TS) {
onResult([], {
noData: true,
});
return;
}
const symbolToUse = (_a = symbolInfo.ticker) !== null && _a !== void 0 ? _a : symbolInfo.name;
const targetResolution = tvResolutionStringToStandardResolutionString(resolution);
const targetMarket = findMarketBySymbol(symbolToUse, this.perpMarketConfigs, this.spotMarketConfigs);
const targetMarketId = targetMarket.type === 'perp'
? types_1.MarketId.createPerpMarket(targetMarket.config.marketIndex)
: types_1.MarketId.createSpotMarket(targetMarket.config.marketIndex);
const fetchCandles = async () => {
const formattedTsRange = this.formatTVRequestedRange(periodParams.from, periodParams.to, targetResolution);
const candles = await this.candleClient.fetch({
env: this.env,
marketId: targetMarketId,
resolution: targetResolution,
fromTs: formattedTsRange.from,
toTs: formattedTsRange.to,
});
return candles;
};
const candlesResult = await pollingSequenceGuard_1.PollingSequenceGuard.fetch(candleFetchingPollKey, fetchCandles);
if (candlesResult.length === 0) {
onResult([], {
noData: true,
});
return;
}
const bars = candlesResult.map((candle) => candleToTvBar(candle, this.candleType));
onResult(bars, {
noData: candlesResult.length === 0,
});
return;
}
async subscribeBars(symbolInfo, resolution, onTick, subscriberGuid, onResetCache) {
this.onResetCache = onResetCache;
const targetResolution = tvResolutionStringToStandardResolutionString(resolution);
const targetMarket = findMarketBySymbol(symbolInfo.ticker, this.perpMarketConfigs, this.spotMarketConfigs);
const targetMarketId = targetMarket.type === 'perp'
? types_1.MarketId.createPerpMarket(targetMarket.config.marketIndex)
: types_1.MarketId.createSpotMarket(targetMarket.config.marketIndex);
// First create the subscription and wait for it to be ready
await this.candleClient.subscribe({
resolution: targetResolution,
marketId: targetMarketId,
env: this.env,
}, subscriberGuid);
// Then set up the event listener once the eventBus exists
this.candleClient.on(subscriberGuid, 'candle-update', (newCandle) => {
const newBar = candleToTvBar(newCandle, this.candleType);
onTick(newBar);
});
}
unsubscribeBars(listenerGuid) {
this.candleClient.unsubscribe(listenerGuid);
}
}
exports.DriftTvFeed = DriftTvFeed;
//# sourceMappingURL=tvFeed.js.map
;