import path from 'path'
import each from 'lodash-es/each.js'
import filter from 'lodash-es/filter.js'
import first from 'lodash-es/first.js'
import get from 'lodash-es/get.js'
import last from 'lodash-es/last.js'
import map from 'lodash-es/map.js'
import size from 'lodash-es/size.js'
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
import fsReadJson from 'wsemi/src/fsReadJson.mjs'
import fsTreeFolder from 'wsemi/src/fsTreeFolder.mjs'
import getFileNameExt from 'wsemi/src/getFileNameExt.mjs'
import getFileTrueName from 'wsemi/src/getFileTrueName.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import istime from 'wsemi/src/istime.mjs'
import pmSeries from 'wsemi/src/pmSeries.mjs'
/**
* ohlc與交易數據提供器
*
* 讀取兩資料夾內以`${key}.json`儲存之時間序列,各檔案為[{time,...}]陣列,time為秒時間字串(格式'YYYY-MM-DDTHH:mm:ss')且由小至大排序
*
* @class
* @param {String} fdOhlc 輸入儲存K線(ohlc)序列資料夾字串
* @param {String} fdParam 輸入儲存指標參數序列資料夾字串
* @returns {Object} 回傳數據提供器物件,提供getKeysParam列舉fdParam內keys函數,getTimeSeries(key)讀取序列函數(先找fdParam再找fdOhlc,讀取後快取,查無key時reject),getTimeSeriesByTimeRange(key,timeStart,timeEnd)依time過濾(含頭含尾)函數,getTimeSeriesFull讀取兩資料夾全部序列函數(回傳[{type,key,arr}],type為'ohlc'或'param'),getTimeSeriesFullByTimeRange(timeStart,timeEnd)依時間範圍過濾全部序列函數(timeStart不可早於資料起始時間,由各ohlc序列首筆判斷;timeEnd不可晚於資料結束時間,由全部序列末筆判斷;否則reject),buildGetTimeSeriesByTimeRange(timeStart,timeEnd)建立序列查詢函數fun(key),buildGetTimeValueByTimeRange(timeStart,timeEnd)建立單點查詢函數fun(key,time),後兩者查無資料時reject
* @example
*
* import fs from 'fs'
* import path from 'path'
* import WDataTdprovide from './src/WDataTdprovide.mjs'
*
* //建立示範資料夾, fdOhlc 放 K 線序列, fdParam 放指標參數序列, 各以 `${key}.json` 儲存
* let fdOhlc = './test/tmp/data-ohlc'
* let fdParam = './test/tmp/data-param'
* fs.mkdirSync(fdOhlc, { recursive: true })
* fs.mkdirSync(fdParam, { recursive: true })
*
* //ohlc 'btc', 8 根 4hr K 線
* let arrOhlc = [
* { time: '2020-01-01T00:00:00', Open: 7160.11, Close: 7173.32 },
* { time: '2020-01-01T04:00:00', Open: 7173.32, Close: 7195.23 },
* { time: '2020-01-01T08:00:00', Open: 7195.23, Close: 7225.01 },
* { time: '2020-01-01T12:00:00', Open: 7225.01, Close: 7209.83 },
* { time: '2020-01-01T16:00:00', Open: 7209.83, Close: 7188.77 },
* { time: '2020-01-01T20:00:00', Open: 7188.77, Close: 7200.85 },
* { time: '2020-01-02T00:00:00', Open: 7200.85, Close: 7156.44 },
* { time: '2020-01-02T04:00:00', Open: 7156.44, Close: 7130.02 },
* ]
* fs.writeFileSync(path.resolve(fdOhlc, 'btc.json'), JSON.stringify(arrOhlc), 'utf8')
*
* //param 'btc_ma', 1day 均線, 起始時間晚於 ohlc(均線需累積足量 K 線)
* let arrParam = [
* { time: '2020-01-01T20:00:00', param: 7198.835 },
* { time: '2020-01-02T00:00:00', param: 7196.021666666667 },
* { time: '2020-01-02T04:00:00', param: 7185.153333333333 },
* ]
* fs.writeFileSync(path.resolve(fdParam, 'btc_ma.json'), JSON.stringify(arrParam), 'utf8')
*
* //wdt, 建立資料提供器
* let wdt = WDataTdprovide(fdOhlc, fdParam)
*
* //getKeysParam, 列舉 fdParam 內全部 keys
* let keysParam = wdt.getKeysParam()
* console.log('getKeysParam:', keysParam)
* // => getKeysParam: [ 'btc_ma' ]
*
* //getTimeSeries, 依 key 讀取完整序列(先找 fdParam 再找 fdOhlc, 讀取後快取)
* let arrBtc = await wdt.getTimeSeries('btc')
* console.log('getTimeSeries:', arrBtc.length, '筆, 首筆', arrBtc[0])
* // => getTimeSeries: 8 筆, 首筆 { time: '2020-01-01T00:00:00', Open: 7160.11, Close: 7173.32 }
*
* //getTimeSeriesByTimeRange, 依 time 過濾(含頭含尾)
* let arrBtcRange = await wdt.getTimeSeriesByTimeRange('btc', '2020-01-01T04:00:00', '2020-01-01T12:00:00')
* console.log('getTimeSeriesByTimeRange:', arrBtcRange.map((v) => v.time))
* // => getTimeSeriesByTimeRange: [ '2020-01-01T04:00:00', '2020-01-01T08:00:00', '2020-01-01T12:00:00' ]
*
* //getTimeSeriesFull, 回傳兩資料夾全部序列
* let vs = await wdt.getTimeSeriesFull()
* console.log('getTimeSeriesFull:', vs.map((v) => `${v.type}:${v.key}(${v.arr.length}筆)`))
* // => getTimeSeriesFull: [ 'ohlc:btc(8筆)', 'param:btc_ma(3筆)' ]
*
* //getTimeSeriesFullByTimeRange, 依時間範圍過濾全部序列
* // timeStart 不可早於資料起始時間(由各 ohlc 序列首筆判斷), timeEnd 不可晚於資料結束時間(由全部序列末筆判斷), 否則 throw
* let vst = await wdt.getTimeSeriesFullByTimeRange('2020-01-01T20:00:00', '2020-01-02T04:00:00')
* console.log('getTimeSeriesFullByTimeRange:', vst.map((v) => `${v.key}(${v.arr.length}筆)`))
* // => getTimeSeriesFullByTimeRange: [ 'btc(3筆)', 'btc_ma(3筆)' ]
*
* //buildGetTimeSeriesByTimeRange, 建立指定時間範圍之序列查詢函數
* let funSeries = await wdt.buildGetTimeSeriesByTimeRange('2020-01-01T20:00:00', '2020-01-02T04:00:00')
* let arrMa = await funSeries('btc_ma')
* console.log('funSeries:', arrMa)
* // => funSeries: [
* // { time: '2020-01-01T20:00:00', param: 7198.835 },
* // { time: '2020-01-02T00:00:00', param: 7196.021666666667 },
* // { time: '2020-01-02T04:00:00', param: 7185.153333333333 }
* // ]
*
* //buildGetTimeValueByTimeRange, 建立指定時間範圍之單點查詢函數
* let funValue = await wdt.buildGetTimeValueByTimeRange('2020-01-01T20:00:00', '2020-01-02T04:00:00')
* let d = await funValue('btc', '2020-01-02T00:00:00')
* console.log('funValue:', d)
* // => funValue: { time: '2020-01-02T00:00:00', Open: 7200.85, Close: 7156.44 }
*
*/
let WDataTdprovide = (fdOhlc, fdParam) => {
let getKeysParam = () => {
//vfps
let vfps = fsTreeFolder(fdParam)
// console.log('vfps', vfps)
//keysParam
let keysParam = map(vfps, (v) => {
let key = getFileTrueName(v.name)
return key
})
// console.log('keysParam', keysParam)
return keysParam
}
let _getTimeSeries = async(key) => {
//fpParam
let fpParam = path.resolve(fdParam, `${key}.json`)
// console.log('fpParam', fpParam)
//fpOhlc
let fpOhlc = path.resolve(fdOhlc, `${key}.json`)
// console.log('fpOhlc', fpOhlc)
//fp
let fp = ''
if (fsIsFile(fpParam)) {
fp = fpParam
}
else if (fsIsFile(fpOhlc)) {
fp = fpOhlc
}
else {
console.log(`fpParam[${fpParam}] or fpOhlc[${fpOhlc}] do not exist`)
throw new Error(`invalid key[${key}]`)
}
//arr
let arr = get(fsReadJson(fp), 'success', null)
// console.log('arr', arr)
//check
if (!isearr(arr)) {
throw new Error(`fp[${fp}] no data`)
}
return arr
}
let kp = {}
let getTimeSeries = async(key) => {
//check
if (haskey(kp, key)) {
// console.log(`use cache key[${key}]`)
return kp[key]
}
// console.log(`use _getTimeSeries key[${key}]`)
//arr
let arr = await _getTimeSeries(key)
//save
kp[key] = arr
return arr
}
let getTimeSeriesByTimeRange = async(key, timeStart, timeEnd) => {
let arr = await getTimeSeries(key)
arr = filter(arr, (m) => {
return m['time'] >= timeStart && m['time'] <= timeEnd
})
return arr
}
let vsFull = []
let getTimeSeriesFull = async() => {
if (size(vsFull) > 0) {
return vsFull
}
let getVfpsJson = (fd) => {
let vfps = fsTreeFolder(fd)
vfps = filter(vfps, (v) => {
return getFileNameExt(v.name) === 'json'
})
return vfps
}
let vs = []
let vfpsOhlc = getVfpsJson(fdOhlc)
await pmSeries(vfpsOhlc, async (v) => {
let key = getFileTrueName(v.name)
let arr = await getTimeSeries(key)
vs.push({
type: 'ohlc',
key,
arr,
})
})
let vfpsParam = getVfpsJson(fdParam)
await pmSeries(vfpsParam, async (v) => {
let key = getFileTrueName(v.name)
let arr = await getTimeSeries(key)
vs.push({
type: 'param',
key,
arr,
})
})
vsFull = vs
return vs
}
let kpVst = {}
let getTimeSeriesFullByTimeRange = async(timeStart, timeEnd) => {
if (!istime(timeStart)) {
throw new Error(`invalid timeStart`)
}
if (!istime(timeEnd)) {
throw new Error(`invalid timeEnd`)
}
let t = `${timeStart}:${timeEnd}`
if (haskey(kpVst, t)) {
return kpVst[t]
}
let vs = await getTimeSeriesFull()
let ts = '1000-01-01T00:00:00'
let te = '9999-01-01T00:00:00'
each(vs, (v) => {
// console.log('v.key', v.key)
let ds = first(v.arr)
let de = last(v.arr)
let _ts = get(ds, 'time', '')
// console.log('_ts', _ts)
let _te = get(de, 'time', '')
// console.log('_te', _te)
if (v.type === 'ohlc' && ts < _ts) { //僅ohlc數據才能判斷起始時間, param數據時間可能會有延後(例如ma,ema等)
ts = _ts
}
if (te > _te) {
te = _te
}
})
// console.log('ts', ts)
// console.log('te', te)
if (timeStart < ts) {
throw new Error(`timeStart[${timeStart}] < timeStartData[${ts}]`)
}
if (timeEnd > te) {
throw new Error(`timeEnd[${timeEnd}] > timeEndData[${te}]`)
}
let vst = []
each(vs, (v) => {
// console.log('v.key', v.key)
let _arr = filter(v.arr, (m) => {
return m['time'] >= timeStart && m['time'] <= timeEnd
})
vst.push({
key: v.key,
arr: _arr,
})
})
kpVst[t] = vst
return vst
}
let buildGetTimeSeriesByTimeRange = async(timeStart, timeEnd) => {
let vs = await getTimeSeriesFullByTimeRange(timeStart, timeEnd)
let kp = {}
each(vs, (v) => {
kp[v.key] = v.arr
})
let fun = async(key) => {
let arr = get(kp, key, [])
if (!isearr(arr)) {
throw new Error(`can not get series by key[${key}] bewteen timeStart[${timeStart}] and timeEnd[${timeEnd}]`)
}
return arr
}
return fun
}
let buildGetTimeValueByTimeRange = async(timeStart, timeEnd) => {
let vs = await getTimeSeriesFullByTimeRange(timeStart, timeEnd)
let kp = {}
each(vs, (v) => {
each(v.arr, (d) => {
let keyTime = `${v.key}_${d.time}`
// console.log('keyTime', keyTime, d)
kp[keyTime] = d
})
})
// console.log('kp', kp)
let fun = async(key, time) => {
let keyTime = `${key}_${time}`
// console.log('keyTime', keyTime, kp[keyTime])
let d = get(kp, keyTime, {})
if (!iseobj(d)) {
throw new Error(`can not get value by key[${key}] and time[${time}] bewteen timeStart[${timeStart}] and timeEnd[${timeEnd}]`)
}
return d
}
return fun
}
let r = {
getKeysParam,
getTimeSeries,
getTimeSeriesByTimeRange,
getTimeSeriesFull,
getTimeSeriesFullByTimeRange,
buildGetTimeSeriesByTimeRange,
buildGetTimeValueByTimeRange,
}
return r
}
export default WDataTdprovide