OHLC rework

This commit is contained in:
tim
2024-08-06 20:59:37 -04:00
parent d47d90e432
commit f9f154f495
4 changed files with 258 additions and 212 deletions

247
src/charts/ohlc.js Normal file
View File

@@ -0,0 +1,247 @@
import {useStore} from "@/store/store.js";
import {nearestOhlcStart} from "@/charts/chart-misc.js";
function dailyFile(resName) {
function _filename(symbol, timestamp) {
const date = new Date(timestamp*1000)
const year = date.getUTCFullYear()
const month = ('0'+(date.getUTCMonth() + 1)).slice(-2)
const day = ('0'+date.getUTCDate()).slice(-2)
return `${resName}/${year}/${month}/${symbol}-${resName}-${year}${month}${day}.csv`
}
return _filename
}
function monthlyFile(resName) {
function _filename(symbol, timestamp) {
const date = new Date(timestamp*1000)
const year = date.getUTCFullYear()
const month = ('0'+(date.getUTCMonth() + 1)).slice(-2)
return `${resName}/${year}/${symbol}-${resName}-${year}${month}.csv`
}
return _filename
}
function yearlyFile(resName) {
function _filename(symbol, timestamp) {
const date = new Date(timestamp*1000)
const year = date.getUTCFullYear()
return `${resName}/${symbol}-${resName}-${year}.csv`
}
return _filename
}
function singleFile(resName) {
function _filename(symbol, timestamp) {
return `${resName}/${symbol}-${resName}.csv`
}
return _filename
}
function nextDay(timestamp) {
const date = new Date(timestamp*1000)
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + 1) / 1000
}
function nextMonth(timestamp) {
const date = new Date(timestamp*1000)
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()) / 1000
}
function nextYear(timestamp) {
const date = new Date(timestamp*1000)
return Date.UTC(date.getUTCFullYear() + 1, date.getUTCMonth(), date.getUTCDate()) / 1000
}
function never(_timestamp) {
return Number.MAX_SAFE_INTEGER
}
const resolutions = [
{ period: 1, tvRes: '1', filename: dailyFile( '1m'), nextStart: nextDay, },
{ period: 3, tvRes: '3', filename: dailyFile( '3m'), nextStart: nextDay, },
{ period: 5, tvRes: '5', filename: dailyFile( '5m'), nextStart: nextDay, },
{ period: 10, tvRes: '10', filename: dailyFile('10m'), nextStart: nextDay, },
{ period: 15, tvRes: '15', filename: dailyFile('15m'), nextStart: nextDay, },
{ period: 30, tvRes: '30', filename: dailyFile('30m'), nextStart: nextDay, },
{ period: 60, tvRes: '60', filename: monthlyFile( '1H'), nextStart: nextMonth, },
{ period: 120, tvRes: '120', filename: monthlyFile( '2H'), nextStart: nextMonth, },
{ period: 240, tvRes: '240', filename: monthlyFile( '4H'), nextStart: nextMonth, },
{ period: 480, tvRes: '480', filename: monthlyFile( '8H'), nextStart: nextMonth, },
{ period: 720, tvRes: '720', filename: monthlyFile('12H'), nextStart: nextMonth, },
{ period: 1440, tvRes: '1D', filename: yearlyFile( '1D'), nextStart: nextYear, },
{ period: 2880, tvRes: '2D', filename: yearlyFile( '2D'), nextStart: nextYear, },
{ period: 4320, tvRes: '3D', filename: yearlyFile( '3D'), nextStart: nextYear, },
{ period: 10080, tvRes: '1W', filename: singleFile( '1W'), nextStart: never, },
]
const resMap = {}
for (const res of resolutions)
resMap[res.tvRes] = res
const seriesStarts = {}
async function getUrl(url) {
try {
const response = await fetch(url)
console.log('got response', response)
if (response.ok)
return await response.text()
else
console.error(`could not fetch ${url}: status ${response.statusText}`)
}
catch (e) {
console.error(`Could not fetch ${url}`, e)
}
return ''
}
export async function loadOHLC (symbol, contract, from, to, tvRes) {
console.log('loadOHLC', tvRes, new Date(1000*from), new Date(1000*to), symbol, contract);
let chainId
let bars = [];
let inverted = symbol.inverted;
let baseURL
let latest = null // latest time, price
function fill(end, period) {
if (latest===null) return
const [start, price] = latest
for (let now=nearestOhlcStart(start, period*60); now < end; now += period )
bars.push({time:now * 1000, open:price, high:price, low:price, close:price})
}
if (symbol.x?.data) {
baseURL = symbol.x.data.uri
contract = symbol.x.data.symbol
chainId = symbol.x.data.chain
inverted ^= symbol.x.data.inverted
}
else {
baseURL = `//ohlc/`
chainId = useStore().chainId
}
baseURL += `${chainId}/${contract}/`
console.log('baseURL', baseURL)
const res = resMap[tvRes]
const fetches = []
let start = from
if (!(baseURL in seriesStarts)) {
try {
const response = await getUrl(baseURL+'quote.csv')
if (response.length) {
seriesStarts[baseURL] = parseInt(response.split(',')[0])
console.log(`Series ${baseURL} starts at ${new Date(start*1000)}`)
}
else {
console.error(`Bad response while fetching ${baseURL+'quote.csv'}`)
}
}
catch (e) {
console.error(e)
}
}
if (baseURL in seriesStarts)
start = Math.max(start, seriesStarts[baseURL])
for(let now = start; now < to; now = res.nextStart(now)) {
const url = baseURL + res.filename(contract, now);
console.log('fetching', url)
const prom = getUrl(url)
fetches.push(prom);
}
const responses = await Promise.all(fetches)
for (const response of responses) {
if (response.length) {
let lineNum = 0
response.split('\n').forEach((line) => {
lineNum++
console.log(`processing line ${lineNum}`, line)
const row = line.split(',')
let time, open, high, low, close=null
switch (row.length) {
case 1:
if (row[0].length !== 0)
console.log(`Warning: weird nonempty row at OHLC line ${lineNum}: ${line}`)
break
case 2:
time = parseInt(row[0])
if (time < start || time >= to)
break
let price = parseFloat(row[1])
if (inverted)
price = 1/price
open = high = low = close = price
break
case 3:
time = parseInt(row[0])
if (time < start || time >= to)
break
open = parseFloat(row[1])
close = parseFloat(row[2])
if (inverted) {
open = 1/open
close = 1/close
}
high = Math.max(open, close)
low = Math.min(open,close)
break
case 5:
time = parseInt(row[0])
if (time < start || time >= to)
break
open = parseFloat(row[1])
high = parseFloat(row[2])
low = parseFloat(row[3])
close = parseFloat(row[4])
if (inverted) {
open = 1/open
high = 1/high
low = 1/low
close = 1/close
}
break
default:
console.log(`Warning: could not parse line ${lineNum} of OHLC file:\n${line}`)
break
}
if (close!==null) {
console.log(`filling up to ${time}`)
fill(time, res.period)
const bar = {time:time*1000, open, high, low, close};
console.log('pushing bar', bar)
bars.push(bar)
latest = [time, close]
}
})
console.log(`processed ${lineNum} lines`)
}
else { console.log('response was empty') }
}
fill(to, res.period)
// noData should be set only if no bars are in the requested period and earlier.
// In our case, we are guaranteed to have contiguous samples.
// So we only return no bars (bars.length==0) if:
// 1. period is entirely before first data available.
// 2. period is entirely after last data available.
// Returning noData based on bars.length works perfectly assuming that TV never asks for case 2.
// This is probably not a safe assumption. The alternative would be to search
// backward to find beginning of history. How far to search?
let noData = bars.length === 0;
if (noData) console.log("noData == true!");
console.log('bars', bars)
return [bars, {noData}];
}