init
This commit is contained in:
922
adv/pairs.go
Normal file
922
adv/pairs.go
Normal 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"}
|
||||
}
|
||||
Reference in New Issue
Block a user