From 4ab0cc9659b8f4e97cb03cca440f01d12f5d4c0b Mon Sep 17 00:00:00 2001 From: 7400 <7400> Date: Thu, 8 Feb 2024 12:32:44 -0800 Subject: [PATCH] historical ohlc support, edit tvWidget to enable. --- public/ohlc | 1 + src/charts/chart.js | 3 + src/charts/datafeed.js | 166 +++++++++++++++++++++++++++++++++++++++++ src/charts/jBars.js | 107 ++++++++++++++++++++++++++ 4 files changed, 277 insertions(+) create mode 120000 public/ohlc create mode 100644 src/charts/datafeed.js create mode 100644 src/charts/jBars.js diff --git a/public/ohlc b/public/ohlc new file mode 120000 index 0000000..1b86e17 --- /dev/null +++ b/public/ohlc @@ -0,0 +1 @@ +../../../ohlc/ \ No newline at end of file diff --git a/src/charts/chart.js b/src/charts/chart.js index b4899da..d79fb48 100644 --- a/src/charts/chart.js +++ b/src/charts/chart.js @@ -1,5 +1,6 @@ import {useChartOrderStore} from "@/orderbuild.js"; import {invokeCallbacks, prototype} from "@/common.js"; +import datafeed from "./datafeed.js"; export let widget = null export let chart = null @@ -29,9 +30,11 @@ export function initWidget(el) { // debug: true, autosize: true, symbol: 'AAPL', + // symbol: 'Bitfinex:BTC/USD', // use this for ohlc interval: '1D', container: el, datafeed: new Datafeeds.UDFCompatibleDatafeed("https://demo-feed-data.tradingview.com"), + // datafeed: datafeed, // use this for ohlc locale: "en", disabled_features: [], enabled_features: [], diff --git a/src/charts/datafeed.js b/src/charts/datafeed.js new file mode 100644 index 0000000..d968c0f --- /dev/null +++ b/src/charts/datafeed.js @@ -0,0 +1,166 @@ +// import { +// makeApiRequest, +// generateSymbol, +// parseFullSymbol, +// } from './helpers.js'; +// import { +// subscribeOnStream, +// unsubscribeFromStream, +// } from './streaming.js'; +import {jBars} from './jBars.js'; + +const lastBarsCache = new Map(); + +// DatafeedConfiguration implementation +const configurationData = { + // Represents the resolutions for bars supported by your datafeed + // supported_resolutions: ['1D', '1W', '1M'], + supported_resolutions: ['1D'], + + // The `exchanges` arguments are used for the `searchSymbols` method if a user selects the exchange + exchanges: [{ + value: 'Bitfinex', + name: 'Bitfinex', + desc: 'Bitfinex', + }, + ], + // The `symbols_types` arguments are used for the `searchSymbols` method if a user selects this symbol type + symbols_types: [{ + name: 'crypto', + value: 'crypto', + }, + ], +}; + +// Obtains all symbols for all exchanges supported by CryptoCompare API +async function getAllSymbols() { + // const data = await makeApiRequest('data/v3/all/exchanges'); + // let allSymbols = []; + return [{ + symbol: 'BTC/USD', + full_name: 'Bitfinex:BTC/USD', + description: 'BTC/USD', + exchange: 'Bitfinex', + type: 'crypto'} + ]; +} + +export default { + onReady: (callback) => { + console.log('[onReady]: Method call'); + setTimeout(() => callback(configurationData)); + }, + + searchSymbols: async ( + userInput, + exchange, + symbolType, + onResultReadyCallback, + ) => { + console.log('[searchSymbols]: Method call'); + const symbols = await getAllSymbols(); + const newSymbols = symbols.filter(symbol => { + const isExchangeValid = exchange === '' || symbol.exchange === exchange; + const isFullSymbolContainsInput = symbol.full_name + .toLowerCase() + .indexOf(userInput.toLowerCase()) !== -1; + return isExchangeValid && isFullSymbolContainsInput; + }); + onResultReadyCallback(newSymbols); + }, + + resolveSymbol: async ( + symbolName, + onSymbolResolvedCallback, + onResolveErrorCallback, + extension + ) => { + console.log('[resolveSymbol]: Method call', symbolName); + const symbols = await getAllSymbols(); + const symbolItem = symbols.find(({ + full_name, + }) => full_name === symbolName); + if (!symbolItem) { + console.log('[resolveSymbol]: Cannot resolve symbol', symbolName); + onResolveErrorCallback('cannot resolve symbol'); + return; + } + // Symbol information object + const symbolInfo = { + ticker: symbolItem.full_name, + name: symbolItem.symbol, + description: symbolItem.description, + type: symbolItem.type, + session: '24x7', + timezone: 'Etc/UTC', + exchange: symbolItem.exchange, + minmov: 1, + pricescale: 100, + has_intraday: false, + has_no_volume: true, + has_weekly_and_monthly: false, + supported_resolutions: configurationData.supported_resolutions, + volume_precision: 2, + data_status: 'streaming', + }; + + console.log('[resolveSymbol]: Symbol resolved', symbolName); + onSymbolResolvedCallback(symbolInfo); + }, + + getBars: async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => { + const { from, to, firstDataRequest } = periodParams; + console.log('[getBars]: Method call', symbolInfo, resolution, from, to); + + // const parsedSymbol = parseFullSymbol(symbolInfo.full_name); + // const urlParameters = { + // e: parsedSymbol.exchange, + // fsym: parsedSymbol.fromSymbol, + // tsym: parsedSymbol.toSymbol, + // toTs: to, + // limit: 2000, + // }; + + try { + var bars = await jBars(from, to, resolution); + + if (firstDataRequest) { + lastBarsCache.set(symbolInfo.full_name, { + ...bars[bars.length - 1], + }); + } + console.log(`[getBars]: returned ${bars.length} bar(s)`); + onHistoryCallback(bars, { + noData: false, + }); + } catch (error) { + console.log('[getBars]: Get error', error); + onErrorCallback(error); + } + }, + + subscribeBars: ( + symbolInfo, + resolution, + onRealtimeCallback, + subscriberUID, + onResetCacheNeededCallback, + ) => { + console.log('[subscribeBars]: Method call with subscriberUID:', subscriberUID); + // throw Error('subscribeBars unimplemented'); + // subscribeOnStream( + // symbolInfo, + // resolution, + // onRealtimeCallback, + // subscriberUID, + // onResetCacheNeededCallback, + // lastBarsCache.get(symbolInfo.full_name), + // ); + }, + + unsubscribeBars: (subscriberUID) => { + console.log('[unsubscribeBars]: Method call with subscriberUID:', subscriberUID); + // throw Error('unsubscribeBars unimplemented'); + // unsubscribeFromStream(subscriberUID); + }, +}; diff --git a/src/charts/jBars.js b/src/charts/jBars.js new file mode 100644 index 0000000..fbbaae3 --- /dev/null +++ b/src/charts/jBars.js @@ -0,0 +1,107 @@ +export async function jBars (from, to, res) { + + console.log('[jBars]: Method call', res, from, to); + + var fromDate = new Date(from*1000); + var toDate = new Date(to*1000); + console.log("fromDate:", fromDate.toUTCString()); + console.log("toDate: ", toDate.toUTCString()); + + const contract = "0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443"; + + // check parameters + + if (res != "1D") throw Error("Only 1D resolution currently supported"); + + console.assert(fromDate.getUTCHours() == 0, "hours should be zero"); + console.assert(fromDate.getUTCMinutes() == 0, "minutes should be zero"); + console.assert(fromDate.getUTCSeconds() == 0, "seconds should be zero"); + console.assert(fromDate.getUTCMilliseconds() == 0, "milliseconds should be zero"); + + // Spoof data + + var spoof; + { + const yr = "2022"; + const mo = "01"; + const url = `/ohlc/42161/${contract}/${res}/${yr}/${contract}-${res}-${yr}${mo}.json` + const response = await fetch(url); + spoof = await response.json(); + } + + var bars = []; + + for ( // Once around for each sample in from-to range + let iDate = fromDate, + // loop state + iMonth = -1, + iolhc = 0, + ohlc; + iDate < toDate; + iDate.setUTCDate(iDate.getUTCDate()+1) + ) { + + let bar = undefined; + + // Fetch one sample file as needed + + if (iMonth != iDate.getUTCMonth()) { + const yr = iDate.getUTCFullYear(); + const mo = String(iDate.getUTCMonth()+1).padStart(2, '0'); + const url = `/ohlc/42161/${contract}/${res}/${yr}/${contract}-${res}-${yr}${mo}.json` + let response = await fetch(url); + if (response.ok) { + ohlc = await response.json(); + } else { + ohlc = []; // no file, then empty data + } + iMonth = iDate.getUTCMonth(); + iolhc = 0; + } + + let ohlcDate = iolhc >= ohlc.length ? undefined : new Date(ohlc[iolhc][0]+'Z'); + + // no ohlc sample, insert a visible sample + + if (ohlcDate == undefined) { + bar = { + time: iDate.getTime(), + open: 50, + high: 50, + low: 0, + close: 0, + } + } + + // ohlc sample not for this time, insert invisible sample + + else if ( iDate.getTime() != ohlcDate.getTime() ) { + + bar = { + time: iDate.getTime(), + // open: 100, + // high: 100, + // low: 0, + // close: 0, + } + + // Copy ohlc sample + + } else { + bar = { + time: iDate.getTime(), + open: ohlc[iolhc][1], + high: ohlc[iolhc][2], + low: ohlc[iolhc][3], + close: ohlc[iolhc][4], + } + iolhc++; + } + + if (bar==undefined) throw "bar==undefined"; + bars.push(bar); + + } + + return bars; +}