This commit is contained in:
2026-08-24 20:53:21 +08:00
commit 6e78bba16a
287 changed files with 32443 additions and 0 deletions

922
adv/pairs.go Normal file
View File

@@ -0,0 +1,922 @@
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"}
}