trading-vue3-js
Version:
Customizable charting lib for traders. Based on https://github.com/C451/trading-vue-js by C451.
499 lines (407 loc) • 14.4 kB
JavaScript
// DataCube "private" methods
import Utils from '../stuff/utils.js'
import DCEvents from './dc_events.js'
import Dataset from './dataset.js'
export default class DCCore extends DCEvents {
// Set TV instance (once). Called by TradingVue itself
init_tvjs($root) {
if (!this.tv) {
this.tv = $root
this.init_data()
this.update_ids()
// Listen to all setting changes
// TODO: works only with merge()
this.tv.$watch(() => this.get_by_query('.settings'),
(n, p) => this.on_settings(n, p))
// Listen to all indices changes
this.tv.$watch(() => this.get('.')
.map(x => x.settings.$uuid),
(n, p) => this.on_ids_changed(n, p))
// Watch for all 'datasets' changes
this.tv.$watch(() => this.get('datasets'),
Dataset.watcher.bind(this))
}
}
// Init Data Structure v1.1
init_data($root) {
if (!('chart' in this.data)) {
this.tv.$set(this.data, 'chart', {
type: 'Candles',
data: this.data.ohlcv || []
})
}
if (!('onchart' in this.data)) {
this.tv.$set(this.data, 'onchart', [])
}
if (!('offchart' in this.data)) {
this.tv.$set(this.data, 'offchart', [])
}
if (!this.data.chart.settings) {
this.tv.$set(this.data.chart,'settings', {})
}
// Remove ohlcv cuz we have Data v1.1^
delete this.data.ohlcv
if (!('datasets' in this.data)) {
this.tv.$set(this.data, 'datasets', [])
}
// Init dataset proxies
for (var ds of this.data.datasets) {
if (!this.dss) this.dss = {}
this.dss[ds.id] = new Dataset(this, ds)
}
}
// Range change callback (called by TradingVue)
// TODO: improve (reliablity + chunk with limited size)
async range_changed(range, tf, check=false) {
if (!this.loader) return
if (!this.loading) {
let first = this.data.chart.data[0][0]
if (range[0] < first) {
this.loading = true
await Utils.pause(250) // Load bigger chunks
range = range.slice() // copy
range[0] = Math.floor(range[0])
range[1] = Math.floor(first)
let prom = this.loader(range, tf, d => {
// Callback way
this.chunk_loaded(d)
})
if (prom && prom.then) {
// Promise way
this.chunk_loaded(await prom)
}
}
}
if (!check) this.last_chunk = [range, tf]
}
// A new chunk of data is loaded
// TODO: bulletproof fetch
chunk_loaded(data) {
// Updates only candlestick data, or
if (Array.isArray(data)) {
this.merge('chart.data', data)
} else {
// Bunch of overlays, including chart.data
for (var k in data) {
this.merge(k, data[k])
}
}
this.loading = false
if (this.last_chunk) {
this.range_changed(...this.last_chunk, true)
this.last_chunk = null
}
}
// Update ids for all overlays
update_ids() {
this.data.chart.id = `chart.${this.data.chart.type}`
var count = {}
// grid_id,layer_id => DC id mapping
this.gldc = {}, this.dcgl = {}
for (var ov of this.data.onchart) {
if (count[ov.type] === undefined) {
count[ov.type] = 0
}
let i = count[ov.type]++
ov.id = `onchart.${ov.type}${i}`
if (!ov.name) ov.name = ov.type + ` ${i}`
if (!ov.settings) this.tv.$set(ov, 'settings', {})
// grid_id,layer_id => DC id mapping
this.gldc[`g0_${ov.type}_${i}`] = ov.id
this.dcgl[ov.id] = `g0_${ov.type}_${i}`
}
count = {}
let grids = [{}]
let gid = 0
for (var ov of this.data.offchart) {
if (count[ov.type] === undefined) {
count[ov.type] = 0
}
let i = count[ov.type]++
ov.id = `offchart.${ov.type}${i}`
if (!ov.name) ov.name = ov.type + ` ${i}`
if (!ov.settings) this.tv.$set(ov, 'settings', {})
// grid_id,layer_id => DC id mapping
gid++
let rgid = (ov.grid || {}).id || gid // real grid_id
// When we merge grid, skip ++
if ((ov.grid || {}).id) gid--
if (!grids[rgid]) grids[rgid] = {}
if (grids[rgid][ov.type] === undefined) {
grids[rgid][ov.type] = 0
}
let ri = grids[rgid][ov.type]++
this.gldc[`g${rgid}_${ov.type}_${ri}`] = ov.id
this.dcgl[ov.id] = `g${rgid}_${ov.type}_${ri}`
}
}
// TODO: chart refine (from the exchange chart)
update_candle(data) {
let ohlcv = this.data.chart.data
let last = ohlcv[ohlcv.length - 1]
let candle = data['candle']
let tf = this.tv.$refs.chart.interval_ms
let t_next = last[0] + tf
let now = data.t || Utils.now()
let t = now >= t_next ? (now - now % tf) : last[0]
// Update the entire candle
if (candle.length >= 6) {
t = candle[0]
} else {
candle = [t, ...candle]
}
this.agg.push('ohlcv', candle)
this.update_overlays(data, t, tf)
return t >= t_next
}
update_tick(data) {
let ohlcv = this.data.chart.data
let last = ohlcv[ohlcv.length - 1]
let tick = data['price']
let volume = data['volume'] || 0
let tf = this.tv.$refs.chart.interval_ms
if (!tf) {
return console.warn('Define the main timeframe')
}
let now = data.t || Utils.now()
if (!last) last = [now - now % tf]
let t_next = last[0] + tf
let t = now >= t_next ? (now - now % tf) : last[0]
if ((t >= t_next || !ohlcv.length) && tick !== undefined) {
// And new zero-height candle
let nc = [t, tick, tick, tick, tick, volume]
this.agg.push('ohlcv', nc, tf)
ohlcv.push(nc)
this.scroll_to(t)
} else if (tick !== undefined) {
// Update an existing one
// TODO: make a separate class Sampler
last[2] = Math.max(tick, last[2])
last[3] = Math.min(tick, last[3])
last[4] = tick
last[5] += volume
this.agg.push('ohlcv', last, tf)
}
this.update_overlays(data, t, tf)
return t >= t_next
}
// Updates all overlays with given values.
update_overlays(data, t, tf) {
for (var k in data) {
if (k === 'price' || k === 'volume' ||
k === 'candle' || k === 't') {
continue
}
if (k.includes('datasets.')) {
this.agg.push(k, data[k], tf)
continue
}
if (!Array.isArray(data[k])) {
var val = [data[k]]
} else {
val = data[k]
}
if (!k.includes('.data')) k += '.data'
this.agg.push(k, [t, ...val], tf)
}
}
// Returns array of objects matching query.
// Object contains { parent, index, value }
// TODO: query caching
get_by_query(query, chuck) {
let tuple = query.split('.')
switch (tuple[0]) {
case 'chart':
var result = this.chart_as_piv(tuple)
break
case 'onchart':
case 'offchart':
result = this.query_search(query, tuple)
break
case 'datasets':
result = this.query_search(query, tuple)
for (var r of result) {
if (r.i === 'data') {
r.v = this.dss[r.p.id].data()
}
}
break
default:
/* Should get('.') return also the chart? */
/*let ch = this.chart_as_query([
'chart',
tuple[1]
])*/
let on = this.query_search(query, [
'onchart',
tuple[0],
tuple[1]
])
let off = this.query_search(query, [
'offchart',
tuple[0],
tuple[1]
])
result = [/*ch[0],*/ ...on, ...off]
break
}
return result.filter(
x => !(x.v || {}).locked || chuck)
}
chart_as_piv(tuple) {
let field = tuple[1]
if (field) return [{
p: this.data.chart,
i: field,
v: this.data.chart[field]
}]
else return [{
p: this.data,
i: 'chart',
v: this.data.chart
}]
}
query_search(query, tuple) {
let side = tuple[0]
let path = tuple[1] || ''
let field = tuple[2]
let arr = this.data[side].filter(x => (
x.id === query ||
(x.id && x.id.includes(path)) ||
x.name === query ||
(x.name && x.name.includes(path)) ||
query.includes((x.settings || {}).$uuid)
))
if (field) {
return arr.map(x => ({
p: x,
i: field,
v: x[field]
}))
}
return arr.map((x, i) => ({
p: this.data[side],
i: this.data[side].indexOf(x),
v: x
}))
}
merge_objects(obj, data, new_obj = {}) {
// The only way to get Vue to update all stuff
// reactively is to create a brand new object.
// TODO: Is there a simpler approach?
Object.assign(new_obj, obj.v)
Object.assign(new_obj, data)
this.tv.$set(obj.p, obj.i, new_obj)
}
// Merge overlapping time series
merge_ts(obj, data) {
// Assume that both arrays are pre-sorted
if (!data.length) return obj.v
let r1 = [obj.v[0][0], obj.v[obj.v.length - 1][0]]
let r2 = [data[0][0], data[data.length - 1][0]]
// Overlap
let o = [Math.max(r1[0],r2[0]), Math.min(r1[1],r2[1])]
if (o[1] >= o[0]) {
let { od, d1, d2 } = this.ts_overlap(obj.v, data, o)
obj.v.splice(...d1)
data.splice(...d2)
// Dst === Overlap === Src
if (!obj.v.length && !data.length) {
this.tv.$set(obj.p, obj.i, od)
return obj.v
}
// If src is totally contained in dst
if (!data.length) { data = obj.v.splice(d1[0]) }
// If dst is totally contained in src
if (!obj.v.length) { obj.v = data.splice(d2[0]) }
this.tv.$set(
obj.p, obj.i, this.combine(obj.v, od, data)
)
} else {
this.tv.$set(
obj.p, obj.i, this.combine(obj.v, [], data)
)
}
return obj.v
}
// TODO: review performance, move to worker
ts_overlap(arr1, arr2, range) {
const t1 = range[0]
const t2 = range[1]
let ts = {} // timestamp map
let a1 = arr1.filter(x => x[0] >= t1 && x[0] <= t2)
let a2 = arr2.filter(x => x[0] >= t1 && x[0] <= t2)
// Indices of segments
let id11 = arr1.indexOf(a1[0])
let id12 = arr1.indexOf(a1[a1.length - 1])
let id21 = arr2.indexOf(a2[0])
let id22 = arr2.indexOf(a2[a2.length - 1])
for (var i = 0; i < a1.length; i++) {
ts[a1[i][0]] = a1[i]
}
for (var i = 0; i < a2.length; i++) {
ts[a2[i][0]] = a2[i]
}
let ts_sorted = Object.keys(ts).sort()
return {
od: ts_sorted.map(x => ts[x]),
d1: [id11, id12 - id11 + 1],
d2: [id21, id22 - id21 + 1]
}
}
// Combine parts together:
// (destination, overlap, source)
combine(dst, o, src) {
function last(arr) { return arr[arr.length - 1][0] }
if (!dst.length) { dst = o; o = [] }
if (!src.length) { src = o; o = [] }
// The overlap right in the middle
if (src[0][0] >= dst[0][0] && last(src) <= last(dst)) {
return Object.assign(dst, o)
// The overlap is on the right
} else if (last(src) > last(dst)) {
// Psh(...) is faster but can overflow the stack
if (o.length < 100000 && src.length < 100000) {
dst.push(...o, ...src)
return dst
} else {
return dst.concat(o, src)
}
// The overlap is on the left
} else if (src[0][0] < dst[0][0]) {
// Push(...) is faster but can overflow the stack
if (o.length < 100000 && src.length < 100000) {
src.push(...o, ...dst)
return src
} else {
return src.concat(o, dst)
}
} else { return [] }
}
// Simple data-point merge (faster)
fast_merge(data, point, main = true) {
if (!data) return
let last_t = (data[data.length - 1] || [])[0]
let upd_t = point[0]
if (!data.length || upd_t > last_t) {
data.push(point)
if (main && this.sett.auto_scroll) {
this.scroll_to(upd_t)
}
} else if (upd_t === last_t) {
if (main) {
this.tv.$set(data, data.length - 1, point)
} else {
data[data.length - 1] = point
}
}
}
scroll_to(t) {
if (this.tv.$refs.chart.cursor.locked) return
let last = this.tv.$refs.chart.last_candle
if (!last) return
let tl = last[0]
let d = this.tv.getRange()[1] - tl
if (d > 0) this.tv.goto(t + d)
}
}