buildKLinesTpSl.mjs

import path from 'path'
import fs from 'fs'
import isestr from 'wsemi/src/isestr.mjs'
import istime from 'wsemi/src/istime.mjs'
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
import WDataTdprovide from 'w-data-tdprovide/src/WDataTdprovide.mjs'
import genKLinesTpSl from './genKLinesTpSl.mjs'


/**
 * 建置各K線與各止盈止損組合之先觸結果快取檔
 *
 * 以w-data-tdprovide讀取fdOhlc與fdParam資料夾內keyOhlc序列,取ts至te範圍K線後,逐一組合modes與tps呼叫genKLinesTpSl
 * 各組合以`${mode}_${tpsl}`為key存入各根K線,win為1(止盈先觸)、0(止損先觸)或-1(至區間末仍未結)
 * timeEnd於建置時由hold位移換算為絕對時間,故下游查表即得而不需位置索引,未結者為null
 * 僅存win、timeEnd與各根close,盈虧由下游loadKLinesTpSl之ordersFromBars以close與手續費重算
 * fdOhlc或fdParam非資料夾、fpJson或opt.keyOhlc非有效字串、opt.ts或opt.te非時間字串時throw
 *
 * Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
 * @function
 * @param {String} fdOhlc 輸入儲存K線(ohlc)序列資料夾字串,各序列以`${key}.json`儲存
 * @param {String} fdParam 輸入儲存指標參數序列資料夾字串
 * @param {String} fpJson 輸入輸出快取json檔案路徑字串,資料夾不存在時自動建立
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {String} opt.keyOhlc 輸入K線序列key字串
 * @param {String} opt.ts 輸入起始秒時間字串
 * @param {String} opt.te 輸入結束秒時間字串
 * @param {Array} [opt.tps=[4,5,6,7,8,9,10]] 輸入止盈止損百分比數值陣列,如5代表止盈與止損皆5%,預設[4,5,6,7,8,9,10]
 * @param {Array} [opt.modes=['long','short']] 輸入交易方向字串陣列,預設['long','short']
 * @returns {Promise} 回傳Promise,resolve為{meta,bars}物件,meta內含keyOhlc、ts、te、tps、modes與nBars(K線根數),bars以進場時間為key,各值內含close與各`${mode}_${tpsl}`之{win,timeEnd},並同步寫出至fpJson
 * @example
 *
 * import fs from 'fs'
 *
 * let arrOhlc = [
 *     { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
 *     { time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
 *     { time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
 *     { time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
 *     { time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
 *     { time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
 * ]
 *
 * //建立數據資料夾, fdOhlc放K線序列, fdParam放指標參數序列
 * fs.mkdirSync('./data-ohlc', { recursive: true })
 * fs.mkdirSync('./data-param', { recursive: true })
 * fs.writeFileSync('./data-ohlc/btc.json', JSON.stringify(arrOhlc), 'utf8')
 *
 * let r = await buildKLinesTpSl('./data-ohlc', './data-param', './cache/kltpsl.json', {
 *     keyOhlc: 'btc',
 *     ts: '2020-01-01T00:00:00',
 *     te: '2020-01-01T20:00:00',
 *     tps: [5],
 *     modes: ['long'],
 * })
 * console.log(r.meta)
 * // => {
 * //   keyOhlc: 'btc',
 * //   ts: '2020-01-01T00:00:00',
 * //   te: '2020-01-01T20:00:00',
 * //   tps: [ 5 ],
 * //   modes: [ 'long' ],
 * //   nBars: 6
 * // }
 * console.log(r.bars['2020-01-01T00:00:00'])
 * // => { close: 100, long_5: { win: 1, timeEnd: '2020-01-01T04:00:00' } }
 * console.log(r.bars['2020-01-01T12:00:00'])
 * // => { close: 95, long_5: { win: -1, timeEnd: null } }
 *
 */
let buildKLinesTpSl = async (fdOhlc, fdParam, fpJson, opt = {}) => {
    //一次性全建(訓練區間固定, 不需增量); 若日後要增量延長: 已結entry不動、未結entry用新OHLC重掃、新棒加key即可

    //check
    if (!fsIsFolder(fdOhlc)) {
        throw new Error(`fdOhlc[${fdOhlc}] is not a folder`)
    }
    if (!fsIsFolder(fdParam)) {
        throw new Error(`fdParam[${fdParam}] is not a folder`)
    }
    if (!isestr(fpJson)) {
        throw new Error(`invalid fpJson`)
    }

    let provideData = WDataTdprovide(fdOhlc, fdParam) //數據資料夾由呼叫端顯式指定

    let {
        keyOhlc,
        ts,
        te,
        tps = [4, 5, 6, 7, 8, 9, 10],
        modes = ['long', 'short'],
    } = opt

    //check
    if (!isestr(keyOhlc)) {
        throw new Error(`invalid opt.keyOhlc`)
    }
    if (!istime(ts)) {
        throw new Error(`invalid opt.ts`)
    }
    if (!istime(te)) {
        throw new Error(`invalid opt.te`)
    }

    //OHLC(訓練區間)
    let arrOhlc = await provideData.getTimeSeriesByTimeRange(keyOhlc, ts, te)
    let n = arrOhlc.length
    let time = arrOhlc.map((r) => r.time)

    //每根 close(各 mode_tpsl 共用)
    let bars = {}
    for (let i = 0; i < n; i++) {
        bars[time[i]] = { close: arrOhlc[i].Close }
    }

    //各 (mode, tpsl) 跑 first-touch → 填 { win, timeEnd }
    for (let mode of modes) {
        for (let tpsl of tps) {
            let { win, hold } = genKLinesTpSl(arrOhlc, { tp: tpsl / 100, sl: tpsl / 100, mode })
            let k = `${mode}_${tpsl}`
            for (let i = 0; i < n; i++) {
                let wn = win[i]
                let timeEnd = (wn >= 0 && Number.isFinite(hold[i])) ? time[i + hold[i]] : null
                bars[time[i]][k] = { win: wn, timeEnd }
            }
        }
    }

    //out
    let out = { meta: { keyOhlc, ts, te, tps, modes, nBars: n }, bars }

    //寫檔
    let jp = path.resolve(fpJson)
    let fd = path.dirname(jp)
    if (!fsIsFolder(fd)) {
        fsCreateFolder(fd)
    }
    fs.writeFileSync(jp, JSON.stringify(out))

    return out
}


export default buildKLinesTpSl