Files
banstagy/adv/pairs.go
2026-08-24 20:53:21 +08:00

923 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"}
}