diff --git a/index.html b/index.html index 3b524b8..06ea651 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,8 @@ dexorder + + diff --git a/src/charts/datafeed.js b/src/charts/datafeed.js index 67ddf77..06168e7 100644 --- a/src/charts/datafeed.js +++ b/src/charts/datafeed.js @@ -1,3 +1,8 @@ +import { + subscribeOnStream, + unsubscribeFromStream, +} from './streaming.js'; + import {jBars} from './jBars.js'; import {metadata} from "@/version.js"; import FlexSearch from "flexsearch"; @@ -121,24 +126,6 @@ export default { 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); - // }, - searchSymbols: async ( userInput, exchange, @@ -207,6 +194,7 @@ export default { }); } console.log(`[getBars]: returned ${bars.length} bar(s)`); + console.log(bars); onHistoryCallback(bars, { noData: false, }); @@ -224,9 +212,20 @@ export default { onResetCacheNeededCallback, ) => { console.log('[subscribeBars]: Method call with subscriberUID:', subscriberUID); + return; // disable + subscribeOnStream( + symbolInfo, + resolution, + onRealtimeCallback, + subscriberUID, + onResetCacheNeededCallback, + lastBarsCache.get(symbolInfo.full_name), + ); }, unsubscribeBars: (subscriberUID) => { console.log('[unsubscribeBars]: Method call with subscriberUID:', subscriberUID); + return; // disable + unsubscribeFromStream(subscriberUID); }, }; diff --git a/src/charts/helpers.js b/src/charts/helpers.js new file mode 100644 index 0000000..9e32849 --- /dev/null +++ b/src/charts/helpers.js @@ -0,0 +1,32 @@ +// Makes requests to CryptoCompare API +export async function makeApiRequest(path) { + try { + const response = await fetch(`https://min-api.cryptocompare.com/${path}`); + return response.json(); + } catch (error) { + throw new Error(`CryptoCompare request error: ${error.status}`); + } +} + +// Generates a symbol ID from a pair of the coins +export function generateSymbol(exchange, fromSymbol, toSymbol) { + const short = `${fromSymbol}/${toSymbol}`; + return { + short, + full: `${exchange}:${short}`, + }; +} + +// Returns all parts of the symbol +export function parseFullSymbol(fullSymbol) { + const match = fullSymbol.match(/^(\w+):(\w+)\/(\w+)$/); + if (!match) { + return null; + } + + return { + exchange: match[1], + fromSymbol: match[2], + toSymbol: match[3], + }; +} diff --git a/src/charts/jBars.js b/src/charts/jBars.js index 28f901d..669f892 100644 --- a/src/charts/jBars.js +++ b/src/charts/jBars.js @@ -2,29 +2,36 @@ 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); + + var fromDate = new Date(from*1000); + if (res=="1W") { // for 1W, day must be Sunday + const day = fromDate.getUTCDay(); // 0<=day<7 + fromDate.setDate(fromDate.getDate() + (7-day)%7 ); + } + + // Set fromDate to be compatible with Tim's datafiles. + // This potentially increases number of samples returned. + + if (res.endsWith("W") || res.endsWith("D")) { // Days/Weeks -- set to midnight + fromDate.setUTCHours(0, 0, 0); + } else { + let minutesRes = parseInt(res); + if (minutesRes >= 60) { // Hours + let hoursRes = Math.floor(minutesRes/60); + let fromHours = fromDate.getUTCHours(); + fromDate.setUTCHours(fromHours - fromHours % hoursRes, 0, 0, 0); + } else { // Minutes + let fromMinutes = fromDate.getUTCMinutes(); + fromDate.setUTCMinutes(fromMinutes - fromMinutes % minutesRes, 0, 0); + } + } + console.log("fromDate:", fromDate.toUTCString()); console.log("toDate: ", toDate.toUTCString()); const contract = "0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443"; - - // check parameters - // 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(); - // } + // const contract = "0xC6962004f452bE9203591991D15f6b388e09E8D0"; const file_res = ['1m', '3m', '5m', '10m', '15m', '30m', '1H', '2H', '4H', '8H', '12H', '1D', '2D', '3D', '1W',]; const supported_res = ['1', '3', '5', '10', '15', '30', '60', '120', '240', '480', '720', '1D', '2D', '3D', '1W',]; @@ -64,13 +71,18 @@ export async function jBars (from, to, res) { const mo = String(iDate.getUTCMonth()+1).padStart(2, '0'); // January is month 0 in Date object const date = is_daily_res ? String(iDate.getUTCDate()).padStart(2, '0') : ""; const yrmo = !is_single_res ? `-${yr}${mo}` : ""; + const server = "https://alpha.dexorder.trade" - let url = `/ohlc/42161/${contract}/${fres}${yrdir}/${contract}-${fres}${yrmo}${date}.json`; + let url = `${server}/ohlc/42161/${contract}/${fres}${yrdir}/${contract}-${fres}${yrmo}${date}.json`; let response = await fetch(url); if (response.ok) { ohlc = await response.json(); + console.log(`Fetch: ${ohlc.length} samples from ${url}`) + console.log(`from: ${new Date(ohlc[0][0]*1000)}`) + console.log(`to: ${new Date(ohlc[ohlc.length-1][0]*1000)}`) } else { + console.log(`Fetch: file not found: ${url}`) ohlc = []; // no file, then empty data } iFile = new Date(iDate); @@ -80,14 +92,18 @@ export async function jBars (from, to, res) { // Skip samples not for our time for(; iohlc < ohlc.length; iohlc++ ) { - if ( new Date(ohlc[iohlc][0]+'Z').getTime() >= iDate.getTime() ) break; + // if ( new Date(ohlc[iohlc][0]+'Z').getTime() >= iDate.getTime() ) break; + if ( ohlc[iohlc][0]*1000 >= iDate.getTime() ) break; } - let ohlcDate = iohlc >= ohlc.length ? undefined : new Date(ohlc[iohlc][0]+'Z'); + // console.log(`iohlc: ${iohlc}`); + + // let ohlcDate = iohlc >= ohlc.length ? undefined : new Date(ohlc[iohlc][0]+'Z'); + let ohlcDate = iohlc >= ohlc.length ? undefined : new Date(ohlc[iohlc][0]*1000); // no ohlc sample file, insert missing sample - const visible_missing_samples = true; + const visible_missing_samples = false; if (ohlcDate == undefined) { bar = { time: iDate.getTime(), @@ -102,9 +118,7 @@ export async function jBars (from, to, res) { } // file exists, but ohlc sample not for this time, insert missing sample - else if ( iDate.getTime() != ohlcDate.getTime() ) { - bar = { time: iDate.getTime(), } @@ -114,10 +128,10 @@ export async function jBars (from, to, res) { low: 0, close: 0, }); + } // Copy ohlc sample - - } else { + else { bar = { time: iDate.getTime(), open: ohlc[iohlc][1] ?? ohlc[iohlc][4], // open diff --git a/src/charts/streaming.js b/src/charts/streaming.js new file mode 100644 index 0000000..3566e6a --- /dev/null +++ b/src/charts/streaming.js @@ -0,0 +1,171 @@ +import { parseFullSymbol } from './helpers.js'; + +const socket = io('wss://streamer.cryptocompare.com'); +const channelToSubscription = new Map(); + +socket.on('connect', () => { + console.log('[socket] Connected'); +}); + +socket.on('disconnect', (reason) => { + console.log('[socket] Disconnected:', reason); +}); + +socket.on('error', (error) => { + console.log('[socket] Error:', error); +}); + +socket.on('m', data => { + console.log('[socket] Message:', data); + const [ + eventTypeStr, + exchange, + fromSymbol, + toSymbol, + , + , + tradeTimeStr, + , + tradePriceStr, + ] = data.split('~'); + + if (parseInt(eventTypeStr) !== 0) { + // Skip all non-trading events + return; + } + const tradePrice = parseFloat(tradePriceStr); + const tradeTime = parseInt(tradeTimeStr); + const channelString = `0~${exchange}~${fromSymbol}~${toSymbol}`; + const subscriptionItem = channelToSubscription.get(channelString); + if (subscriptionItem === undefined) { + return; + } + const lastDailyBar = subscriptionItem.lastDailyBar; + const nextDailyBarTime = getNextDailyBarTime(lastDailyBar.time, subscriptionItem.resolution); + + console.log("tradeTime ", tradeTime, new Date(tradeTime)) + console.log("lastDailyBar.time", lastDailyBar.time, new Date(lastDailyBar.time)) + console.log("nextDailyBarTime ", nextDailyBarTime, new Date(nextDailyBarTime)) + + let bar; + if (tradeTime >= nextDailyBarTime) { + bar = { + time: nextDailyBarTime, + open: tradePrice, + high: tradePrice, + low: tradePrice, + close: tradePrice, + }; + console.log('[socket] Generate new bar', bar); + console.log("time:", bar.time.toString(), new Date(bar.time).toUTCString()) + } else { + bar = { + ...lastDailyBar, + high: Math.max(lastDailyBar.high, tradePrice), + low: Math.min(lastDailyBar.low, tradePrice), + close: tradePrice, + }; + console.log('[socket] Update the latest bar by price', tradePrice); + } + subscriptionItem.lastDailyBar = bar; + + // Send data to every subscriber of that symbol + subscriptionItem.handlers.forEach(handler => handler.callback(bar)); +}); + +function getNextDailyBarTime(barTime, res) { + const date = new Date(barTime); + const resDigits = res.slice(0, -1) + if (res.endsWith("W")) { + date.setDate(date.getDate() + parseInt(resDigits)*7); + } else if (res.endsWith("D")) { + date.setDate(date.getDate() + parseInt(resDigits)); + } else { + date.setMinutes(date.getMinutes() + parseInt(res)) + } + return date.getTime(); +} + +export function subscribeOnStream( + symbolInfo, + resolution, + onRealtimeCallback, + subscriberUID, + onResetCacheNeededCallback, + lastDailyBar, +) { + // return; + const parsedSymbol = parseFullSymbol(symbolInfo.full_name); + const channelString = `0~${parsedSymbol.exchange}~${parsedSymbol.fromSymbol}~${parsedSymbol.toSymbol}`; + const handler = { + id: subscriberUID, + callback: onRealtimeCallback, + }; + let subscriptionItem = channelToSubscription.get(channelString); + if (subscriptionItem) { + // Already subscribed to the channel, use the existing subscription + subscriptionItem.handlers.push(handler); + return; + } + subscriptionItem = { + subscriberUID, + resolution, + lastDailyBar, + handlers: [handler], + }; + channelToSubscription.set(channelString, subscriptionItem); + console.log('[subscribeBars]: Subscribe to streaming. Channel:', channelString); + socket.emit('SubAdd', { subs: [channelString] }); +} + +export function unsubscribeFromStream(subscriberUID) { + // return; + // Find a subscription with id === subscriberUID + for (const channelString of channelToSubscription.keys()) { + const subscriptionItem = channelToSubscription.get(channelString); + const handlerIndex = subscriptionItem.handlers + .findIndex(handler => handler.id === subscriberUID); + + if (handlerIndex !== -1) { + // Remove from handlers + subscriptionItem.handlers.splice(handlerIndex, 1); + + if (subscriptionItem.handlers.length === 0) { + // Unsubscribe from the channel if it was the last handler + console.log('[unsubscribeBars]: Unsubscribe from streaming. Channel:', channelString); + socket.emit('SubRemove', { subs: [channelString] }); + channelToSubscription.delete(channelString); + break; + } + } + } +} + +function sim() { + // Assuming these variables hold the data you extracted earlier + const eventTypeStr = "0"; + const exchange = "Uniswap"; + const fromSymbol = "WETH"; + const toSymbol = "USD"; + const tradeTimeStr = (Date.now()).toString(); + const tradePriceStr = (55+Date.now()%23).toString(); + + // Constructing the original string + const data = [ + eventTypeStr, + exchange, + fromSymbol, + toSymbol, + '', // Placeholder for the fifth element + '', // Placeholder for the sixth element + tradeTimeStr, + '', // Placeholder for the eighth element + tradePriceStr, + ].join('~'); + socket._callbacks['$m'][0](data); +} + +// window.sim = sim; +socket._callbacks['$connect'][0](); +setInterval(sim, 10*1000); +;