|
@@ -0,0 +1,381 @@
|
|
|
|
|
+const logger = require("./utils/logger");
|
|
|
|
|
+const NumKit = require('./utils/num-kit')
|
|
|
|
|
+const TimeKit = require('./utils/time-kit')
|
|
|
|
|
+const ChartKit = require('./utils/chart-kit');
|
|
|
|
|
+const fs = require('fs').promises;
|
|
|
|
|
+
|
|
|
|
|
+async function readData() {
|
|
|
|
|
+ let data = await fs.readFile('./data/k_lines.json', 'utf8')
|
|
|
|
|
+
|
|
|
|
|
+ return JSON.parse(data)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 概率学说 */
|
|
|
|
|
+// 统计过去N根累计涨幅,【有一定关系】
|
|
|
|
|
+function statisticA(kLines, index) {
|
|
|
|
|
+ let startK = kLines[index - 12]
|
|
|
|
|
+ let endK = kLines[index - 1]
|
|
|
|
|
+
|
|
|
|
|
+ if (!startK || !endK) {
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return parseInt(100 * (endK.Close - startK.Open) / startK.Open) + 100
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 返回过去一共NK涨幅超过M%【这个有关的】
|
|
|
|
|
+function statisticB(kLines, index) {
|
|
|
|
|
+ let count = 0
|
|
|
|
|
+ for (let i = index - 1; i >= index - 2; i--) {
|
|
|
|
|
+ let kLine = kLines[i]
|
|
|
|
|
+
|
|
|
|
|
+ if (!kLines[i - 1]) break
|
|
|
|
|
+
|
|
|
|
|
+ if (100 * (kLine.Open - kLine.Close) / kLine.Open < 1) continue
|
|
|
|
|
+
|
|
|
|
|
+ count += 1
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return count
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 指标学说 */
|
|
|
|
|
+
|
|
|
|
|
+// 一个新的指标,前N根共有M根与BTC走势一致
|
|
|
|
|
+function statisticC(kLines, index, btcKLines, btcIndex) {
|
|
|
|
|
+ let count = 0
|
|
|
|
|
+ for (let i = 6; i >= 1; i--) {
|
|
|
|
|
+ let kLine = kLines[index - i]
|
|
|
|
|
+ let btcKLine = btcKLines[btcIndex - i]
|
|
|
|
|
+
|
|
|
|
|
+ if (!kLines[index - i - 1] || !btcKLines[btcIndex - i - 1]) break
|
|
|
|
|
+
|
|
|
|
|
+ if (kLine.Open > kLine.Close && btcKLine.Open > btcKLine.Close) count += 1
|
|
|
|
|
+ if (kLine.Open < kLine.Close && btcKLine.Open < btcKLine.Close) count += 1
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return count
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 形态学说 */
|
|
|
|
|
+
|
|
|
|
|
+// 如果前一根k线是阴线,并且收了影线,影线涨幅>=实体跌幅*0.732【有用,能过滤60%的交易,但是过滤的交易很多都是能盈利的,所以这个因子只能说保留一下】
|
|
|
|
|
+// function statisticD(_kLines, _index) {
|
|
|
|
|
+// let k = kLines[index - 1]
|
|
|
|
|
+//
|
|
|
|
|
+// if (!k) return 0
|
|
|
|
|
+//
|
|
|
|
|
+// // 计算实体的跌幅
|
|
|
|
|
+// const bodyDecline = k.Open - k.Close;
|
|
|
|
|
+//
|
|
|
|
|
+// // 如果跌幅小于5%,不适用这个过滤
|
|
|
|
|
+// if (100 * (k.Low - k.Open) / k.Open > -10) return 1
|
|
|
|
|
+//
|
|
|
|
|
+// // 计算下影线的涨幅
|
|
|
|
|
+// const lowerShadowGain = Math.min(k.Open, k.Close) - k.Low;
|
|
|
|
|
+//
|
|
|
|
|
+// // 判断下影线的涨幅是否大于或等于实体的跌幅
|
|
|
|
|
+// const hasLongLowerShadow = (lowerShadowGain / k.Low) >= (bodyDecline / k.Open) * 1;
|
|
|
|
|
+//
|
|
|
|
|
+// // 阴线并且下影线涨幅大于或等于实体跌幅
|
|
|
|
|
+// return hasLongLowerShadow ? 1 : 0;
|
|
|
|
|
+//
|
|
|
|
|
+// return 0
|
|
|
|
|
+// }
|
|
|
|
|
+
|
|
|
|
|
+// function statisticE(kLines, index) {
|
|
|
|
|
+// let kLine = kLines[index]
|
|
|
|
|
+// let date = new Date(kLine.Time)
|
|
|
|
|
+//
|
|
|
|
|
+// // date.getDay() * 100
|
|
|
|
|
+// return date.getHours()
|
|
|
|
|
+// }
|
|
|
|
|
+
|
|
|
|
|
+// 过去24小时主动买入比例
|
|
|
|
|
+// function statisticF(kLines, index) {
|
|
|
|
|
+// let sum = 0
|
|
|
|
|
+// let buySum = 0
|
|
|
|
|
+// for (let i = index - 1; i >= index - 24; i--) {
|
|
|
|
|
+// let kLine = kLines[i]
|
|
|
|
|
+//
|
|
|
|
|
+// if (!kLines[i - 1]) break
|
|
|
|
|
+//
|
|
|
|
|
+// sum += kLine.Turnover
|
|
|
|
|
+// buySum += kLine.BuyTurnover
|
|
|
|
|
+// }
|
|
|
|
|
+//
|
|
|
|
|
+// return parseInt(100 * buySum / sum)
|
|
|
|
|
+// }
|
|
|
|
|
+
|
|
|
|
|
+// 指标过滤
|
|
|
|
|
+function filter(btcKLines, kLines, index, btcIndex, symbol) {
|
|
|
|
|
+ // // 过去N根K的累计涨幅
|
|
|
|
|
+ // let upRateN = statisticA(kLines, index)
|
|
|
|
|
+ // if (upRateN >= 90) {
|
|
|
|
|
+ // return false
|
|
|
|
|
+ // }
|
|
|
|
|
+
|
|
|
|
|
+ // 一共NK涨幅超过M%
|
|
|
|
|
+ if (statisticB(kLines, index) > 0) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // // 过去N根k线一共有M根与btc走势一致
|
|
|
|
|
+ // if (statisticC(kLines, index, btcKLines, btcIndex) > 4) {
|
|
|
|
|
+ // return false
|
|
|
|
|
+ // }
|
|
|
|
|
+
|
|
|
|
|
+ return NOOBS.indexOf(symbol) === -1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+let realCountMap = {}
|
|
|
|
|
+function getRealDragonMap(btcKLines, kLinesMap, dayCount, BUY_LIMIT_RATE) {
|
|
|
|
|
+ let realDragonMap = {}
|
|
|
|
|
+ for (let symbol in kLinesMap) {
|
|
|
|
|
+ let kLines = kLinesMap[symbol]
|
|
|
|
|
+
|
|
|
|
|
+ let index = kLines.length - (dayCount + 1)
|
|
|
|
|
+ let btcIndex = btcKLines.length - (dayCount + 1)
|
|
|
|
|
+ let kLine = kLines[index]
|
|
|
|
|
+ let prevKline = kLines[index - 1]
|
|
|
|
|
+
|
|
|
|
|
+ // 开盘第一K不计算
|
|
|
|
|
+ if (!kLine || !prevKline) continue
|
|
|
|
|
+ // 注意btc的时间戳要和计算的k的时间戳一样
|
|
|
|
|
+ if (btcKLines[btcIndex].Time !== kLine.Time) continue
|
|
|
|
|
+
|
|
|
|
|
+ // 指标过滤
|
|
|
|
|
+ if (!filter(btcKLines, kLines, index, btcIndex, symbol)) continue
|
|
|
|
|
+
|
|
|
|
|
+ let rate = 100 * (kLine.Open - kLine.Close) / kLine.Open
|
|
|
|
|
+ // let upRate = 100 * (kLine.High - kLine.Open) / kLine.Open
|
|
|
|
|
+
|
|
|
|
|
+ if (rate > BUY_LIMIT_RATE) {
|
|
|
|
|
+ kLine.Rate = rate
|
|
|
|
|
+ kLine.Profit = NumKit.getSubFloat(rate - BUY_LIMIT_RATE, 2)
|
|
|
|
|
+
|
|
|
|
|
+ // if (upRate > 150) kLine.Profit = 140
|
|
|
|
|
+
|
|
|
|
|
+ realDragonMap[symbol] = kLine
|
|
|
|
|
+
|
|
|
|
|
+ if (!realCountMap[symbol]) {
|
|
|
|
|
+ realCountMap[symbol] = kLine.Profit
|
|
|
|
|
+ } else {
|
|
|
|
|
+ realCountMap[symbol] += kLine.Profit
|
|
|
|
|
+ }
|
|
|
|
|
+ realCountMap[symbol] = NumKit.getSubFloat(realCountMap[symbol], 2)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return realDragonMap
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+let fakeCountMap = {}
|
|
|
|
|
+function getFakeDragonMap(btcKLines, kLinesMap, dayCount, BUY_LIMIT_RATE) {
|
|
|
|
|
+ let fakeDragonMap = {}
|
|
|
|
|
+ for (let symbol in kLinesMap) {
|
|
|
|
|
+ let kLines = kLinesMap[symbol]
|
|
|
|
|
+ let index = kLines.length - (dayCount + 1)
|
|
|
|
|
+ let btcIndex = btcKLines.length - (dayCount + 1)
|
|
|
|
|
+ let kLine = kLines[index]
|
|
|
|
|
+ let prevKline = kLines[index - 1]
|
|
|
|
|
+
|
|
|
|
|
+ // 开盘第一K不计算
|
|
|
|
|
+ if (!kLine || (!prevKline)) continue
|
|
|
|
|
+ // 注意btc的时间戳要和计算的k的时间戳一样
|
|
|
|
|
+ if (btcKLines[btcIndex].Time !== kLine.Time) continue
|
|
|
|
|
+
|
|
|
|
|
+ // 指标过滤
|
|
|
|
|
+ if (!filter(btcKLines, kLines, index, btcIndex, symbol)) continue
|
|
|
|
|
+
|
|
|
|
|
+ let rate = 100 * (kLine.Open - kLine.Close) / kLine.Open
|
|
|
|
|
+ if (rate < BUY_LIMIT_RATE) {
|
|
|
|
|
+ kLine.Rate = rate
|
|
|
|
|
+ // kLine.Profit = NumKit.getSubFloat(rate > 0 ? rate - BUY_LIMIT_RATE : -BUY_LIMIT_RATE, 2)
|
|
|
|
|
+ kLine.Profit = NumKit.getSubFloat(rate - BUY_LIMIT_RATE, 2)
|
|
|
|
|
+ fakeDragonMap[symbol] = kLine
|
|
|
|
|
+
|
|
|
|
|
+ if (!fakeCountMap[symbol]) {
|
|
|
|
|
+ fakeCountMap[symbol] = kLine.Profit
|
|
|
|
|
+ } else {
|
|
|
|
|
+ fakeCountMap[symbol] += kLine.Profit
|
|
|
|
|
+ }
|
|
|
|
|
+ fakeCountMap[symbol] = NumKit.getSubFloat(fakeCountMap[symbol], 2)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return fakeDragonMap
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+let dataLeft = {}
|
|
|
|
|
+let dataRight = {}
|
|
|
|
|
+let tempLeft = []
|
|
|
|
|
+let tempRight = []
|
|
|
|
|
+let dataY = []
|
|
|
|
|
+function dragonAnalysis(btcKLines, kLinesMap, dragonMap, _dayCount) {
|
|
|
|
|
+ for (let symbol in dragonMap) {
|
|
|
|
|
+ // let kLines = kLinesMap[symbol]
|
|
|
|
|
+ // let index = kLines.length - (dayCount + 1)
|
|
|
|
|
+
|
|
|
|
|
+ // let x = statisticE(kLines, index)
|
|
|
|
|
+ // dataY = [100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220, 222, 300, 302, 304, 306, 308, 310, 312, 314, 316, 318, 320, 322, 400, 402, 404, 406, 408, 410, 412, 414, 416, 418, 420, 422, 500, 502, 504, 506, 508, 510, 512, 514, 516, 518, 520, 522, 600, 602, 604, 606, 608, 610, 612, 614, 616, 618, 620, 622, 700, 702, 704, 706, 708, 710, 712, 714, 716, 718, 720, 722]
|
|
|
|
|
+ // let x = statisticE(btcKLines, kLines, index)
|
|
|
|
|
+ let x = symbol
|
|
|
|
|
+ // let x = statisticF(kLines, index)
|
|
|
|
|
+ let y = dragonMap[symbol].Profit
|
|
|
|
|
+
|
|
|
|
|
+ // 打印实际的量
|
|
|
|
|
+ if (1 === 1) {
|
|
|
|
|
+ if (y > 0) {
|
|
|
|
|
+ if (dataRight[x]) {
|
|
|
|
|
+ dataRight[x] += y
|
|
|
|
|
+ } else {
|
|
|
|
|
+ dataRight[x] = y
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (dataLeft[x]) {
|
|
|
|
|
+ dataLeft[x] += y
|
|
|
|
|
+ } else {
|
|
|
|
|
+ dataLeft[x] = y
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 打印倍数
|
|
|
|
|
+ if (y > 0) {
|
|
|
|
|
+ if (tempRight[x]) {
|
|
|
|
|
+ tempRight[x] += y
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tempRight[x] = y
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (tempLeft[x]) {
|
|
|
|
|
+ tempLeft[x] += y
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tempLeft[x] = y
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ dataRight[x] = (tempLeft[x]?tempRight[x]/Math.abs(tempLeft[x]):0)
|
|
|
|
|
+ if (dataRight[x] > 10) dataRight[x] = 10
|
|
|
|
|
+ if (dataRight[x] < 0.5) dataRight[x] = 0.5
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+let NOOBS = []
|
|
|
|
|
+// let NOOBS = []
|
|
|
|
|
+
|
|
|
|
|
+async function main() {
|
|
|
|
|
+ let kLinesMap = await readData()
|
|
|
|
|
+
|
|
|
|
|
+ const BUY_LIMIT_RATE = 0 // 从什么比例入场
|
|
|
|
|
+
|
|
|
|
|
+ const FIRST_FEW_DAYS = 0 // 第几K的数据,0表示今K,1表示昨K,2表示前K,以此类推
|
|
|
|
|
+ const BAKE_TEST_DAYS = 144 // 一共回测多少K
|
|
|
|
|
+ // const FIRST_FEW_DAYS = 710 // 第几K的数据,0表示今K,1表示昨K,2表示前K,以此类推
|
|
|
|
|
+ // const BAKE_TEST_DAYS = 1 // 一共回测多少K
|
|
|
|
|
+
|
|
|
|
|
+ let profitList = []
|
|
|
|
|
+ let initProfit = 1
|
|
|
|
|
+ let profitCount = []
|
|
|
|
|
+ let btcKLines = kLinesMap['BTC_USDT']
|
|
|
|
|
+ let totalProfit = 0
|
|
|
|
|
+ for (let day_count = FIRST_FEW_DAYS + BAKE_TEST_DAYS - 1; day_count > FIRST_FEW_DAYS; day_count--) {
|
|
|
|
|
+ let btcKline = btcKLines[btcKLines.length - (day_count + 1)]
|
|
|
|
|
+ // let btcKlineDownRate = 100 * (btcKline.Open - btcKline.Low) / btcKline.Open // 可能的回撤计算
|
|
|
|
|
+ let btcKlineUpRate = 100 * (btcKline.High - btcKline.Close) / btcKline.High // 可能的回撤计算
|
|
|
|
|
+
|
|
|
|
|
+ // 赚钱榜
|
|
|
|
|
+ // logger.info("----------------赚钱榜数据分析----------------")
|
|
|
|
|
+ let realDragonProfit = 0
|
|
|
|
|
+ let realDragonMap = getRealDragonMap(btcKLines, kLinesMap, day_count, BUY_LIMIT_RATE)
|
|
|
|
|
+ for (let symbol in realDragonMap) {
|
|
|
|
|
+ realDragonProfit += realDragonMap[symbol].Profit
|
|
|
|
|
+ // logger.info(`${realDragonMap[symbol].Symbol}, ${realDragonMap[symbol].Profit}%`)
|
|
|
|
|
+ }
|
|
|
|
|
+ dragonAnalysis(btcKLines, kLinesMap, realDragonMap, day_count)
|
|
|
|
|
+
|
|
|
|
|
+ // logger.info("----------------亏钱榜数据分析----------------")
|
|
|
|
|
+ // 亏钱榜
|
|
|
|
|
+ let fakeDragonProfit = 0
|
|
|
|
|
+ let fakeDragonMap = getFakeDragonMap(btcKLines, kLinesMap, day_count, BUY_LIMIT_RATE)
|
|
|
|
|
+ for (let symbol in fakeDragonMap) {
|
|
|
|
|
+ fakeDragonProfit += fakeDragonMap[symbol].Profit
|
|
|
|
|
+ // logger.info(`${fakeDragonMap[symbol].Symbol}, ${fakeDragonMap[symbol].Profit}%`)
|
|
|
|
|
+ }
|
|
|
|
|
+ dragonAnalysis(btcKLines, kLinesMap, fakeDragonMap, day_count)
|
|
|
|
|
+
|
|
|
|
|
+ realDragonProfit = NumKit.getSubFloat(realDragonProfit, 2)
|
|
|
|
|
+ fakeDragonProfit = NumKit.getSubFloat(fakeDragonProfit, 2)
|
|
|
|
|
+ let realLength = Object.keys(realDragonMap).length
|
|
|
|
|
+ let fakeLength = Object.keys(fakeDragonMap).length
|
|
|
|
|
+ let avgProfit = NumKit.getSubFloat((fakeDragonProfit + realDragonProfit) / (realLength + fakeLength), 2)
|
|
|
|
|
+ let isStopLoss = false
|
|
|
|
|
+ if ((Object.keys(fakeDragonMap).length !== 0 || Object.keys(realDragonMap).length !== 0)) {
|
|
|
|
|
+ if (btcKlineUpRate > 3.4) {
|
|
|
|
|
+ avgProfit -= 6.8
|
|
|
|
|
+ if (avgProfit < -6.8) avgProfit = -6.8
|
|
|
|
|
+ isStopLoss = true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ avgProfit = NumKit.getSubFloat(avgProfit - 0.05, 2) // 手续费算进去
|
|
|
|
|
+ initProfit *= (100 + avgProfit) / 100
|
|
|
|
|
+ profitList.push(initProfit)
|
|
|
|
|
+ profitCount.push(profitList.length)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ avgProfit = 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ let index = kLinesMap['BTC_USDT'].length - (day_count + 1)
|
|
|
|
|
+ let btcK = kLinesMap['BTC_USDT'][index]
|
|
|
|
|
+ let dateStr = TimeKit.getTimeByMillisecond(btcK.Time)
|
|
|
|
|
+ // let _btcUpRate = NumKit.getSubFloat(100 * (btcK.Close - btcK.Open) / btcK.Open, 2)
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(`${day_count}根(${dateStr}), ${realLength + fakeLength}只, 平均每只利润${avgProfit}% ${isStopLoss ? '(止损)' : ''},`
|
|
|
|
|
+ + `赚钱(${realLength}只)利润${realDragonProfit}%,亏钱(${fakeLength}只)利润${fakeDragonProfit}%`
|
|
|
|
|
+ // + `,BTC涨幅${btcUpRate}%`
|
|
|
|
|
+ )
|
|
|
|
|
+ logger.info('')
|
|
|
|
|
+ logger.info('--------------------------------------------------------------------------------------------')
|
|
|
|
|
+
|
|
|
|
|
+ totalProfit += avgProfit
|
|
|
|
|
+ totalProfit = NumKit.getSubFloat(totalProfit, 2)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let dayProfit = NumKit.getSubFloat(totalProfit / BAKE_TEST_DAYS, 2)
|
|
|
|
|
+ logger.info(JSON.stringify(profitList))
|
|
|
|
|
+ logger.info(JSON.stringify(profitCount))
|
|
|
|
|
+ logger.info(`利润期望值总和:${totalProfit}%,平均根化${dayProfit}%。`)
|
|
|
|
|
+ // let lastData = []
|
|
|
|
|
+ // for (let x = 0; x < dataRight.length; x++) {
|
|
|
|
|
+ // lastData.push([x, dataRight[x]])
|
|
|
|
|
+ // }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // 这里可以生成noobs
|
|
|
|
|
+ dataY = Object.keys(dataRight)
|
|
|
|
|
+ let noobs = []
|
|
|
|
|
+ for (let symbol in dataLeft) {
|
|
|
|
|
+ if (dataLeft[symbol] + dataRight[symbol] < 0) {
|
|
|
|
|
+ noobs.push(symbol)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ logger.info(JSON.stringify(noobs))
|
|
|
|
|
+ // 如果key是字符串,就要加这些
|
|
|
|
|
+ dataLeft = Object.values(dataLeft)
|
|
|
|
|
+ dataRight = Object.values(dataRight)
|
|
|
|
|
+ let rst = 'option=' + JSON.stringify(ChartKit.printBar(dataY, dataLeft, dataRight), null, 2)
|
|
|
|
|
+ // let rst = 'option=' + JSON.stringify(ChartKit.printPointChart(lastData), null, 2)
|
|
|
|
|
+ require('fs').writeFile('./data/option.txt', rst, 'utf8', (err) => {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ logger.error('写入文件时发生错误:', err);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ logger.info('分析结果已写入到 ./data/option.txt');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+main().catch((error) => {
|
|
|
|
|
+ logger.error(error.stack);
|
|
|
|
|
+ process.exitCode = 1;
|
|
|
|
|
+})
|