import map from 'lodash-es/map.js'
import get from 'lodash-es/get.js'
import keys from 'lodash-es/keys.js'
import size from 'lodash-es/size.js'
import every from 'lodash-es/every.js'
import each from 'lodash-es/each.js'
import sum from 'lodash-es/sum.js'
import range from 'lodash-es/range.js'
import sumBy from 'lodash-es/sumBy.js'
import mean from 'lodash-es/mean.js'
import uniq from 'lodash-es/uniq.js'
import isfun from 'wsemi/src/isfun.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import haskey from 'wsemi/src/haskey.mjs'
/**
* 校正calc函式之optMethod: 搜尋參數,使來源側(src)各keyOut之z-score分布形狀對齊參考側(ref)對應keyOut
*
* 本專案用例: src=溢價指數(index) OHLC,ref=價格(price) OHLC,校正index系變換偏移量
* 搜尋為逐維座標下降: 每維先粗掃(現值+0+正負mags量級),再以粗掃最佳為中心做倍率細化與微調
* 防churn: 需(score0-scoreBest)/score0 > threshold,且(score0-scoreBest) > thresholdAbs,兩者皆達門檻才採納搜尋結果,否則optMethod維持optMethodInit
*
* Unit Test: {@link https://github.com/yuda-lyu/w-data-trade/blob/master/test/unit-dataCalibrate.test.mjs Github}
* @function
* @param {Function} calcFn 輸入指標計算函式,簽章為(arr,keyIn,optMethod)=>rrs(各期{period,vs:[{time,...keyOuts}]}之陣列,可回傳Promise)
* @param {Array} arrSrc 輸入來源側(src)之K線陣列
* @param {Array} arrRef 輸入參考側(ref)之K線陣列
* @param {Array} pairs 輸入兩側keyOut對應表,元素為{keyOutSrc,keyOutRef}物件或字串(代表兩側同名之縮寫)
* @param {Object} optMethodInit 輸入現行optMethod(搜尋起點與防churn比較基準),params中不存在之鍵搜尋時以0起算
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {Array} [opt.params=keys(optMethodInit)] 輸入要校正之參數名陣列,預設為optMethodInit全部鍵
* @param {String} [opt.keyIn='none'] 輸入傳入calcFn之keyIn,預設'none'
* @param {Object} [opt.optMethodRef={}] 輸入參考側呼叫calcFn所用之optMethod,預設{}
* @param {Array} [opt.mags=[1e-4,3e-4,1e-3,3e-3,0.01,0.03,0.1,0.3]] 輸入粗掃量級階梯(正值,內部自動含正負與0)
* @param {Number} [opt.threshold=0.15] 輸入採納門檻(相對改善比例)
* @param {Number} [opt.thresholdAbs=0.01] 輸入採納門檻(絕對改善量)
* @param {Number} [opt.nSweep] 輸入逐維掃描輪數,預設單參數為1輪,多參數為2輪
* @param {Function} [opt.funLog] 輸入搜尋進度回呼函式(msg),可選
* @returns {Promise} 回傳Promise,resolve為{adopt,optMethod,optMethodBest,score0,scoreBest,nEval,detail},adopt為是否採納搜尋結果之布林值,optMethod為依防churn判定之最終值(adopt=false時即optMethodInit),optMethodBest為搜尋所得最佳值(不論是否採納),score0為起始參數之目標函數值,scoreBest為搜尋所得最佳目標函數值,nEval為calcFn對arrSrc之實際評估次數,detail為各pair於最終optMethod下之形狀對照陣列,各元素為{keyOutSrc,keyOutRef,dist,pNearZeroSrc,pNearZeroRef}
* @example
*
* let mkArr = (n) => {
* let arr = []
* for (let i = 0; i < n; i++) {
* arr.push({ time: `t${i}`, u: Math.sin(i * 1.7) })
* }
* return arr
* }
*
* let calcFn = async (arr, keyIn, optMethod) => {
* let a = optMethod.a !== undefined ? optMethod.a : 0
* let vs = arr.map((v) => {
* let x = v.u + a * v.u * v.u
* return { time: v.time, X: x }
* })
* return [{ period: 'p1', len: 1, vs }]
* }
*
* let arr = mkArr(400)
*
* dataCalibrate(calcFn, arr, arr, ['X'], { a: 0 }, { optMethodRef: { a: 0.1 } })
* .then((r) => {
* console.log(r)
* // => {
* // adopt: true,
* // optMethod: { a: 0.1 },
* // optMethodBest: { a: 0.1 },
* // score0: 0.0478199962161957,
* // scoreBest: 0,
* // nEval: 26,
* // detail: [
* // { keyOutSrc: 'X', keyOutRef: 'X', dist: 0, pNearZeroSrc: 0.0425, pNearZeroRef: 0.0425 }
* // ]
* // }
* })
*
*/
let dataCalibrate = async (calcFn, arrSrc, arrRef, pairs, optMethodInit, opt = {}) => {
//check
if (!isfun(calcFn)) {
throw new Error(`invalid calcFn`)
}
if (!isearr(arrSrc)) {
throw new Error(`invalid arrSrc`)
}
if (!isearr(arrRef)) {
throw new Error(`invalid arrRef`)
}
if (!isearr(pairs)) {
throw new Error(`invalid pairs`)
}
if (!iseobj(optMethodInit)) {
throw new Error(`invalid optMethodInit`)
}
//ps: 正規化 pairs(字串縮寫展開)並逐元素檢核
let ps = map(pairs, (p) => {
if (isestr(p)) {
return { keyOutSrc: p, keyOutRef: p }
}
let keyOutSrc = get(p, 'keyOutSrc')
let keyOutRef = get(p, 'keyOutRef')
if (!isestr(keyOutSrc)) {
throw new Error(`invalid pair.keyOutSrc[${keyOutSrc}]`)
}
if (!isestr(keyOutRef)) {
throw new Error(`invalid pair.keyOutRef[${keyOutRef}]`)
}
return { keyOutSrc, keyOutRef }
})
//opt
let params = get(opt, 'params', keys(optMethodInit))
let keyIn = get(opt, 'keyIn', 'none')
let optMethodRef = get(opt, 'optMethodRef', {})
let mags = get(opt, 'mags', [1e-4, 3e-4, 1e-3, 3e-3, 0.01, 0.03, 0.1, 0.3])
let threshold = get(opt, 'threshold', 0.15)
let thresholdAbs = get(opt, 'thresholdAbs', 0.01)
let nSweep = get(opt, 'nSweep', size(params) > 1 ? 2 : 1)
let funLog = get(opt, 'funLog', null)
let log = (msg) => {
if (isfun(funLog)) {
funLog(msg)
}
}
//check
if (!isearr(params) || !every(params, isestr)) {
throw new Error(`invalid opt.params[${params}]`)
}
if (!isestr(keyIn)) {
throw new Error(`invalid opt.keyIn[${keyIn}]`)
}
//pool: 跑 calc 後彙集指定 keyOut 之全期有限值
let pool = (rrs, keyOut) => {
let vs = []
each(rrs, (v) => {
each(v.vs, (m) => {
let x = m[keyOut]
if (Number.isFinite(x)) {
vs.push(x)
}
})
})
return vs
}
//zshape: 自解 avg/std 轉 z-score 後取形狀特徵(十分位 + |z|<0.1 佔比)
let zshape = (vs) => {
let n = size(vs)
if (n < 100) {
return null
}
let avg = sum(vs) / n
let std = Math.sqrt(sum(map(vs, (x) => (x - avg) ** 2)) / n)
if (!(std > 0)) {
return null
}
let zs = map(vs, (x) => (x - avg) / std)
zs.sort((a, b) => a - b)
let qs = map(range(1, 10), (i) => zs[Math.floor(n * i / 10)]) //十分位
let pNearZero = sumBy(zs, (z) => Math.abs(z) < 0.1 ? 1 : 0) / n
return { qs, pNearZero }
}
//dist: 兩形狀距離 = 十分位平均絕對差 + |z|<0.1 佔比差
let dist = (a, b) => {
if (!a || !b) {
return Infinity
}
let dq = mean(map(a.qs, (q, i) => Math.abs(q - b.qs[i])))
return dq + Math.abs(a.pNearZero - b.pNearZero)
}
//ref 側參考形狀(僅算一次)
let refShapes = {}
if (true) {
let rrs = await calcFn(arrRef, keyIn, optMethodRef)
each(ps, (p) => {
refShapes[p.keyOutRef] = zshape(pool(rrs, p.keyOutRef))
})
}
//score: 候選 optMethod 之目標函數(各 pair 距離平均), 含快取
let nEval = 0
let cache = new Map()
let score = async (om) => {
let k = JSON.stringify(map(params, (p) => om[p]))
if (cache.has(k)) {
return cache.get(k)
}
nEval++
let s = null
try {
let rrs = await calcFn(arrSrc, keyIn, om)
let ds = map(ps, (p) => dist(zshape(pool(rrs, p.keyOutSrc)), refShapes[p.keyOutRef]))
s = mean(ds)
}
catch (err) {
s = Infinity
}
if (!Number.isFinite(s)) {
s = Infinity
}
cache.set(k, s)
return s
}
//best: 搜尋起點(params 中不存在之鍵以 0 起算)
let best = { ...optMethodInit }
each(params, (p) => {
if (!haskey(best, p)) {
best[p] = 0
}
})
//搜尋: 逐維座標下降, 每維三階段(粗網格跨數量級 → 倍率細化 → 微調)
let s0 = await score(best)
let sBest = s0
for (let sweep = 1; sweep <= nSweep; sweep++) {
for (let p of params) {
//cands: 現值 + 0 + 正負粗網格
let cands = [best[p], 0]
each(mags, (m) => {
cands.push(m, -m)
})
//粗掃
let vBest = best[p]
for (let v of uniq(cands)) {
let s = await score({ ...best, [p]: v })
if (s < sBest) {
sBest = s
vBest = v
}
}
//倍率細化 + 微調(以粗掃最佳為中心)
if (vBest !== 0) {
for (let ratios of [[0.3, 0.5, 0.7, 1.5, 2, 3], [0.85, 0.9, 1.1, 1.2]]) {
let vC = vBest
for (let r of ratios) {
let v = vC * r
let s = await score({ ...best, [p]: v })
if (s < sBest) {
sBest = s
vBest = v
}
}
}
}
best[p] = vBest
log(`sweep${sweep} ${p}: ${vBest} (score ${sBest.toFixed(4)}, eval ${nEval})`)
}
}
//防churn: 相對與絕對改善皆達門檻才採納
let adopt = (s0 - sBest) / s0 > threshold && (s0 - sBest) > thresholdAbs
let optMethod = adopt ? { ...best } : { ...optMethodInit }
//detail: 最終 optMethod 下各 pair 形狀對照
let detail = []
if (true) {
let rrs = await calcFn(arrSrc, keyIn, optMethod)
each(ps, (p) => {
let si = zshape(pool(rrs, p.keyOutSrc))
let sp = refShapes[p.keyOutRef]
detail.push({
keyOutSrc: p.keyOutSrc,
keyOutRef: p.keyOutRef,
dist: dist(si, sp),
pNearZeroSrc: si ? si.pNearZero : null,
pNearZeroRef: sp ? sp.pNearZero : null,
})
})
}
return { adopt, optMethod, optMethodBest: { ...best }, score0: s0, scoreBest: sBest, nEval, detail }
}
export default dataCalibrate