package adv import ( "errors" "flag" "fmt" "io" "math" "net/http" "path/filepath" "slices" "sort" "strings" "time" "gonum.org/v1/gonum/floats" "go.uber.org/zap" "github.com/banbox/banbot/biz" "github.com/banbox/banbot/btime" "github.com/banbox/banbot/config" "github.com/banbox/banbot/core" "github.com/banbox/banbot/entry" "github.com/banbox/banbot/opt" "github.com/banbox/banbot/orm" "github.com/banbox/banbot/orm/ormo" utils2 "github.com/banbox/banbot/utils" "github.com/banbox/banexg/errs" "github.com/banbox/banexg/log" "github.com/banbox/banexg/utils" "github.com/spf13/cobra" ) func init() { runConfig := func(args *config.CmdArgs, run func(*config.CmdArgs) *errs.Error) error { core.SetRunMode(core.RunModeOther) args.Init() return run(args) } bindCommon := func(command *cobra.Command, args *config.CmdArgs) { flags := command.Flags() flags.StringArrayVar((*[]string)(&args.Configs), "config", nil, "config path; may be repeated") flags.StringVar(&args.ConfigData, "config-data", "", "inline YAML config") flags.StringVar(&args.Logfile, "logfile", "", "log file path") flags.StringVar(&args.DataDir, "datadir", "", "path to the data directory") flags.BoolVar(&args.NoDefault, "no-default", false, "ignore config.yml and config.local.yml") flags.StringVar(&args.LogLevel, "level", "info", "logging level") flags.IntVar(&args.MaxPoolSize, "max-pool-size", 0, "maximum database pool size") flags.BoolVar(&core.NetDisable, "net-off", false, "disable network requests") } newConfigCommand := func(name, help string, run func(*config.CmdArgs) *errs.Error, bind func(*cobra.Command, *config.CmdArgs)) *cobra.Command { args := &config.CmdArgs{} command := &cobra.Command{ Use: name, Short: help, Args: cobra.NoArgs, RunE: func(_ *cobra.Command, _ []string) error { return runConfig(args, run) }, } bindCommon(command, args) if bind != nil { bind(command, args) } return command } dayRets := newConfigCommand("dayRets", "calc return rate for pairs by months", CalcPairDayPerfs, func(command *cobra.Command, args *config.CmdArgs) { command.Flags().StringVar(&args.InPath, "in", "", "input file or directory") }) entry.AddCommand("tool", dayRets) entry.AddCommand("tool", newConfigCommand("updMkt", "update coin markets by month", UpdateCoinMarkets, nil)) entry.AddCommand("tool", newConfigCommand("buildFac", "build factor samples", BuildFacSamples, nil)) entry.AddCommand("tool", &cobra.Command{ Use: "testFac", Short: "test factors", DisableFlagParsing: true, RunE: func(_ *cobra.Command, args []string) error { return TestFactors(args) }, }) } var urlCoinMarket = "https://api.coinmarketcap.com/data-api/v3/cryptocurrency/listings/historical?convertId=2781&date=%s&limit=%d&start=%d" var coinSlugMap = map[string]string{ "BTCDOM": "", "1000XEC": "ecash", "LUNA2": "terra-luna-v2", "DODOX": "dodo", "BEAMX": "onbeam", "1000RATS": "", "RONIN": "ronin", "1000SHIB": "shiba-inu", "1000LUNC": "terra-luna", "1000FLOKI": "floki-inu", "1000PEPE": "pepe", "1000BONK": "bonk1", } type PairReturns struct { StartMS int64 EndMS int64 Pairs map[string]*ItemReturns } type ItemReturns struct { StartMS int64 EndMS int64 Arr []float64 } type CoinMarket struct { Name string `json:"name"` Symbol string `json:"symbol"` ListMS int64 `json:"list_ms"` Slug string `json:"slug"` FirstMS int64 `json:"first_ms"` LastMS int64 `json:"last_ms"` MarketHis []*MarketSlice `json:"market_his"` } type MarketSlice struct { TimeMS int64 `json:"time_ms"` CirculatingSupply float64 `json:"circulating_supply"` TotalSupply float64 `json:"total_supply"` Price float64 `json:"price"` MarketCap float64 `json:"market_cap"` } type CoinMarketHis struct { Coins map[string]*CoinMarket Times []int64 } type CoinMarketResponse struct { Data []struct { ID int `json:"id"` Name string `json:"name"` Symbol string `json:"symbol"` Slug string `json:"slug"` DateAdded string `json:"dateAdded"` CirculatingSupply float64 `json:"circulatingSupply"` TotalSupply float64 `json:"totalSupply"` Quotes []struct { Name string `json:"name"` Price float64 `json:"price"` MarketCap float64 `json:"marketCap"` } `json:"quotes"` } `json:"data"` Status struct { ErrorCode string `json:"error_code"` ErrorMessage string `json:"error_message"` } `json:"status"` } /* 某个品种在某个时间的历史收益,市场、未来回报等信息 */ type PairSample struct { Offset int // 值int是在ItemReturns.Arr的位置索引 Pair string Code string Price float64 MarketCap float64 Vols []float64 } type FacHis struct { Times []int64 Pairs map[string]*ItemReturns Data map[int64]map[string]*PairSample // time: pair: sample } type FacState struct { Ret *ItemReturns Sample *PairSample Groups map[string]string Label string Tmp float64 // 因子计算临时使用 OutScore float64 // 输出分数 OutID int // 输出分数排序序号 } type GpState struct { count float64 // 该块中FacState的数量 sumScore float64 // 该块的平均分数 } /* CalcPairDayPerfs 从订单计算每个品种的每日收益率 */ func CalcPairDayPerfs(args *config.CmdArgs) *errs.Error { err := biz.SetupComs(args) if err != nil { return err } orders, err := ormo.LoadOrdersGob(args.InPath) if err != nil { return err } pairOrders := make(map[string][]*ormo.InOutOrder) minTimeMS, maxTimeMS := btime.TimeMS(), int64(0) totalCost := float64(0) for _, o := range orders { odList, _ := pairOrders[o.Symbol] pairOrders[o.Symbol] = append(odList, o) minTimeMS = min(minTimeMS, o.Enter.CreateAt) maxTimeMS = max(maxTimeMS, o.Exit.CreateAt) totalCost += o.EnterCost() / o.Leverage } // 假设初始金额能同时开50笔 initStake := totalCost / float64(len(orders)) * 50 // 解析开始时间和结束时间,对齐到月的1号 startYear, startMon, _ := btime.MSToTime(minTimeMS).Date() endYear, endMon, endDay := btime.MSToTime(maxTimeMS).Date() endAddMon := 0 if endDay > 1 { endAddMon = 1 } startMS := time.Date(startYear, startMon, 1, 0, 0, 0, 0, time.UTC).UnixMilli() endTime := time.Date(endYear, endMon, 1, 0, 0, 0, 0, time.UTC) endMS := endTime.AddDate(0, endAddMon, 0).UnixMilli() symbolMap := orm.GetExSymbolMap(core.ExgName, core.Market) dayMSecs := int64(utils.TFToSecs("1d") * 1000) retMap := make(map[string]*ItemReturns) var addNum int for pair, odList := range pairOrders { sort.Slice(odList, func(i, j int) bool { a, b := odList[i], odList[j] return a.Enter.CreateAt < b.Enter.CreateAt }) exs := symbolMap[pair] _, bars, err := orm.GetOHLCV(exs, "1d", startMS, endMS, 0, false) if err != nil { return err } var closes []float64 if len(bars) > 0 { bars, addNum = utils2.FillOHLCVLacks(bars, startMS, endMS, dayMSecs) if addNum > 0 { log.Info("fill ohlcv lack", zap.String("pair", pair), zap.Int("num", addNum)) } closes = make([]float64, len(bars)) for i, b := range bars { closes[i] = b.Close } } // 计算每日回报率 returns, dayOff, dayEnd := ormo.CalcUnitReturns(odList, closes, startMS, endMS, dayMSecs) retRates := make([]float64, len(returns)) for i, ret := range returns { retRates[i] = ret / initStake } retMap[pair] = &ItemReturns{ StartMS: startMS + int64(dayOff)*dayMSecs, EndMS: startMS + int64(dayEnd)*dayMSecs, Arr: retRates[dayOff:dayEnd], } } outPath := filepath.Join(filepath.Dir(args.InPath), "out.gob") return utils2.EncodeGob(outPath, &PairReturns{ StartMS: startMS, EndMS: endMS, Pairs: retMap, }) } func loadHisMarkets() (*CoinMarketHis, string, *errs.Error) { path := filepath.Join(config.GetDataDir(), "cryptoMarkets.gob") var data = &CoinMarketHis{ Coins: make(map[string]*CoinMarket), Times: make([]int64, 0), } err := utils2.DecodeGobFile(path, data) if err != nil { return data, path, err } return data, path, nil } /* UpdateCoinMarkets 更新BanDataDir目录下的cryptoMarkets.gob全币种市场信息。每个月更新一次。 */ func UpdateCoinMarkets(args *config.CmdArgs) *errs.Error { err := biz.SetupComs(args) if err != nil { return err } data, path, err := loadHisMarkets() if err != nil { log.Warn("load cryptoMarkets error", zap.Error(err)) } // 确定开始时间 startTime := time.Date(2014, 1, 1, 0, 0, 0, 0, time.UTC) if len(data.Times) > 0 { startTime = time.UnixMilli(data.Times[len(data.Times)-1]).UTC().AddDate(0, 1, 0) } lastTimeMS := startTime.AddDate(0, -1, 0).UnixMilli() batchSize := 2000 isLast := false // 循环获取每个月的数据 for { if startTime.After(time.Now()) { pyear, pmon, pday := time.Now().UTC().Date() startTime = time.Date(pyear, pmon, pday, 0, 0, 0, 0, time.UTC).AddDate(0, 0, -1) isLast = true } // 获取该月所有币种数据 monthData := make(map[string]*MarketSlice) start := 1 fetchNum, fixLack := 0, 0 timeMS := startTime.UnixMilli() for { url := fmt.Sprintf(urlCoinMarket, startTime.Format("2006-01-02"), batchSize, start) time.Sleep(time.Second * 1) resp, err_ := http.Get(url) log.Info("request", zap.String("url", url)) if err_ != nil { return errs.New(errs.CodeNetFail, err_) } body, err_ := io.ReadAll(resp.Body) resp.Body.Close() if err_ != nil { return errs.New(errs.CodeNetFail, err_) } var rsp CoinMarketResponse if err_ = utils.Unmarshal(body, &rsp, utils.JsonNumAuto); err_ != nil { return errs.New(errs.CodeUnmarshalFail, err_) } if rsp.Status.ErrorCode != "0" { return errs.NewMsg(errs.CodeRunTime, "code: %v, msg: %v", rsp.Status.ErrorCode, rsp.Status.ErrorMessage) } if len(rsp.Data) == 0 { break } for _, coin := range rsp.Data { slice := &MarketSlice{ TimeMS: timeMS, CirculatingSupply: coin.CirculatingSupply, TotalSupply: coin.TotalSupply, } if len(coin.Quotes) > 0 { slice.Price = coin.Quotes[0].Price slice.MarketCap = coin.Quotes[0].MarketCap } monthData[coin.Slug] = slice // 更新或创建币种信息 if cm, exists := data.Coins[coin.Slug]; exists { if len(cm.MarketHis) == 0 || timeMS > cm.MarketHis[len(cm.MarketHis)-1].TimeMS { // 检查是否有缺失的月份数据需要补充 if !isLast { if cm.LastMS < lastTimeMS { fixLack += 1 lastSlice := cm.MarketHis[len(cm.MarketHis)-1] // 从上次更新的下个月开始,直到上个月为止,补充所有缺失的月份 curTime := time.UnixMilli(cm.LastMS).UTC().AddDate(0, 1, 0) for curTime.UnixMilli() <= lastTimeMS { newSlice := &MarketSlice{ TimeMS: curTime.UnixMilli(), CirculatingSupply: lastSlice.CirculatingSupply, TotalSupply: lastSlice.TotalSupply, Price: lastSlice.Price, MarketCap: lastSlice.MarketCap, } cm.MarketHis = append(cm.MarketHis, newSlice) cm.LastMS = curTime.UnixMilli() curTime = curTime.AddDate(0, 1, 0) } } // 添加当前月份的数据 cm.MarketHis = append(cm.MarketHis, slice) cm.LastMS = timeMS } if cm.Name != coin.Name { cm.Name = coin.Name } if cm.Symbol != coin.Symbol { cm.Symbol = coin.Symbol } fetchNum += 1 } } else if !isLast { listTime, _ := time.Parse("2006-01-02T15:04:05.000Z", coin.DateAdded) data.Coins[coin.Slug] = &CoinMarket{ Name: coin.Name, Symbol: coin.Symbol, ListMS: listTime.UnixMilli(), Slug: coin.Slug, FirstMS: timeMS, LastMS: timeMS, MarketHis: []*MarketSlice{slice}, } fetchNum += 1 } } // 这里可能返回数量不足batchSize个,但不代表结束了,继续请求返回为空才表示结束 start += batchSize } // 添加时间戳 data.Times = append(data.Times, timeMS) // 保存数据 if err := utils2.EncodeGob(path, data); err != nil { log.Error("save market data failed", zap.Error(err)) return err } else { log.Info("update coin markets ok", zap.String("date", startTime.Format("2006-01-02")), zap.Int("fetch_num", fetchNum), zap.Int("fixLack", fixLack)) } // 移动到下一个月 lastTimeMS = timeMS startTime = startTime.AddDate(0, 1, 0) if isLast { break } } return nil } /* BuildFacSamples 构建因子定期采样历史 */ func BuildFacSamples(args *config.CmdArgs) *errs.Error { err := biz.SetupComs(args) if err != nil { return err } data, _, err := loadHisMarkets() if err != nil { return err } retData := &PairReturns{Pairs: make(map[string]*ItemReturns)} retPath := "E:\\trade\\go\\bandata\\backtest\\383f7dc32a_all_pairs\\out.gob" if !utils2.Exists(retPath) { args.InPath = filepath.Join(filepath.Dir(retPath), "orders.gob") err = CalcPairDayPerfs(args) if err != nil { return err } } err = utils2.DecodeGobFile(retPath, retData) if err != nil { return err } if len(retData.Pairs) == 0 { return errs.NewMsg(errs.CodeRunTime, "no Pairs for PairReturns") } res := &FacHis{Pairs: retData.Pairs, Data: make(map[int64]map[string]*PairSample)} startMS := retData.StartMS // 构建Symbol到CoinMarket的映射 var b strings.Builder symbolToCoin := make(map[string]*CoinMarket) badCoins := make(map[string]bool) skipCoins := make(map[string]bool) dupCoins := make(map[string][]string) for _, coin := range data.Coins { if old, ok := symbolToCoin[coin.Symbol]; ok { dups, _ := dupCoins[coin.Symbol] if len(dups) == 0 { dups = append(dups, old.Slug) } dupCoins[coin.Symbol] = append(dups, coin.Slug) continue } symbolToCoin[coin.Symbol] = coin b.WriteString(coin.Symbol) b.WriteString(", ") } symbolMap := orm.GetExSymbolMap(core.ExgName, core.Market) log.Info("all coins", zap.String("v", b.String())) if len(dupCoins) > 0 { log.Info("dupCoins", zap.String("v", utils2.MapToStr(dupCoins, true, 0))) } // 设置月份间隔(这里设置为3个月) monNum := 2 // 将startMS转换为月初 startTime := time.UnixMilli(startMS).UTC() startTime = time.Date(startTime.Year(), startTime.Month(), 1, 0, 0, 0, 0, time.UTC) startTime = startTime.AddDate(0, monNum, 0) endTime := time.UnixMilli(retData.EndMS).UTC() // 从startTime开始遍历时间点 for curTime := startTime; !curTime.After(endTime); curTime = curTime.AddDate(0, monNum, 0) { timeMS := curTime.UnixMilli() timeMap := make(map[string]*PairSample) prevTime := curTime.AddDate(0, -monNum, 0) nextTime := curTime.AddDate(0, monNum, 0) // 对每个pair的returns进行检查 for pair, itemRet := range retData.Pairs { // 检查时间范围是否符合要求 if itemRet.StartMS > prevTime.UnixMilli() || itemRet.EndMS < nextTime.UnixMilli() { continue } // 计算当前时间点在Arr中的索引 offset := int((timeMS - itemRet.StartMS) / (24 * 3600 * 1000)) // 转换为天数 if offset < 0 || offset >= len(itemRet.Arr) { continue } // 查找对应的CoinMarket baseCode, _, _, _ := core.SplitSymbol(pair) coin, ok := symbolToCoin[baseCode] if !ok { if slug, ok := coinSlugMap[baseCode]; ok { if slug == "" { skipCoins[baseCode] = true continue } coin = data.Coins[slug] } else { badCoins[baseCode] = true continue } } // 查找当前时间点对应的MarketSlice var mktSlice *MarketSlice for _, slice := range coin.MarketHis { if slice.TimeMS <= timeMS { mktSlice = slice } else { break } } if mktSlice == nil { // 太新,跳过 continue } // 获取成交量数据 exs := symbolMap[pair] if exs == nil { return errs.NewMsg(errs.CodeRunTime, "ExSymbol not exist: %v", pair) } // 计算2周和1个月的成交量 barStartMS := prevTime.UnixMilli() _, bars, err := orm.GetOHLCV(exs, "1d", barStartMS, timeMS, 0, false) if err != nil { log.Warn("get volume failed", zap.String("pair", pair), zap.Error(err)) continue } // 计算成交量 vols := make([]float64, len(bars)) for i, bar := range bars { vols[i] = bar.Close * bar.Volume } // 构建PairSample sample := &PairSample{ Offset: offset, Pair: pair, Code: baseCode, Price: mktSlice.Price, MarketCap: mktSlice.MarketCap, Vols: vols, } timeMap[pair] = sample } if len(timeMap) > 0 { res.Data[timeMS] = timeMap res.Times = append(res.Times, timeMS) } } if len(badCoins) > 0 { log.Warn("unknown coins", zap.String("v", utils2.MapToStr(badCoins, false, 0))) } if len(skipCoins) > 0 { log.Warn("skip coins", zap.String("v", utils2.MapToStr(skipCoins, false, 0))) } // 保存结果 name := fmt.Sprintf("facHis_%dM.gob", monNum) outPath := filepath.Join(config.GetDataDir(), name) return utils2.EncodeGob(outPath, res) } func TestFactors(args []string) error { parser := flag.NewFlagSet("", flag.ExitOnError) var monNum, outNum, offNum int var facStr, outBy string parser.IntVar(&monNum, "mon", 2, "month for period") parser.IntVar(&outNum, "out", 3, "output group num") parser.IntVar(&offNum, "off", 0, "skip group num in head") parser.StringVar(&facStr, "fac", "", "factors to test") parser.StringVar(&outBy, "outBy", "", "calc output score by") err_ := parser.Parse(args) if err_ != nil { return err_ } facArr := utils2.SplitSolid(facStr, ",", true) if len(facArr) == 0 { return errors.New("-fac is required") } dayNum := monNum * 30 fac := &FacHis{Pairs: make(map[string]*ItemReturns), Data: make(map[int64]map[string]*PairSample)} facPath := filepath.Join(config.GetDataDir(), fmt.Sprintf("facHis_%dM.gob", monNum)) err := utils2.DecodeGobFile(facPath, fac) if err != nil { return err } groupMeas := make(map[string][]*GpState) for i, stampMS := range fac.Times { if i <= offNum { continue } pairSamples := fac.Data[stampMS] items := make([]*FacState, 0) for pair, smp := range pairSamples { ret := fac.Pairs[pair] items = append(items, &FacState{ Ret: ret, Sample: smp, Groups: make(map[string]string), }) } // 计算每个采样所属的因子分组 for _, factor := range facArr { var pos []int var tags []string if factor == "marketCap" { pos, tags = facMarketCap(items) } else if factor == "vol1M" { // 优于vol2w和marketCap pos, tags = facVol1M(items) } else if factor == "volStd" { // 成交量波动较低的 pos, tags = facVolStd(items) } else if factor == "mont1" { // 3M下,跟mont2不相上下 pos, tags = facMont1(items, dayNum) } else if factor == "mont2" { pos, tags = facMont2(items, dayNum) } else if factor == "mont1y" { pos, tags = facMont1Y(items, dayNum) } else if factor == "sharpe1y" { pos, tags = facSharpe1Y(items, dayNum) } else if factor == "score1y" { pos, tags = facScore1Y(items, dayNum) } else { return errors.New("unknown factor: " + factor) } if len(pos)+1 != len(tags) { return errors.New("invalid factor: " + factor) } start := 0 for i, end := range pos { tag := tags[i] for j := start; j < end; j++ { items[j].Groups[factor] = tag } start = end } tag := tags[len(tags)-1] for j := start; j < len(items); j++ { items[j].Groups[factor] = tag } } // 组合因子分组 groups := make(map[string][]*FacState) for _, sta := range items { var b strings.Builder for _, f := range facArr { b.WriteString(sta.Groups[f] + "+") } sta.Label = strings.TrimRight(b.String(), "+") gpItems, _ := groups[sta.Label] groups[sta.Label] = append(gpItems, sta) end := sta.Sample.Offset + dayNum if end > len(sta.Ret.Arr) { end = len(sta.Ret.Arr) } // 计算未来分数,并进行分组打分 futures := sta.Ret.Arr[sta.Sample.Offset:end] if outBy == "profit" { sta.OutScore = floats.Sum(futures) } else if outBy == "sharpe" { sta.OutScore, err_ = utils2.SharpeRatioBy(futures, 0.02, 365, true) if err_ != nil { return err_ } } else if outBy == "smart" || outBy == "" { sharpe, err_ := utils2.SharpeRatioBy(futures, 0.02, 365, true) if err_ != nil { return err_ } profit := floats.Sum(futures) sta.OutScore = math.Log2(profit) * sharpe } else { return errors.New("unknown outBy: " + outBy) } } // 按输出分数降序 sort.Slice(items, func(i, j int) bool { return items[i].OutScore < items[j].OutScore }) for i, it := range items { it.OutID = i } // 计算每个目标块应包含的FacState数量 blockNum := int(math.Ceil(float64(len(items)) / float64(outNum))) for key, gpList := range groups { // 遍历分组中的每个FacState,根据其OutID确定所属块 blocks, ok := groupMeas[key] if !ok { blocks = make([]*GpState, outNum) for i := 0; i < outNum; i++ { blocks[i] = &GpState{} } groupMeas[key] = blocks } for _, state := range gpList { blockID := state.OutID / blockNum // 确定目标块ID if blockID >= outNum { blockID = outNum - 1 // 处理边界情况 } // 更新该块的统计信息 block := blocks[blockID] block.count++ block.sumScore += utils2.NanInfTo(state.OutScore, 0) } } } countDsList := make([]*opt.ChartDs, len(groupMeas)) // 用于绘制数量分布图 scoreDsList := make([]*opt.ChartDs, len(groupMeas)) // 用于绘制平均分数图 // 生成x轴标签 xLabels := make([]string, outNum) for i := 0; i < outNum; i++ { xLabels[i] = fmt.Sprintf("Block%d", i+1) } keys := utils2.KeysOfMap(groupMeas) slices.Sort(keys) for i, key := range keys { counts := make([]float64, outNum) for j := 0; j < outNum; j++ { counts[j] = groupMeas[key][j].count } countDsList[i] = &opt.ChartDs{ Label: key, Data: counts, } scores := make([]float64, outNum) for j := 0; j < outNum; j++ { bk := groupMeas[key][j] scores[j] = bk.sumScore / bk.count } scoreDsList[i] = &opt.ChartDs{ Label: key, Data: scores, } } facNames := strings.Join(facArr, "+") // 生成数量分布折线图 countPath := filepath.Join(config.GetDataDir(), fmt.Sprintf("fac_count_%dM.html", monNum)) err = opt.DumpChart(countPath, "Distribution: "+facNames, xLabels, 5, nil, countDsList) if err != nil { return err } // 生成平均分数折线图 scorePath := filepath.Join(config.GetDataDir(), fmt.Sprintf("fac_score_%dM.html", monNum)) err = opt.DumpChart(scorePath, "Average Scores: "+facNames, xLabels, 5, nil, scoreDsList) if err != nil { return err } log.Info(fmt.Sprintf("generate in BanDataDir: fac_score_%dM.html fac_count_%dM.html", monNum, monNum)) return nil } func facMarketCap(items []*FacState) ([]int, []string) { slices.SortFunc(items, func(a, b *FacState) int { return int((b.Sample.MarketCap - a.Sample.MarketCap) / 1000) }) totalNum := float64(len(items)) bigNum := int(totalNum * 0.3) midEnd := int(totalNum*0.4) + bigNum return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} } func facVol1M(items []*FacState) ([]int, []string) { for _, it := range items { start := max(0, len(it.Sample.Vols)-30) it.Tmp = floats.Sum(it.Sample.Vols[start:]) } slices.SortFunc(items, func(a, b *FacState) int { return int((b.Tmp - a.Tmp) / 1000) }) totalNum := float64(len(items)) bigNum := int(totalNum * 0.3) midEnd := int(totalNum*0.4) + bigNum return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} } func facVolStd(items []*FacState) ([]int, []string) { for _, it := range items { arr := it.Sample.Vols data := make([]float64, 0, len(arr)) for i, v := range arr[1:] { data = append(data, v/arr[i]) } it.Tmp = utils2.StdDevVolatility(data, 1) } slices.SortFunc(items, func(a, b *FacState) int { return int((b.Tmp - a.Tmp) / 1000) }) totalNum := float64(len(items)) bigNum := int(totalNum * 0.3) midEnd := int(totalNum*0.4) + bigNum return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} } func facMont1(items []*FacState, unitDays int) ([]int, []string) { for _, it := range items { start := max(0, it.Sample.Offset-unitDays) it.Tmp = floats.Sum(it.Ret.Arr[start:it.Sample.Offset]) } slices.SortFunc(items, func(a, b *FacState) int { return int((b.Tmp - a.Tmp) / 1000) }) totalNum := float64(len(items)) bigNum := int(totalNum * 0.3) midEnd := int(totalNum*0.4) + bigNum return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} } func facMont2(items []*FacState, unitDays int) ([]int, []string) { for _, it := range items { start := max(0, it.Sample.Offset-unitDays*2) it.Tmp = floats.Sum(it.Ret.Arr[start:it.Sample.Offset]) } slices.SortFunc(items, func(a, b *FacState) int { return int((b.Tmp - a.Tmp) / 1000) }) totalNum := float64(len(items)) bigNum := int(totalNum * 0.3) midEnd := int(totalNum*0.4) + bigNum return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} } func facMont1Y(items []*FacState, unitDays int) ([]int, []string) { for _, it := range items { start := max(0, it.Sample.Offset-unitDays*6) it.Tmp = floats.Sum(it.Ret.Arr[start:it.Sample.Offset]) } slices.SortFunc(items, func(a, b *FacState) int { return int((b.Tmp - a.Tmp) / 1000) }) totalNum := float64(len(items)) bigNum := int(totalNum * 0.3) midEnd := int(totalNum*0.4) + bigNum return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} } func facSharpe1Y(items []*FacState, unitDays int) ([]int, []string) { for _, it := range items { start := max(0, it.Sample.Offset-unitDays*6) arr := it.Ret.Arr[start:it.Sample.Offset] sharpe, err := utils2.SharpeRatioBy(arr, 0.02, 365, true) if err != nil { log.Info("calc sharpe fail", zap.Error(err)) } it.Tmp = sharpe } slices.SortFunc(items, func(a, b *FacState) int { return int((b.Tmp - a.Tmp) / 1000) }) totalNum := float64(len(items)) bigNum := int(totalNum * 0.3) midEnd := int(totalNum*0.4) + bigNum return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} } func facScore1Y(items []*FacState, unitDays int) ([]int, []string) { for _, it := range items { start := max(0, it.Sample.Offset-unitDays*6) arr := it.Ret.Arr[start:it.Sample.Offset] sharpe, err_ := utils2.SharpeRatioBy(arr, 0.02, 365, true) if err_ != nil { log.Error("calc sharpe fail", zap.Error(err_)) } profit := floats.Sum(arr) it.Tmp = math.Log2(profit) * sharpe } slices.SortFunc(items, func(a, b *FacState) int { return int((b.Tmp - a.Tmp) / 1000) }) totalNum := float64(len(items)) bigNum := int(totalNum * 0.3) midEnd := int(totalNum*0.4) + bigNum return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} }