commit 33d670ab30caf2f6e76d271248b656209288da4a Author: lychiyu Date: Wed Jun 26 00:19:25 2024 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eec3d14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +*.log +*.yaml \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..528277c --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# trade +I hope trade is "The last trade app you need" ! + +[中文](README_cn.md) + +# Features + +1. Develop/write strategy with only go language,no other script need +2. Event base framework,easy to extend +3. Support binance,okx,ctp +4. use[goplus](https://goplus.org/)as script engine +5. can build strategy to go golang plugin,best performance + +# build + +``` shell +make +``` + +## simple run +``` shell +cd dist +./trade --help +``` + +# Use +## replace your key and secret +replace your key and secret in dist/configs/trade.yaml + +## download history Kline + +``` shell +# run first +./trade download --binSize 1m --start "2020-01-01 08:00:00" --end "2021-01-01 08:00:00" --exchange binance --symbol BTCUSDT +# auto download kline +./trade download --symbol BTCUSDT -a --exchange binance +``` + +## backtest + +``` shell +./trade backtest --script debug.go --start "2020-01-01 08:00:00" --end "2021-01-01 08:00:00" --symbol BTCUSDT --exchange binance +``` + +## real trade + +``` shell +./trade trade --symbol BTCUSDT --exchange binance --script debug.go +``` + + +## strategy +show examples: + +[strategy](https://git.qtrade.icu/coin-quant/strategy) + + +## Thanks + +[goplus](https://goplus.org/) + +[vnpy](https://github.com/vnpy/vnpy) diff --git a/README_cn.md b/README_cn.md new file mode 100644 index 0000000..a418596 --- /dev/null +++ b/README_cn.md @@ -0,0 +1,61 @@ +# trade +希望trade能成为 "你的最后一个交易系统"! + +# Features + +1. 使用go语言来开发/运行策略,不需要其他脚本语言 +2. 基于事件模型,方便扩展 +3. 支持币安,okx,ctp +4. 使用[goplus](https://goplus.org/)作为脚本引擎 +5. 可以将策略编译为go plugin,执行效率高 + +# 编译 + +``` shell +make +``` + +## 运行 +``` shell +cd dist +./trade --help +``` + +# 使用 +## 在配置文件中填写你的secret,key + +## 下载K线历史 + +``` shell +# 首次运行 +./trade download --binSize 1m --start "2020-01-01 08:00:00" --end "2021-01-01 08:00:00" --exchange binance --symbol BTCUSDT +# 自动下载K线 +./trade download --symbol BTCUSDT -a --exchange binance +``` + +## 回测 + +``` shell +./trade backtest --script debug.go --start "2020-01-01 08:00:00" --end "2021-01-01 08:00:00" --symbol BTCUSDT --exchange binance +``` + +## 实盘 + +``` shell +./trade trade --symbol BTCUSDT --exchange binance --script debug.go +``` + + +## 策略 + +策略文档: +[策略](./doc/strategy.md) + +参考例子: +[strategy](https://git.qtrade.icu/coin-quant/strategy) + +## 鸣谢 + +[goplus](https://goplus.org/) + +[vnpy](https://github.com/vnpy/vnpy) diff --git a/cmd/backtest.go b/cmd/backtest.go new file mode 100644 index 0000000..b1055f8 --- /dev/null +++ b/cmd/backtest.go @@ -0,0 +1,119 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/trade/pkg/ctl" + + log "github.com/sirupsen/logrus" + + "git.qtrade.icu/coin-quant/trade/pkg/report" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + scriptFile string + rptFile string + startStr string + endStr string + binSize string + symbol string + exchangeName string + balanceInit float64 + param string + loadOnce int + fee float64 + lever float64 + simpleReport bool + + rptDB string +) + +// backtestCmd represents the backtest command +var backtestCmd = &cobra.Command{ + Use: "backtest", + Short: "backtest with script", + Long: `backtest a script between start and end`, + Run: runBacktest, +} + +func init() { + rootCmd.AddCommand(backtestCmd) + + backtestCmd.PersistentFlags().StringVar(&scriptFile, "script", "", "script file to backtest") + backtestCmd.PersistentFlags().StringVarP(&rptFile, "report", "o", "report.html", "output report html file path") + backtestCmd.PersistentFlags().Float64VarP(&balanceInit, "balance", "", 100000, "init total balance") + backtestCmd.PersistentFlags().StringVar(¶m, "param", "", "param json string") + backtestCmd.PersistentFlags().IntVarP(&loadOnce, "load", "", 50000, "load db once limit") + backtestCmd.PersistentFlags().Float64VarP(&fee, "fee", "", 0.0001, "fee") + backtestCmd.PersistentFlags().Float64VarP(&lever, "lever", "", 1, "lever") + backtestCmd.PersistentFlags().BoolVarP(&simpleReport, "console", "", false, "print report to console") + backtestCmd.PersistentFlags().StringVarP(&rptDB, "reportDB", "d", "", "save all actions to sqlite db") + initTimerange(backtestCmd) +} + +func runBacktest(cmd *cobra.Command, args []string) { + if scriptFile == "" { + log.Fatal("strategy file can't be empty") + return + } + startTime, endTime, err := parseTimerange() + if err != nil { + log.Fatal(err.Error()) + return + } + cfg := viper.GetViper() + db, err := initDB(cfg) + if err != nil { + log.Fatal("init db failed:", err.Error()) + } + + r := report.NewReportSimple() + back, err := ctl.NewBacktest(db, exchangeName, symbol, param, startTime, endTime) + if err != nil { + log.Fatal("init backtest failed:", err.Error()) + } + back.SetScript(scriptFile) + back.SetReporter(r) + back.SetBalanceInit(balanceInit, fee) + back.SetLoadDBOnce(loadOnce) + back.SetLever(lever) + + err = back.Run() + + if err != nil { + fmt.Println("run backtest error", err.Error()) + log.Fatal("run backtest error", err.Error()) + } + if simpleReport { + result, err := r.GetResult() + if err != nil { + return + } + // for _, v := range result.Actions { + // fmt.Println(v.Time, v.Action, v.Amount, v.Price, v.Profit, v.TotalProfit) + // } + buf, err := json.Marshal(result) + if err != nil { + return + } + fmt.Println(string(buf)) + return + } + err = r.GenRPT(rptFile) + if err != nil { + return + } + if rptDB != "" { + err = r.ExportToDB(rptDB) + if err != nil { + fmt.Println("export to DB failed:", err.Error()) + return + } + } + err = common.OpenURL(rptFile) + return +} diff --git a/cmd/build.go b/cmd/build.go new file mode 100644 index 0000000..0548f7a --- /dev/null +++ b/cmd/build.go @@ -0,0 +1,39 @@ +package cmd + +import ( + "fmt" + + "git.qtrade.icu/coin-quant/trade/pkg/ctl" + "github.com/spf13/cobra" +) + +// buildCmd represents the build command +var buildCmd = &cobra.Command{ + Use: "build", + Short: "build script to go plugin", + Long: `"build script to go plugin`, + Run: runBuild, +} + +var ( + output string + keepTemp bool +) + +func init() { + rootCmd.AddCommand(buildCmd) + buildCmd.PersistentFlags().StringVar(&scriptFile, "script", "", "script file to backtest") + buildCmd.PersistentFlags().StringVar(&output, "output", "", "plugin output file") + buildCmd.PersistentFlags().BoolVarP(&keepTemp, "keep", "k", false, "keep temp dir") +} + +func runBuild(cmd *cobra.Command, args []string) { + b := ctl.NewBuilder(scriptFile, output) + b.SetKeepTemp(keepTemp) + err := b.Build() + if err != nil { + fmt.Println("build failed:", err.Error()) + return + } + fmt.Printf("build success: %s\n", output) +} diff --git a/cmd/download.go b/cmd/download.go new file mode 100644 index 0000000..0e84a3d --- /dev/null +++ b/cmd/download.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "fmt" + + "git.qtrade.icu/coin-quant/trade/pkg/ctl" + + log "github.com/sirupsen/logrus" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// downloadCmd represents the download command +var downloadCmd = &cobra.Command{ + Use: "download", + Short: "download data from exchange", + Long: `download data from exchange`, + Run: runDownload, +} + +var ( + bAuto *bool +) + +func init() { + rootCmd.AddCommand(downloadCmd) + initTimerange(downloadCmd) + bAuto = downloadCmd.PersistentFlags().BoolP("auto", "a", false, "auto download") +} + +func runDownload(cmd *cobra.Command, args []string) { + cfg := viper.GetViper() + startTime, endTime, err := parseTimerange() + if err != nil { + log.Fatal(err.Error()) + return + } + db, err := initDB(cfg) + if err != nil { + log.Fatal("init db failed:", err.Error()) + } + var down *ctl.DataDownload + if *bAuto { + down = ctl.NewDataDownloadAuto(cfg, db, exchangeName, symbol, binSize) + } else { + down = ctl.NewDataDownload(cfg, db, exchangeName, symbol, binSize, startTime, endTime) + } + err = down.Run() + if err != nil { + fmt.Println("download data error", err.Error()) + log.Fatal("download data error", err.Error()) + } +} diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..4ca1489 --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "fmt" + "os" + + "git.qtrade.icu/coin-quant/trade/pkg/ctl" + "github.com/olekukonko/tablewriter" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var listCmd = &cobra.Command{ + Use: "list", + Short: "list local datas", + Long: `list local datas`, + Run: runList, +} + +func init() { + rootCmd.AddCommand(listCmd) + +} +func runList(cmd *cobra.Command, args []string) { + cfg := viper.GetViper() + db, err := initDB(cfg) + if err != nil { + fmt.Println("init db failed:", err.Error()) + log.Fatal("init db failed:", err.Error()) + } + l, err := ctl.NewLocalData(db) + if err != nil { + fmt.Println("init localdata failed:", err.Error()) + log.Fatal("init localdata failed:", err.Error()) + } + infos, err := l.ListAll() + if err != nil { + fmt.Println("localdata listAll failed:", err.Error()) + log.Fatal("localdata listAll failed:", err.Error()) + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Exchange", "Symbol", "Binsize", "Start", "End"}) + + for _, v := range infos { + table.Append([]string{v.Exchange, v.Symbol, v.BinSize, v.Start.String(), v.End.String()}) + } + table.Render() +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..c74c251 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,169 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/spf13/cobra" + + "net/http" + _ "net/http/pprof" + + "git.qtrade.icu/coin-quant/trade/pkg/ctl" + "git.qtrade.icu/coin-quant/trade/pkg/process/dbstore" + + _ "git.qtrade.icu/coin-quant/exchange/include" + homedir "github.com/mitchellh/go-homedir" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +var ( + cfgFile string + logFile string + debug bool + runPprof bool + + logF *os.File +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "trade", + Short: "The last trade system you need", + Long: `The last trade system you need. +Trade with all popular exchanges. +Backtest with golang script/plugin`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// RootCmd export RootCmd +func RootCmd() *cobra.Command { + return rootCmd +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + defer func() { + if logF != nil { + logF.Close() + } + }() + if err := rootCmd.Execute(); err != nil { + fmt.Println("run command error:", err.Error()) + } + +} + +func init() { + cobra.OnInitialize(initConfig) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.configs/trade.yaml or ./configs/trade.yaml)") + rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "P", false, "run debug mode") + rootCmd.PersistentFlags().BoolVarP(&runPprof, "pprof", "p", false, "run with pprof mode at :8088") + rootCmd.PersistentFlags().StringVarP(&logFile, "log", "l", "trade.log", "log file") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { + var err error + if logFile != "" { + logF, err = os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + log.Error("open log file failed:", err.Error()) + } else { + log.SetOutput(logF) + } + } + if debug { + log.SetLevel(log.DebugLevel) + } + if !runPprof { + return + } + go func() { + http.ListenAndServe("0.0.0.0:8088", nil) + }() + + } +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Search config in home directory with name ".trade" (without extension). + viper.AddConfigPath(filepath.Join(home, ".configs")) + viper.AddConfigPath("./configs") + ex, err := os.Executable() + if err != nil { + panic(err) + } + exPath := filepath.Dir(ex) + viper.AddConfigPath(filepath.Join(exPath, "configs")) + viper.SetConfigName("trade") + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + ctl.SetConfig(viper.GetViper()) + } +} + +func initTimerange(cmd *cobra.Command) { + cmd.PersistentFlags().StringVarP(&startStr, "start", "s", "2019-01-01 10:00:00", "start time") + cmd.PersistentFlags().StringVarP(&endStr, "end", "e", "", "end time") + cmd.PersistentFlags().StringVarP(&binSize, "binSize", "b", "1m", "binSize: 1m,5m,15m,1h,1d") + cmd.PersistentFlags().StringVar(&symbol, "symbol", "BTCUSDT", "symbol") + cmd.PersistentFlags().StringVar(&exchangeName, "exchange", "binance", "exchage name, support binance,okex current now") +} + +func parseTimerange() (startTime, endTime time.Time, err error) { + if startStr == "" { + err = errors.New("start/end time can't be empty") + return + } + startTime, err = time.Parse("2006-01-02 15:04:05", startStr) + if err != nil { + err = errors.New("parse start time error") + return + } + if endStr == "" { + endTime = time.Now() + return + } + endTime, err = time.Parse("2006-01-02 15:04:05", endStr) + if err != nil { + err = errors.New("parse end time error") + return + } + return +} + +func initDB(cfg *viper.Viper) (db *dbstore.DBStore, err error) { + db, err = dbstore.LoadDB(cfg) + return +} diff --git a/cmd/trade.go b/cmd/trade.go new file mode 100644 index 0000000..dd3f82f --- /dev/null +++ b/cmd/trade.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/trade/pkg/ctl" + "git.qtrade.icu/coin-quant/trade/pkg/report" + log "github.com/sirupsen/logrus" + + "github.com/spf13/cobra" +) + +// tradeCmd represents the trade command +var tradeCmd = &cobra.Command{ + Use: "trade", + Short: "trade with script", + Long: `trade with script`, + Run: runTrade, +} + +var ( + recentDay int +) + +func init() { + rootCmd.AddCommand(tradeCmd) + tradeCmd.PersistentFlags().StringVar(&scriptFile, "script", "", "script file to backtest") + tradeCmd.PersistentFlags().StringVarP(&rptFile, "report", "o", "report.html", "output report html file path") + tradeCmd.PersistentFlags().StringVarP(&binSize, "binSize", "b", "1m", "binSize: 1m,5m,15m,1h,1d") + tradeCmd.PersistentFlags().StringVar(&symbol, "symbol", "XBTUSD", "symbol") + tradeCmd.PersistentFlags().StringVar(&exchangeName, "exchange", "bitmex", "exchage name, only support bitmex current now") + tradeCmd.PersistentFlags().IntVarP(&recentDay, "recent", "r", 1, "load recent (n) day datas,default 1") + tradeCmd.PersistentFlags().StringVar(¶m, "param", "", "param json string") +} + +func runTrade(cmd *cobra.Command, args []string) { + if scriptFile == "" { + log.Fatal("strategy file can't be empty") + return + } + var gracefulStop = make(chan os.Signal) + signal.Notify(gracefulStop, syscall.SIGTERM) + signal.Notify(gracefulStop, syscall.SIGINT) + real, err := ctl.NewTrade(exchangeName, symbol) + if recentDay != 0 { + real.SetLoadRecent(time.Duration(recentDay) * time.Hour * 24) + } + r := report.NewReportSimple() + real.SetReporter(r) + paramData := make(map[string]interface{}) + if param != "" { + err = json.Unmarshal([]byte(param), ¶mData) + if err != nil { + log.Fatal("param error:", err.Error()) + } + } + err = real.AddScript(filepath.Base(scriptFile), scriptFile, param) + if err != nil { + fmt.Println("AddScript failed:", err.Error()) + return + } + // real.SetScript(scriptFile) + go func() { + sig := <-gracefulStop + fmt.Printf("caught sig: %+v", sig) + real.Stop() + }() + err = real.Start() + if err != nil { + log.Fatal("trade error:", err.Error()) + } + real.Wait() + fmt.Println("begin to geneate report to ", rptFile) + err = r.GenRPT(rptFile) + if err != nil { + return + } + fmt.Println("open report ", rptFile) + err = common.OpenURL(rptFile) + return +} diff --git a/configs/trade.yaml.bak b/configs/trade.yaml.bak new file mode 100644 index 0000000..993cae4 --- /dev/null +++ b/configs/trade.yaml.bak @@ -0,0 +1,12 @@ +exchanges: + binance: + type: binance + key: 1111111 + secret: 222222 +proxy: socks5://127.0.0.1:7891 +db: + type: mysql + uri: root:123456@tcp(127.0.0.1:3306)/exchange +#plugins: +# - type: exchange +# uri: plugins/ctp.so diff --git a/doc/strategy.md b/doc/strategy.md new file mode 100644 index 0000000..b3b3d3e --- /dev/null +++ b/doc/strategy.md @@ -0,0 +1,217 @@ +# 策略说明 + +## 创建策略项目 +创建策略项目可以直接使用模板,也可以全部手动创建 +### 直接使用模板 +``` +git clone https://git.qtrade.icu/coin-quant/strategy +cd strategy +# 添加自定义策略 +``` + +### 手动创建项目 + +``` +mkdir strategy +cd strategy +go mod init strategy +# 复制 helper.go,并且修改package +cp $SRC/pkg/helper/helper.go ./define.go +# 创建新的策略 +touch demo.go +``` + + +## 策略 +没一个策略,都是一个go的struct,这个struct一般长这个样子: + +``` +package strategy + +import ( + . "https://git.qtrade.icu/coin-quant/trademodel" +) + +type Demo struct { + engine Engine // 这个是和引擎交互用的 + + position float64 //仓位 + + strParam string + intParam int + floatParam float64 +} + +// 这个是策略的创建函数,必须是下面这种格式: func New{struct}() *{struct} +func NewDemo() *Demo { + return new(Demo) +} + +// 这个函数提供策略需要的参数列表,供trade引擎调用 +// trade 可以通过在命令行直接输入参数的方式,来传递参数 +func (d *Demo) Param() []Param { + return []Param{ + // 这个函数有 5个参数: + // 1. 这个Param的key + // 2. 这个Param的中文简称 + // 3. 这个Param的具体解释 + // 4. 这个Param的默认值 + // 5. 这个Param对应的变量的的指针 + StringParam("str", "字符串参数", "只是一个简单的参数", "15m", &d.strParam), + IntParam("intparam", "数字参数", "一个简单的数字参数 ", 12, &d.intParam), + FloatParam("floatparam", "浮点参数", "简单的浮点参数", 1, &d.floatParam), + } +} + +// 这个函数是在策略初始化的时候调用的 +// engine就是trade引擎的接口 +// params是传入的参数信息 +func (d *Demo) Init(engine Engine, params ParamData) { + // 这里 d.strParam,d.intParam,d.floatParam已经自动解析了,无需再次解析 + d.engine = engine + // 合并K线,第一个参数是原始K线级别,第二个参数是目标级别,第三个参数是K线合并完成后的回调函数 + engine.Merge("1m", "30m", d.OnCandle30m) + // 合并K线,第一个参数是原始K线级别,第二个参数是目标级别,第三个参数是K线合并完成后的回调函数 + engine.Merge("1m", "1h", d.OnCandle1h) +} + +// 1m K线回调函数 +// 在回测中,candle.ID是数据库中的ID +// 在实盘中 candle.ID=-1表示是历史数据,非-1表示正常的实时K线 +func (d *Demo) OnCandle(candle *Candle) { +} + +// 仓位同步函数, pos 表示仓位, 正数表示多仓,负数表示空仓,price是开仓的价格 +func (d *Demo) OnPosition(pos, price float64) { + d.position = pos +} + +// 自己的订单成交时候的回调函数 +func (d *Demo) OnTrade(trade *Trade) { + +} + +// 交易所中实时的成交信息 +func (d *Demo) OnTradeMarket(trade *Trade) { + +} + +// 交易所中实时推送的深度信息,根据交易所限制不同,深度信息也不同 +func (d *Demo) OnDepth(depth *Depth) { +} + +// Init函数中定义的 30m K线回调函数 +// 在回测中,candle.ID是数据库中的ID +// 在实盘中 candle.ID=-1表示是历史数据,非-1表示正常的实时K线 +func (d *Demo) OnCandle30m(candle *Candle) { +} + +// Init函数中定义的 1h K线回调函数 +// 在回测中,candle.ID是数据库中的ID +// 在实盘中 candle.ID=-1表示是历史数据,非-1表示正常的实时K线 +func (d *Demo) OnCandle1h(candle *Candle) { + // 自定义判断逻辑 + //... + + // 可以在策略启动后的任何地方调用交易函数 + // OpenLong,CloseLong,OpenShort,CloseShort,StopLong,StopShort... + d.engine.OpenLong(candle.Close, 1) +} + +``` + +## Engine说明 +Engine的定义如下: + +``` +const ( + // 策略运行中 + StatusRunning = 0 + // 策略成功 + StatusSuccess = 1 + // 策略失败 + StatusFail = -1 +) + +type Engine interface { + // 开多,返回order id + OpenLong(price, amount float64) string + // 平多,返回order id + CloseLong(price, amount float64) string + // 开空,返回order id + OpenShort(price, amount float64) string + // 平空,返回order id + CloseShort(price, amount float64) string + // 发送 多单止损 订单,返回order id + StopLong(price, amount float64) string + // 发送 空单止损 订单,返回order id + StopShort(price, amount float64) string + // 取消某个订单,参数是order id + CancelOrder(string) + // 取消所有订单 + CancelAllOrder() + // 执行订单,一般调用上面的就可以了,不用调用这个 + DoOrder(typ trademodel.TradeType, price, amount float64) string + // 添加指标,指标具体文档在下面 + AddIndicator(name string, params ...int) (ind indicator.CommonIndicator) + // 获取当前的仓位 + Position() (pos, price float64) + // 获取当前的余额 + Balance() float64 + // 日志 + Log(v ...interface{}) + // 添加新的订阅事件,当前无需调用 + Watch(watchType string) + // 发送消息通知,需要添加消息类型的processer才会生效 + SendNotify(title, content, contentType string) + // 合并K线,src是原始级别,这里固定是1m,dst是目标级别,fn是回调函数 + Merge(src, dst string, fn common.CandleFn) + // 设置余额,仅在回测时有用 + SetBalance(balance float64) + // 更新状态,一般无需调用,状态说明见上面的定义 + UpdateStatus(status int, msg string) +} + +``` + +## 指标说明 +trade内置了一些常见的指标,代码详见 [indicator](https://git.qtrade.icu/coin-quant/indicator) + +| 名称 | 说明 | 参数 | 例子 | +|----------|---------------------------|---------------------------------------------------|-----------------------------------------------------------| +| EMA | 只有一个参数表示是一根EMA | 数字 | AddIndicator("EMA", 9) 长度为9的EMA | +| EMA | 两个参数表示EMA交叉指标 | 两个数字:快线、慢线 | AddIndicator("EMA", 9, 26) 长度为9的EMA和长度为26的EMA | +| MACD | 标准的MACD | 三个数字:快线、慢线、dea | AddIndicator("macd",12,26,9) | +| SMAMACD | 使用SMA代替EMA计算的MACD | 三个数字:快线、慢线、dea | AddIndicator("macd",12,26,9) | +| SMA | 只有一个参数表示一根SMA | 数字 | AddIndicator("SMA", 9) 长度为9的SMA | +| SMA | 两个参数表示SMA交叉指标 | 两个数字:快线、慢线 | AddIndicator("SMA", 9, 26) 长度为9的SMA和长度为26的SMA | +| SSMA | 只有一个参数表示一根SSMA | 数字 | AddIndicator("SSMA", 9) 长度为9的SSMA | +| SSMA | 两个参数表示SSMA交叉指标 | 两个数字:快线、慢线 | AddIndicator("SSMA", 9, 26) 长度为9的SSMA和长度为26的SSMA | +| STOCHRSI | 随机相对强弱指数 | 4个参数:STOCH窗口长度、RSI窗口长度、平滑k、平滑d | AddIndicator("STOCHRSI", 14,14,3,3) | +| RSI | 只有一个参数表示一根RSI | 数字 | AddIndicator("RSI", 9)表示一根长度是9的RSI | +| RSI | 两个参数表示RSI交叉指标 | 两个数字:快线、慢线 | AddIndicator("RSI", 9, 26)长度为9的RSI和长度为26的RSI | +| BOLL | BOLL指标 | 两个参数:长度、多元 | AddIndicator("BOLL", 20,2) | + +### 返回值 CommonIndicator 说明 + +1. 如果是单根指标线,Result()返回当前的值,如果是两个指标线,则返回的是快线的当前值 +2. Indicator() 返回一个map + 针对有两根线的 EMA/SMA/SSMA/RSI +``` +result: 同Result()的值 +fast: 快线的值 +slow: 慢线的值 +crossUp: 1 表示金叉, 0表示没有金叉 +crossDown: 1 表示死叉, 0 表示没有死叉 +``` + +boll指标 + +``` +result: 同Result()的值,表示中间的均线的值 +top: 上边线的值 +bottom: 下边线的值 +``` + + +MACD/SMAMACD/STOCHRSI用这种方法只能获取到Result的值,建议直接使用 NewMACD/NewMACDWithSMA/NewSTOCHRSI方法 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f967220 --- /dev/null +++ b/go.mod @@ -0,0 +1,89 @@ +module git.qtrade.icu/coin-quant/trade + +go 1.22.0 + +require ( + git.qtrade.icu/coin-quant/base v0.0.0-20240625154116-db5c83a1dd97 + git.qtrade.icu/coin-quant/exchange v0.0.0-20240625155123-430837ac683b + git.qtrade.icu/coin-quant/indicator v0.0.0-20240625151736-c23020eee562 + git.qtrade.icu/coin-quant/trademodel v0.0.0-20240625151548-cef4b6fc28b9 + github.com/go-sql-driver/mysql v1.8.1 + github.com/goplus/igop v0.25.0 + github.com/json-iterator/go v1.1.12 + github.com/lib/pq v1.10.9 + github.com/mitchellh/go-homedir v1.1.0 + github.com/montanaflynn/stats v0.7.1 + github.com/olekukonko/tablewriter v0.0.5 + github.com/shopspring/decimal v1.4.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + github.com/tidwall/gjson v1.17.1 + golang.org/x/mod v0.18.0 + golang.org/x/tools v0.22.0 + modernc.org/sqlite v1.30.1 + xorm.io/xorm v1.3.9 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/adshao/go-binance/v2 v2.4.5 // indirect + github.com/bitly/go-simplejson v0.5.0 // indirect + github.com/deepmap/oapi-codegen v1.10.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/getkin/kin-openapi v0.94.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/swag v0.21.1 // indirect + github.com/goccy/go-json v0.9.6 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect + github.com/goplus/reflectx v1.2.2 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/timandy/routine v1.1.1 // indirect + github.com/visualfc/funcval v0.1.4 // indirect + github.com/visualfc/gid v0.1.0 // indirect + github.com/visualfc/goembed v0.3.2 // indirect + github.com/visualfc/xtype v0.2.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.52.1 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect + xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d02a525 --- /dev/null +++ b/go.sum @@ -0,0 +1,416 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.qtrade.icu/coin-quant/base v0.0.0-20240625154116-db5c83a1dd97 h1:3Mx0bvsodQg0XgQbFZEUTJ8KFOW2yXMBM25qjHDXNf0= +git.qtrade.icu/coin-quant/base v0.0.0-20240625154116-db5c83a1dd97/go.mod h1:UGO6cehLrOO0xEgEzVosvlakoTyxPz1rpvUtT7WDN8M= +git.qtrade.icu/coin-quant/exchange v0.0.0-20240625155123-430837ac683b h1:sEQBCgRiHRqUcskW7Lz+2E08LfY7dUbccpgiUKhQ9ao= +git.qtrade.icu/coin-quant/exchange v0.0.0-20240625155123-430837ac683b/go.mod h1:xlZzC4YB10wTfDL3OvmJEjYpaQ5Nl7gphE/63gaoShM= +git.qtrade.icu/coin-quant/indicator v0.0.0-20240625151736-c23020eee562 h1:oA06Mq/hJtzJ6k7ZW6kd3RY9EBDLVCEPDFADiP9JwIk= +git.qtrade.icu/coin-quant/indicator v0.0.0-20240625151736-c23020eee562/go.mod h1:x1+rqPrwJqPLETFdMQGhzp71Z3ZxAlNFExGVOhk+IT0= +git.qtrade.icu/coin-quant/trademodel v0.0.0-20240625151548-cef4b6fc28b9 h1:9T1u+MzfbG9jZU1wzDtmBoOwN1m/fRX0iX7NbLwAHgU= +git.qtrade.icu/coin-quant/trademodel v0.0.0-20240625151548-cef4b6fc28b9/go.mod h1:SZnI+IqcRlKVcDSS++NIgthZX4GG1OU4UG+RDrSOD34= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= +github.com/adshao/go-binance/v2 v2.4.5 h1:V3KpolmS9a7TLVECSrl2gYm+GGBSxhVk9ILaxvOTOVw= +github.com/adshao/go-binance/v2 v2.4.5/go.mod h1:41Up2dG4NfMXpCldrDPETEtiOq+pHoGsFZ73xGgaumo= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/deepmap/oapi-codegen v1.10.1 h1:xybuJUR6D8l7P+LAuxOm5SD7nTlFKHWvOPl31q+DDVs= +github.com/deepmap/oapi-codegen v1.10.1/go.mod h1:TvVmDQlUkFli9gFij/gtW1o+tFBr4qCHyv2zG+R0YZY= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/getkin/kin-openapi v0.94.0 h1:bAxg2vxgnHHHoeefVdmGbR+oxtJlcv5HsJJa3qmAHuo= +github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.9.6 h1:5/4CtRQdtsX0sal8fdVhTaiMN01Ri8BExZZ8iRmHQ6E= +github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20180708170036-38b413be4187/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/goplus/c2go v0.7.26/go.mod h1:ePAStubV/ls8mmdPGQo6VfADTVd46rKuBemE4zzBDnA= +github.com/goplus/gogen v1.15.2/go.mod h1:92qEzVgv7y8JEFICWG9GvYI5IzfEkxYdsA1DbmnTkqk= +github.com/goplus/gop v1.2.6/go.mod h1:uREWbR1MrFaviZ4Mbx4ZCcAYDoqzO0iv1Qo6Np0Xx4E= +github.com/goplus/igop v0.25.0 h1:DDc66JmkghgZ+EMGm6LoN6WhsJgKJa+AswG9zTOGsIk= +github.com/goplus/igop v0.25.0/go.mod h1:V8Kf/b4nrw0OPPodwnOYZPCpXvU+hqzuhSAXIToT0ME= +github.com/goplus/mod v0.13.10/go.mod h1:HDuPZgpWiaTp3PUolFgsiX+Q77cbUWB/mikVHfYND3c= +github.com/goplus/reflectx v1.2.2 h1:T1p20OIH/HcnAvQQNnDLwl6AZOjU34icsfc6migD6L8= +github.com/goplus/reflectx v1.2.2/go.mod h1:wHOS9ilbB4zrecI0W1dMmkW9JMcpXV7VjALVbNU9xfM= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= +github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.23/go.mod h1:sAXjRwzSvCN6soO4RLoWWm1bVPpb8iOuv0IYfH8OWd8= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/qiniu/x v1.13.10/go.mod h1:INZ2TSWSJVWO/RuELQROERcslBwVgFG7MkTfEdaQz9E= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/timandy/routine v1.1.1 h1:6/Z7qLFZj3GrzuRksBFzIG8YGUh8CLhjnnMePBQTrEI= +github.com/timandy/routine v1.1.1/go.mod h1:OZHPOKSvqL/ZvqXFkNZyit0xIVelERptYXdAHH00adQ= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/visualfc/funcval v0.1.4 h1:lAI88zQYfRzmC7mKF4+swXeCZvb8wb1f3lMSDRAY2mQ= +github.com/visualfc/funcval v0.1.4/go.mod h1:3Izv+irhArmrTvy+lmL6pIq16gSOzx73CIka51J9eR0= +github.com/visualfc/gid v0.1.0 h1:sg95vChDrUS5hngkfg4yxN6JAF6qIwwvJ+TC8cuZVto= +github.com/visualfc/gid v0.1.0/go.mod h1:YiRDLgdRp89d4OduAkhv5AAtc6XjUlx74IRLYofu3+E= +github.com/visualfc/goembed v0.3.2 h1:a9m6o9VTzNk3mEF98C8cHp8f8P8BblyjsjajXGfTp8w= +github.com/visualfc/goembed v0.3.2/go.mod h1:jCVCz/yTJGyslo6Hta+pYxWWBuq9ADCcIVZBTQ0/iVI= +github.com/visualfc/xtype v0.2.0 h1:0ESNXyWHtK01kaOzOyqHsR1ZjEPdNu/IWPZkf0VOHl8= +github.com/visualfc/xtype v0.2.0/go.mod h1:183MDtzLqyDkCm5zCH42vJGq/aQE5W25k3Z6UOZxLF0= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk= +modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.17.10 h1:6wrtRozgrhCxieCeJh85QsxkX/2FFrT9hdaWPlbn4Zo= +modernc.org/ccgo/v4 v4.17.10/go.mod h1:0NBHgsqTTpm9cA5z2ccErvGZmtntSM9qD2kFAs6pjXM= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M= +modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk= +modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 h1:bvLlAPW1ZMTWA32LuZMBEGHAUOcATZjzHcotf3SWweM= +xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= +xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU= +xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d622dc1 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "git.qtrade.icu/coin-quant/trade/cmd" + +func main() { + cmd.Execute() +} diff --git a/pkg/core/events.go b/pkg/core/events.go new file mode 100644 index 0000000..1fbc7cf --- /dev/null +++ b/pkg/core/events.go @@ -0,0 +1,121 @@ +package core + +import ( + "fmt" + "reflect" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/tidwall/gjson" +) + +// Events +const ( + EventCandle = "candle" + EventOrder = "order" + // own trades + EventTrade = "trade" + EventPosition = "position" + EventCurPosition = "cur_position" // position of current script + EventRiskLimit = "risk_limit" + EventDepth = "depth" + // all trades in the markets + EventTradeMarket = "trade_market" + + EventBalance = "balance" + EventBalanceInit = "balance_init" + + EventWatch = "watch" + EventWatchCandle = "watch_candle" + + EventNotify = "notify" + + EventError = "error" +) + +var ( + EventTypes = map[string]reflect.Type{ + EventCandle: reflect.TypeOf(Candle{}), + EventOrder: reflect.TypeOf(TradeAction{}), + // EventOrderCancelAll = "order_cancel_all" + EventTrade: reflect.TypeOf(Trade{}), + EventPosition: reflect.TypeOf(Position{}), + // EventCurPosition = "cur_position" // position of current script + // EventRiskLimit = "risk_limit" + EventDepth: reflect.TypeOf(Depth{}), + EventTradeMarket: reflect.TypeOf(Trade{}), + EventBalance: reflect.TypeOf(Balance{}), + EventBalanceInit: reflect.TypeOf(BalanceInfo{}), + EventWatch: reflect.TypeOf(WatchParam{}), + EventNotify: reflect.TypeOf(NotifyEvent{}), + EventWatchCandle: reflect.TypeOf(CandleParam{}), + } + + json = jsoniter.ConfigCompatibleWithStandardLibrary +) + +// CandleParam get candle param +type CandleParam struct { + Start time.Time + End time.Time + Exchange string + BinSize string + Symbol string +} + +// NotifyEvent event to send notify +type NotifyEvent struct { + Type string // text,markdown + Title string + Content string +} + +// RiskLimit risk limit +type RiskLimit struct { + Code string // symbol info, empty = global + Lever float64 // lever + MaxLostRatio float64 // max lose ratio +} + +// Key key of r +func (r RiskLimit) Key() string { + return fmt.Sprintf("%s-%.2f", r.Code, r.Lever) +} + +type EventData struct { + Type string `json:"type"` + Data interface{} `json:"data"` + Extra interface{} `json:"extra"` +} + +// UnmarshalJSON EventData can't be used as Embed +func (d *EventData) UnmarshalJSON(buf []byte) (err error) { + ret := gjson.ParseBytes(buf) + d.Type = ret.Get("type").String() + typ, ok := EventTypes[d.Type] + if ok { + d.Data = reflect.New(typ).Interface() + } else { + d.Data = map[string]interface{}{} + } + err = json.Unmarshal([]byte(ret.Get("data").Raw), d.Data) + return +} + +// WatchParam add watch event param +type WatchParam = EventData + +func NewWatchCandle(cp *CandleParam) *WatchParam { + wp := &WatchParam{ + Type: EventWatchCandle, + Data: cp, + Extra: cp.Symbol, + } + return wp +} + +// BalanceInfo balance +type BalanceInfo struct { + Balance float64 + Fee float64 +} diff --git a/pkg/core/risk.go b/pkg/core/risk.go new file mode 100644 index 0000000..798d506 --- /dev/null +++ b/pkg/core/risk.go @@ -0,0 +1,34 @@ +package core + +// RiskLimits risk limits +type RiskLimits map[string]RiskLimit + +func NewRiskLimits() (rl RiskLimits) { + rl = make(RiskLimits) + return rl +} + +func (rl RiskLimits) Update(limit RiskLimit) { + rl[limit.Key()] = limit +} + +func (rl RiskLimits) GetLimitRatio(limit RiskLimit) (ret float64) { + l, ok := rl[limit.Key()] + if ok { + ret = l.MaxLostRatio + return + } + limit.Lever = 0 + l, ok = rl[limit.Key()] + if ok { + ret = l.MaxLostRatio + return + } + limit.Code = "" + l, ok = rl[limit.Key()] + if ok { + ret = l.MaxLostRatio + return + } + return +} diff --git a/pkg/core/symbol.go b/pkg/core/symbol.go new file mode 100644 index 0000000..eec4b3f --- /dev/null +++ b/pkg/core/symbol.go @@ -0,0 +1,16 @@ +package core + +import "strings" + +// SymbolInfo symbol infos +type SymbolInfo struct { + ID int64 `xorm:"pk autoincr null 'id'"` + Exchange string `xorm:"notnull unique(esr) 'exchange'"` + Symbol string `xorm:"notnull unique(esr) 'symbol'"` + Resolutions string `xorm:"notnull unique(esr) 'resolutions'"` + Pricescale int `xorm:"notnull 'pricescale'"` +} + +func (si *SymbolInfo) GetResolutions() []string { + return strings.Split(si.Resolutions, ",") +} diff --git a/pkg/core/watch.go b/pkg/core/watch.go new file mode 100644 index 0000000..762e4f7 --- /dev/null +++ b/pkg/core/watch.go @@ -0,0 +1,9 @@ +package core + +// CandleInfo candle data with symbol info +type CandleInfo struct { + Exchange string + Symbol string + BinSize string + Data interface{} +} diff --git a/pkg/ctl/back.go b/pkg/ctl/back.go new file mode 100644 index 0000000..f5f7c69 --- /dev/null +++ b/pkg/ctl/back.go @@ -0,0 +1,172 @@ +package ctl + +import ( + "errors" + "sync" + "time" + + "git.qtrade.icu/coin-quant/base/common" + + "git.qtrade.icu/coin-quant/trade/pkg/event" + "git.qtrade.icu/coin-quant/trade/pkg/process/dbstore" + "git.qtrade.icu/coin-quant/trade/pkg/process/rpt" + "git.qtrade.icu/coin-quant/trade/pkg/process/vex" + + log "github.com/sirupsen/logrus" +) + +type Backtest struct { + progress int + exchange string + symbol string + paramData string + start time.Time + end time.Time + running bool + stop chan bool + db *dbstore.DBStore + scriptFile string + rpt rpt.Reporter + balanceInit float64 + loadDBOnce int + fee float64 + lever float64 + + closeAllWhenFinished bool +} + +// NewBacktest constructor of Backtest +func NewBacktest(db *dbstore.DBStore, exchange, symbol, param string, start time.Time, end time.Time) (b *Backtest, err error) { + b = new(Backtest) + b.start = start + b.end = end + b.exchange = exchange + b.symbol = symbol + b.db = db + b.balanceInit = 100000 + b.loadDBOnce = 50000 + b.paramData = param + return +} + +func (b *Backtest) CloseAllWhenFinished(bCloseAll bool) { + b.closeAllWhenFinished = bCloseAll +} + +func (b *Backtest) SetLoadDBOnce(loadOnce int) { + b.loadDBOnce = loadOnce +} + +func (b *Backtest) SetBalanceInit(balanceInit, fee float64) { + b.balanceInit = balanceInit + b.fee = fee +} + +func (b *Backtest) SetLever(lever float64) { + b.lever = lever +} + +func (b *Backtest) SetScript(scriptFile string) { + b.scriptFile = scriptFile +} + +func (b *Backtest) SetReporter(rpt rpt.Reporter) { + b.rpt = rpt +} + +// Start start backtest +func (b *Backtest) Start() (err error) { + b.running = true + go b.Run() + return +} + +// Stop stop backtest +func (b *Backtest) Stop() (err error) { + b.stop <- true + return +} + +// Run !TODO need support multi binsizes +func (b *Backtest) Run() (err error) { + defer func() { + b.running = false + }() + closeCh := make(chan bool) + param := event.NewBaseProcesser("param") + bSize := "1m" + tbl := b.db.NewKlineTbl(b.exchange, b.symbol, bSize) + tbl.SetLoadOnce(b.loadDBOnce) + tbl.SetLoadDataMode(true) + tbl.SetCloseCh(closeCh) + ex := vex.NewVExchange(b.symbol) + engine, err := NewScript(b.scriptFile, b.paramData, b.symbol) + if err != nil { + return + } + r := rpt.NewRpt(b.rpt) + processers := event.NewSyncProcessers() + processers.Add(param) + processers.Add(tbl) + processers.Add(ex) + processers.Add(engine) + processers.Add(r) + + var stopOnce sync.Once + errorCh := make(chan bool) + processers.SetErrorCallback(func(err error) { + if errors.Is(err, common.ErrNoBalance) { + stopOnce.Do(func() { + log.Errorf("got error: %s, just exit", err.Error()) + processers.Stop() + errorCh <- true + }) + } + }) + + err = processers.Start() + if err != nil { + return + } + + param.Send("balance_init", EventBalanceInit, &BalanceInfo{Balance: b.balanceInit, Fee: b.fee}) + param.Send("risk_init", EventRiskLimit, &RiskLimit{Lever: b.lever}) + candleParam := CandleParam{ + Start: b.start, + End: b.end, + Symbol: b.symbol, + BinSize: bSize, + } + + log.Info("backtest candle param:", candleParam) + param.Send("load_candle", EventWatch, NewWatchCandle(&candleParam)) + // TODO wait for finish + select { + case <-closeCh: + case <-errorCh: + // FIXME: tbl maybe not close + } + if b.closeAllWhenFinished { + time.Sleep(time.Second * 10) + ex.CloseAll() + } + processers.WaitClose(time.Second * 10) + return +} + +// Progress return the progress of current backtest +func (b *Backtest) Progress() (progress int) { + return b.progress +} + +// IsRunning return if the backtest is running +func (b *Backtest) IsRunning() (ret bool) { + return b.running +} + +// Result return the result of current backtest +// must call after end of the backtest +func (b *Backtest) Result() (err error) { + + return +} diff --git a/pkg/ctl/build.go b/pkg/ctl/build.go new file mode 100644 index 0000000..3bf770b --- /dev/null +++ b/pkg/ctl/build.go @@ -0,0 +1,207 @@ +package ctl + +import ( + _ "embed" + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime/debug" + "strings" + "text/template" + + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/trade/pkg/process/goscript/engine" + + "golang.org/x/mod/modfile" + "golang.org/x/mod/module" +) + +var ( + buildInfo *debug.BuildInfo + + //go:embed tmpl/define.go + defineGo string + //go:embed tmpl/export.go + exportGo string +) + +type Builder struct { + source string + output string + debugMode bool + keepTemp bool +} + +func NewBuilder(src, output string) *Builder { + b := new(Builder) + b.source = src + b.output = output + return b +} + +func (b *Builder) SetKeepTemp(keepTemp bool) { + b.keepTemp = keepTemp +} + +func (b *Builder) Build() (err error) { + if b.source == "" { + err = errors.New("strategy file can't be empty") + return + } + if b.output == "" { + b.output = strings.Replace(b.source, ".go", ".so", 1) + } + baseName := filepath.Base(b.source) + dir := baseName[0 : len(baseName)-len(filepath.Ext(b.source))] + tempDir, err := ioutil.TempDir("", dir) + if err != nil { + err = fmt.Errorf("create temp dir failed: %w", err) + return + } + defer func() { + if !b.keepTemp { + os.RemoveAll(tempDir) + } + }() + err = common.CopyWithMainPkg(filepath.Join(tempDir, baseName), b.source) + if err != nil { + err = fmt.Errorf("copy file failed: %w", err) + return + } + err = ioutil.WriteFile(filepath.Join(tempDir, "define.go"), []byte(defineGo), 0644) + // err = common.CopyWithMainPkg(filepath.Join(tempDir, "define.go"), filepath.Join(common.GetExecDir(), "tmpl", "define.go")) + if err != nil { + err = fmt.Errorf("write tmpl file define.go failed: %w", err) + return + } + runner, err := engine.NewRunner(b.source) + if err != nil { + err = fmt.Errorf("export.go error: %w", err.Error()) + return + } + + // fTmpl := filepath.Join(common.GetExecDir(), "tmpl", "export.go") + // tmpl, err := template.ParseFiles(fTmpl) + tmpl, err := template.New("export").Parse(exportGo) + if err != nil { + err = fmt.Errorf("export.go error: %w", err) + return + } + fExport, err := os.Create(filepath.Join(tempDir, "export.go")) + if err != nil { + err = fmt.Errorf("create export.go error: %w", err) + return + } + err = tmpl.Execute(fExport, map[string]string{"Name": runner.GetName()}) + if err != nil { + err = fmt.Errorf("create export.go error: %w", err) + return + } + e := exec.Command("go", "mod", "init", dir) + e.Dir = tempDir + err = e.Run() + if err != nil { + err = fmt.Errorf("run command failed: %w", err) + return + } + if b.keepTemp { + fmt.Println("temp dir:", tempDir) + } + dst, _ := filepath.Abs(b.output) + runGoGet := true + var output []byte + for i := 0; i != 2; i++ { + if i == 1 { + runGoGet, err = b.fixGoMod(tempDir) + if err != nil { + err = fmt.Errorf("fixGoMod failed: %w", err) + return + } + if !runGoGet { + break + } + } + + eBuildGet := exec.Command("go", "get", "-v") + eBuildGet.Dir = tempDir + output, err = eBuildGet.CombinedOutput() + if err != nil { + err = fmt.Errorf("run go get command failed: %w, %s", err, string(output)) + return + } + } + + eBuild := exec.Command("go", "build", "--buildmode=plugin", "-o", dst) + eBuild.Dir = tempDir + output, err = eBuild.CombinedOutput() + if err != nil { + err = fmt.Errorf("run build command failed: %w, %s", err, string(output)) + return + } + return +} + +func (b *Builder) fixGoMod(dir string) (hasFixed bool, err error) { + gomod := filepath.Join(dir, "go.mod") + f, err := os.Open(gomod) + if err != nil { + return + } + buf, err := ioutil.ReadAll(f) + if err != nil { + f.Close() + return + } + f.Close() + mf, err := modfile.Parse("go.mod", buf, nil) + if err != nil { + return + } + var ver string + var replaceModPaths []module.Version + for _, v := range mf.Require { + ver = fixRequireVersion(v.Mod.Path) + if ver != "" && ver != v.Mod.Version { + replaceModPaths = append(replaceModPaths, module.Version{Path: v.Mod.Path, Version: ver}) + } + } + for _, v := range replaceModPaths { + mf.AddRequire(v.Path, v.Version) + if b.debugMode { + fmt.Println("fix path version:", v.Path, v.Version) + } + } + + buf, err = mf.Format() + if err != nil { + return + } + f, err = os.OpenFile(gomod, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.ModePerm) + if err != nil { + return + } + _, err = f.Write(buf) + f.Close() + hasFixed = true + return +} + +func fixRequireVersion(modPath string) (ver string) { + for _, v := range buildInfo.Deps { + if v.Path == modPath { + return v.Version + } + } + return +} + +func init() { + var ok bool + buildInfo, ok = debug.ReadBuildInfo() + if !ok { + panic("read build info failed") + } +} diff --git a/pkg/ctl/download.go b/pkg/ctl/download.go new file mode 100644 index 0000000..2c4955e --- /dev/null +++ b/pkg/ctl/download.go @@ -0,0 +1,163 @@ +package ctl + +import ( + "fmt" + "time" + + "git.qtrade.icu/coin-quant/exchange" + "git.qtrade.icu/coin-quant/trade/pkg/process/dbstore" + "git.qtrade.icu/coin-quant/trademodel" + + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +type DataDownload struct { + exchange string + start time.Time + end time.Time + binSize string + symbol string + running bool + stop chan bool + bInit bool + db *dbstore.DBStore + cfg *viper.Viper + isAuto bool +} + +// NewDataDownloadAuto constructor of DataDownload +func NewDataDownloadAuto(cfg *viper.Viper, db *dbstore.DBStore, exchange, symbol, binSize string) (d *DataDownload) { + d = new(DataDownload) + d.cfg = cfg + d.exchange = exchange + d.symbol = symbol + d.binSize = binSize + d.db = db + d.isAuto = true + return +} + +// NewDataDownload constructor of DataDownload +func NewDataDownload(cfg *viper.Viper, db *dbstore.DBStore, exchange, symbol, binSize string, start time.Time, end time.Time) (d *DataDownload) { + d = new(DataDownload) + d.cfg = cfg + d.start = start + d.end = end + d.exchange = exchange + d.symbol = symbol + d.binSize = binSize + d.db = db + return +} + +func (d *DataDownload) SetBinSize(binSize string) { + d.binSize = binSize +} + +// Start start backtest +func (d *DataDownload) Start() (err error) { + d.running = true + go d.Run() + return +} + +// Stop stop backtest +func (d *DataDownload) Stop() (err error) { + d.stop <- true + return +} +func (d *DataDownload) AutoRun() (err error) { + tbl := d.db.GetKlineTbl(d.exchange, d.symbol, d.binSize) + var invalidTime time.Time + var tmTemp, start time.Time + start = time.Now() + tmTemp = tbl.GetNewest() + if tmTemp == invalidTime { + err = fmt.Errorf("no start found in db,you must set start time") + return + } + // log.Info(k, "temp time newest:", tmTemp) + if tmTemp.Sub(start) < 0 { + start = tmTemp.Add(-time.Minute) + } + end := time.Now() + log.Debugf("autorun start:%s, end:%s", start, end) + err = d.download(start, end) + return +} + +// Run run backtest and wait for finish +func (d *DataDownload) Run() (err error) { + if d.isAuto { + err = d.AutoRun() + } else { + err = d.download(d.start, d.end) + } + return +} + +func (d *DataDownload) download(start, end time.Time) (err error) { + log.Info("begin download candle:", start, end, d.symbol, d.binSize) + exchangeType := viper.GetString(fmt.Sprintf("exchanges.%s.name", d.exchange)) + fmt.Println(d.exchange, exchangeType) + ex, err := exchange.NewExchange(exchangeType, exchange.WrapViper(d.cfg), d.exchange) + if err != nil { + return + } + tbl := d.db.GetKlineTbl(d.exchange, d.symbol, d.binSize) + klines, errChan := exchange.KlineChan(ex, d.symbol, d.binSize, start, end) + var t time.Time + cache := make([]interface{}, 1024) + i := 0 + for v := range klines { + cache[i] = v + i++ + t = time.Now() + if i >= 1024 { + + err = tbl.WriteDatas(cache) + if err != nil { + fmt.Printf("write %s - %s error: %s\n", cache[0].(*trademodel.Candle).Time(), cache[i-1].(*trademodel.Candle).Time(), err.Error()) + log.Errorf("%s write error: %s value: %#v %s", time.Now().Format(time.RFC3339), time.Since(t), v, err.Error()) + return + } else { + fmt.Printf("write %s - %s success\n", cache[0].(*trademodel.Candle).Time(), cache[i-1].(*trademodel.Candle).Time()) + } + i = 0 + } + + // log.Infof("%s write finish: %s len: %d ", time.Now().Format(time.RFC3339), time.Since(t), len(v)) + } + if i > 0 { + + err = tbl.WriteDatas(cache[0:i]) + if err != nil { + fmt.Printf("write %s - %s error: %s\n", cache[0].(*trademodel.Candle).Time(), cache[i-1].(*trademodel.Candle).Time(), err.Error()) + log.Errorf("%s write error: %s value: %#v %s", time.Now().Format(time.RFC3339), time.Since(t), len(cache), err.Error()) + return + } else { + fmt.Printf("write %s - %s success\n", cache[0].(*trademodel.Candle).Time(), cache[i-1].(*trademodel.Candle).Time()) + } + } + err = <-errChan + // log.Debugf("%s-%s %s %s %s data total %d stored\n", gStart, + // lastStart, + // d.source, + // d.symbol, + // d.binSize, + // total) + return +} + +// Progress return the progress of current backtest +func (d *DataDownload) Progress() (progress int) { + return d.Progress() +} + +// Result return the result of current backtest +// must call after end of the backtest +func (d *DataDownload) Result() (err error) { + + return +} diff --git a/pkg/ctl/list.go b/pkg/ctl/list.go new file mode 100644 index 0000000..6c30f75 --- /dev/null +++ b/pkg/ctl/list.go @@ -0,0 +1,113 @@ +package ctl + +import ( + "sort" + "time" + + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/trade/pkg/process/dbstore" + "git.qtrade.icu/coin-quant/trademodel" + log "github.com/sirupsen/logrus" +) + +type LocalDataInfo struct { + dbstore.TableInfo + + Start time.Time + End time.Time +} + +type LocalData struct { + db *dbstore.DBStore +} + +func NewLocalData(db *dbstore.DBStore) (l *LocalData, err error) { + l = new(LocalData) + l.db = db + return +} + +func (l *LocalData) ListAll() (infos []LocalDataInfo, err error) { + tbls, err := l.db.GetKlineTables() + if err != nil { + return + } + var temp []LocalDataInfo + for _, v := range tbls { + if v.Exchange == "DCE" { + continue + } + temp, err = l.checkOne(v) + if err != nil { + log.Errorf("check table %s_%s_%s failed", v.Exchange, v.Symbol, v.BinSize) + continue + } + infos = append(infos, temp...) + } + sort.Slice(infos, func(i, j int) bool { + infoA := infos[i] + infoB := infos[j] + if infoA.Exchange == infoB.Exchange { + if infoA.Symbol == infoB.Symbol { + tA, _ := common.GetBinSizeDuration(infoA.BinSize) + tB, _ := common.GetBinSizeDuration(infoB.BinSize) + return tA < tB + } + return infoA.Symbol < infoB.Symbol + } + return infoA.Exchange < infoB.Exchange + }) + return +} + +func (l *LocalData) checkOne(tbl dbstore.TableInfo) (infos []LocalDataInfo, err error) { + ktbl := l.db.GetKlineTbl(tbl.Exchange, tbl.Symbol, tbl.BinSize) + tEnd := ktbl.GetNewest() + tStart := ktbl.GetOldest() + nCount, _ := ktbl.Count() + dur, err := common.GetBinSizeDuration(tbl.BinSize) + if err != nil { + return + } + nDur := int64(tEnd.Sub(tStart)/dur) + 1 + if nDur == nCount { + infos = []LocalDataInfo{LocalDataInfo{Start: tStart, End: tEnd, TableInfo: tbl}} + return + } + if nCount == 0 { + return + } + return l.checkOneRaw(ktbl, tStart, tEnd, dur, tbl) +} +func (l *LocalData) checkOneRaw(ktbl *dbstore.KlineTbl, tStart, tEnd time.Time, nDur time.Duration, tbl dbstore.TableInfo) (infos []LocalDataInfo, err error) { + binSize := tbl.BinSize + datas, err := ktbl.DataChan(tStart, tEnd, binSize) + if err != nil { + return + } + var i time.Duration + var resetStart bool + tempStart := tStart + tempEnd := tEnd + for d := range datas { + for _, v := range d { + c, _ := v.(*trademodel.Candle) + if resetStart { + tempStart = c.Time() + } + if c.Time().Sub(tempStart) != i*nDur { + infos = append(infos, LocalDataInfo{Start: tempStart, End: tempEnd, TableInfo: tbl}) + resetStart = true + i = 0 + } else { + resetStart = false + i++ + } + tempEnd = c.Time() + } + } + if tempStart != tempEnd { + infos = append(infos, LocalDataInfo{Start: tempStart, End: tempEnd, TableInfo: tbl}) + } + return +} diff --git a/pkg/ctl/script.go b/pkg/ctl/script.go new file mode 100644 index 0000000..63baf46 --- /dev/null +++ b/pkg/ctl/script.go @@ -0,0 +1,25 @@ +package ctl + +import ( + "git.qtrade.icu/coin-quant/trade/pkg/event" + "git.qtrade.icu/coin-quant/trade/pkg/process/goscript" + "path" +) + +type Scripter interface { + event.Processer + AddScript(name, src, param string) (err error) + RemoveScript(name string) error + ScriptCount() int +} + +func NewScript(file, param, symbol string) (s Scripter, err error) { + var gEngine *goscript.GoEngine + gEngine, err = goscript.NewDefaultGoEngine() + if err != nil { + return + } + s = gEngine + err = s.AddScript(path.Base(file), file, param) + return +} diff --git a/pkg/ctl/tmpl/define.go b/pkg/ctl/tmpl/define.go new file mode 100644 index 0000000..cd3cc1e --- /dev/null +++ b/pkg/ctl/tmpl/define.go @@ -0,0 +1,57 @@ +package main + +import ( + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/base/engine" + . "git.qtrade.icu/coin-quant/trademodel" +) + +type Runner interface { + Param() (paramInfo []common.Param) + Init(engine Engine, params common.ParamData) error + OnCandle(candle *Candle) + OnPosition(pos, price float64) + OnTrade(trade *Trade) + OnTradeMarket(trade *Trade) + OnDepth(depth *Depth) + // OnEvent(e Event) +} + +type CandleFn = common.CandleFn +type Param = common.Param +type ParamData = common.ParamData + +type Engine = engine.Engine + +var StringParam = common.StringParam +var IntParam = common.IntParam +var FloatParam = common.FloatParam +var BoolParam = common.BoolParam + +func min(a, b float64) float64 { + if a < b { + return a + } + return b +} + +func max(a, b float64) float64 { + if a > b { + return a + } + return b +} + +var formatFloat = common.FormatFloat + +// FloatMul return a*b +var FloatMul = common.FloatMul + +// FloatAdd return a*b +var FloatAdd = common.FloatAdd + +// FloatSub return a-b +var FloatSub = common.FloatSub + +// FloatDiv return a/b +var FloatDiv = common.FloatDiv diff --git a/pkg/ctl/tmpl/export.go b/pkg/ctl/tmpl/export.go new file mode 100644 index 0000000..a234bcb --- /dev/null +++ b/pkg/ctl/tmpl/export.go @@ -0,0 +1,4 @@ +package main + +var NewStrategy = New{{.Name}} +var _ Runner = NewStrategy() diff --git a/pkg/ctl/trade.go b/pkg/ctl/trade.go new file mode 100644 index 0000000..b345263 --- /dev/null +++ b/pkg/ctl/trade.go @@ -0,0 +1,167 @@ +package ctl + +import ( + "errors" + "fmt" + "git.qtrade.icu/coin-quant/trade/pkg/event" + "git.qtrade.icu/coin-quant/trade/pkg/process/exchange" + "git.qtrade.icu/coin-quant/trade/pkg/process/goscript" + "git.qtrade.icu/coin-quant/trade/pkg/process/notify" + "git.qtrade.icu/coin-quant/trade/pkg/process/rpt" + "sync" + "time" + + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +var ( + cfg *viper.Viper +) + +func SetConfig(c *viper.Viper) { + cfg = c +} + +// Trade trade with multi scripts +type Trade struct { + exchangeType string + exchangeName string + symbol string + running bool + stop chan bool + rpt rpt.Reporter + proc *event.Processers + engine *goscript.GoEngine + wg sync.WaitGroup + loadRecent time.Duration +} + +// NewTrade constructor of Trade +func NewTrade(exchange, symbol string) (b *Trade, err error) { + b = new(Trade) + b.exchangeName = exchange + b.symbol = symbol + b.exchangeType = cfg.GetString(fmt.Sprintf("exchanges.%s.name", b.exchangeName)) + gEngine, err := goscript.NewGoEngine(symbol) + if err != nil { + return + } + b.engine = gEngine + b.loadRecent = time.Hour * 24 + return +} + +func (b *Trade) SetLoadRecent(recent time.Duration) { + b.loadRecent = recent +} + +func (b *Trade) SetStatusCh(ch chan *goscript.Status) { + b.engine.SetStatusCh(ch) +} + +func (b *Trade) SetReporter(rpt rpt.Reporter) { + b.rpt = rpt +} + +func (b *Trade) AddScript(name, scriptFile, param string) (err error) { + err = b.engine.AddScript(name, scriptFile, param) + return +} + +func (b *Trade) ScriptCount() int { + return b.engine.ScriptCount() +} + +func (b *Trade) RemoveScript(name string) (err error) { + if !b.running { + err = errors.New("Trade is not working,must start it first") + return + } + err = b.engine.RemoveScript(name) + return +} + +// Start start backtest +func (b *Trade) Start() (err error) { + if b.running { + return + } + b.running = true + err = b.init() + if err != nil { + b.running = false + return + } + b.wg.Add(1) + go b.Run() + return +} + +// Stop stop backtest +func (b *Trade) Stop() (err error) { + b.proc.Stop() + b.stop <- true + return +} + +func (b *Trade) init() (err error) { + b.stop = make(chan bool) + param := event.NewBaseProcesser("param") + ex, err := exchange.GetTradeExchange(b.exchangeType, cfg, b.exchangeName, b.symbol) + if err != nil { + err = fmt.Errorf("creat exchange trade %s failed:%s", b.exchangeName, err.Error()) + return + } + notify, err := notify.NewNotify(cfg) + if err != nil { + log.Errorf("creat notify failed:%s", err.Error()) + err = nil + } + b.proc = event.NewProcessers() + procs := []event.Processer{param, ex, b.engine} + if notify != nil { + procs = append(procs, notify) + } + if b.rpt != nil { + r := rpt.NewRpt(b.rpt) + procs = append(procs, r) + } + + err = b.proc.Adds(procs...) + if err != nil { + log.Error("add processers error:", err.Error()) + return + } + err = b.proc.Start() + if err != nil { + log.Error("start processers error:", err.Error()) + return + } + candleParam := CandleParam{ + Start: time.Now().Add(-1 * b.loadRecent), + Symbol: b.symbol, + BinSize: "1m", + } + log.Info("real trade candle param:", candleParam) + param.Send("candle", EventWatch, NewWatchCandle(&candleParam)) + + log.Info("real trade watch trade_market") + param.Send("trade", EventWatch, &WatchParam{Type: EventTradeMarket, Extra: b.symbol, Data: map[string]interface{}{"name": "market"}}) + log.Info("real trade watch depth") + param.Send("depth", EventWatch, &WatchParam{Type: EventDepth, Extra: b.symbol, Data: map[string]interface{}{"name": "depth"}}) + return +} + +func (b *Trade) Wait() (err error) { + b.wg.Wait() + return +} + +func (b *Trade) Run() (err error) { + defer b.wg.Done() + // TODO wait for finish + <-b.stop + b.proc.WaitClose(time.Second * 10) + return +} diff --git a/pkg/event/bus.go b/pkg/event/bus.go new file mode 100644 index 0000000..b189fa9 --- /dev/null +++ b/pkg/event/bus.go @@ -0,0 +1,183 @@ +package event + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + + jsoniter "github.com/json-iterator/go" + + log "github.com/sirupsen/logrus" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +// ProcessCall callback to process event +type ProcessCall func(e *Event) error + +type ProcessCallInfo struct { + Cb ProcessCall + Name string +} +type ProcessList []ProcessCallInfo + +// Bus event bus +type Bus struct { + syncMode bool + chs map[string]chan *Event + cache int + procs map[string]ProcessList + procsMutex sync.RWMutex + + processEvent int64 + lastEventTime time.Time + routines int32 +} + +func NewBus(cache int) *Bus { + b := new(Bus) + b.cache = cache + b.chs = make(map[string]chan *Event) + b.procs = make(map[string]ProcessList) + return b +} + +func NewSyncBus() *Bus { + b := new(Bus) + b.syncMode = true + b.chs = make(map[string]chan *Event) + b.procs = make(map[string]ProcessList) + return b +} + +func (b *Bus) runProc(sub string, ch chan *Event) (err error) { + atomic.AddInt32(&b.routines, 1) + defer atomic.AddInt32(&b.routines, -1) + log.Debug("Bus runProc of ", sub) + if ch == nil { + err = fmt.Errorf("no such event channel: %s", sub) + panic(err.Error()) + return + } + b.procsMutex.RLock() + procs := b.procs[sub] + b.procsMutex.RUnlock() + for e := range ch { + for _, p := range procs { + err = p.Cb(e) + if err != nil { + b.Send(NewErrorEvent(p.Name, err.Error(), err)) + // log.Errorf("process %s error: %s", sub, err.Error()) + continue + } + } + atomic.AddInt64(&b.processEvent, -1) + releaseEvent(e) + } + return +} + +// Subscribe event +func (b *Bus) Subscribe(from, sub string, cb ProcessCall) (err error) { + b.procsMutex.Lock() + pi := ProcessCallInfo{Cb: cb, Name: from} + _, ok := b.procs[sub] + if !ok { + b.procs[sub] = ProcessList{pi} + } else { + b.procs[sub] = append(b.procs[sub], pi) + } + b.procsMutex.Unlock() + return +} + +func (b *Bus) Send(e *Event) (err error) { + typ := e.GetType() + procs, ok := b.procs[typ] + if !ok { + log.Warnf("Send %s event,but no subscribers, skip", e.GetType()) + return + } + atomic.AddInt64(&b.processEvent, 1) + if b.syncMode { + return b.sendSync(procs, e) + } + + chs := b.chs[typ] + b.lastEventTime = time.Now() + chs <- e + return +} +func (b *Bus) sendSync(procs ProcessList, e *Event) (err error) { + for _, p := range procs { + err = p.Cb(e) + if err != nil { + // log.Errorf("subscribe %s process error: %s", e.GetType(), err.Error()) + b.Send(NewErrorEvent(p.Name, err.Error(), err)) + continue + } + } + releaseEvent(e) + atomic.AddInt64(&b.processEvent, -1) + return +} + +func (b *Bus) WaitEmpty() { + // time.Sleep(time.Millisecond) + value := atomic.LoadInt64(&b.processEvent) + for value != 0 { + time.Sleep(time.Millisecond) + value = atomic.LoadInt64(&b.processEvent) + } +} + +func (b *Bus) Close() { + t := time.Now() + var value int64 + for { + time.Sleep(time.Nanosecond) + value = atomic.LoadInt64(&b.processEvent) + if value != 0 { + continue + } + if time.Since(b.lastEventTime) > time.Second*5 || time.Since(t) > time.Second*5 { + break + } + } + + for _, v := range b.chs { + close(v) + } + + var n int32 + for { + n = atomic.LoadInt32(&b.routines) + if n == 0 { + break + } + time.Sleep(time.Millisecond) + log.Info("event bus routines all finished, left:", n) + } +} + +func (b *Bus) Start() { + if b.syncMode { + return + } + for k := range b.procs { + ch := make(chan *Event, b.cache) + b.chs[k] = ch + go b.runProc(k, ch) + } + // wait for all routines start + var n int32 + for { + n = atomic.LoadInt32(&b.routines) + if n == int32(len(b.procs)) { + break + } + time.Sleep(time.Millisecond) + log.Infof("event bus %d routines, started: %d", len(b.procs), n) + } +} diff --git a/pkg/event/event.go b/pkg/event/event.go new file mode 100644 index 0000000..25254f7 --- /dev/null +++ b/pkg/event/event.go @@ -0,0 +1,72 @@ +package event + +import ( + "sync" + + "git.qtrade.icu/coin-quant/trade/pkg/core" +) + +var ( + eventPool = sync.Pool{New: func() interface{} { + return new(Event) + }} +) + +// Event base event +type Event struct { + Data core.EventData + Name string + // Time time.Time + From string +} + +func NewErrorEvent(from, msg string, err error) *Event { + e := new(Event) + e.Name = msg + e.Data.Type = core.EventError + e.Data.Data = err + e.From = from + // e.Time = time.Now() + return e +} + +func NewEvent(name, strType, from string, data interface{}, extra interface{}) *Event { + e := eventPool.Get().(*Event) + // e := new(Event) + e.Name = name + e.Data.Type = strType + e.From = from + e.Data.Data = data + // e.Time = time.Now() + e.Data.Extra = extra + return e +} + +func releaseEvent(e *Event) { + e.Data.Data = nil + e.Data.Extra = nil + eventPool.Put(e) +} + +func (e *Event) GetName() string { + return e.Name +} + +func (e *Event) GetType() string { + return e.Data.Type +} + +// func (e *Event) GetTime() time.Time { +// return e.Time +// } + +func (e *Event) GetFrom() string { + return e.From +} + +func (e *Event) GetData() interface{} { + return e.Data.Data +} +func (e *Event) GetExtra() interface{} { + return e.Data.Extra +} diff --git a/pkg/event/event_test.go b/pkg/event/event_test.go new file mode 100644 index 0000000..a4ed624 --- /dev/null +++ b/pkg/event/event_test.go @@ -0,0 +1,21 @@ +package event + +import ( + "testing" + + "git.qtrade.icu/coin-quant/trade/pkg/core" +) + +func TestUnmarshalEvent(t *testing.T) { + // buf := `{"data":{"type":"balance","data":{"Balance":100000}},"Name":"BTCUSDT","Time":"2021-10-31T11:15:49.131137699+08:00","From":"VExchange"}` + var e = Event{Data: core.EventData{Type: "balance", Data: &core.BalanceInfo{Balance: 100}}, Name: "BTCUSDT"} + buf, err := json.Marshal(e) + if err != nil { + t.Fatal(err.Error()) + } + t.Log(string(buf)) + err = json.Unmarshal(buf, &e) + if err != nil { + t.Fatal(err.Error()) + } +} diff --git a/pkg/event/processer.go b/pkg/event/processer.go new file mode 100644 index 0000000..150cdda --- /dev/null +++ b/pkg/event/processer.go @@ -0,0 +1,65 @@ +package event + +// Processer handler of event +type Processer interface { + Init(*Bus) error + GetName() string + + Start() error + Stop() error +} + +// BaseProcesser basic processer +type BaseProcesser struct { + Bus *Bus + Name string +} + +// NewBaseProcesser constructor +func NewBaseProcesser(name string) *BaseProcesser { + bp := new(BaseProcesser) + bp.Name = name + return bp +} + +// Subscribe event +func (b *BaseProcesser) Subscribe(sub string, cb ProcessCall) (err error) { + b.Bus.Subscribe(b.Name, sub, cb) + return +} + +// Send send event +func (b *BaseProcesser) Send(name, strType string, data interface{}) { + b.Bus.Send(NewEvent(name, strType, b.Name, data, nil)) +} + +// SendExtra send event with extra info +func (b *BaseProcesser) SendWithExtra(name, strType string, data, extra interface{}) { + b.Bus.Send(NewEvent(name, strType, b.Name, data, extra)) +} + +// Init call before start +func (b *BaseProcesser) Init(bus *Bus) (err error) { + b.Bus = bus + return +} + +// Start start the processer +func (b *BaseProcesser) Start() (err error) { + return +} + +// Stop stop the processer +func (b *BaseProcesser) Stop() (err error) { + return +} + +// GetName return the processer name +func (b *BaseProcesser) GetName() string { + return b.Name +} + +// CreateEvent create new event +func (b *BaseProcesser) CreateEvent(name, strType string, data interface{}) *Event { + return NewEvent(name, strType, b.Name, data, nil) +} diff --git a/pkg/event/processers.go b/pkg/event/processers.go new file mode 100644 index 0000000..b56b866 --- /dev/null +++ b/pkg/event/processers.go @@ -0,0 +1,97 @@ +package event + +import ( + "time" + + "git.qtrade.icu/coin-quant/trade/pkg/core" +) + +type ErrorCallback func(error) + +// Processers processers +type Processers struct { + handlers []Processer + bus *Bus + errorCb ErrorCallback +} + +// NewProcessers create default Processers +func NewProcessers() *Processers { + p := new(Processers) + p.bus = NewBus(1024) + return p +} + +// NewSyncProcessers create sync Processers +func NewSyncProcessers() *Processers { + p := new(Processers) + p.bus = NewSyncBus() + return p +} + +func (h *Processers) SetErrorCallback(fn ErrorCallback) { + h.errorCb = fn + +} + +func (h *Processers) onError(e *Event) error { + errInfo := e.Data.Data.(error) + if h.errorCb == nil { + return nil + } + h.errorCb(errInfo) + return nil +} + +// Adds add processer +func (h *Processers) Adds(ehs ...Processer) (err error) { + for _, v := range ehs { + err = h.Add(v) + if err != nil { + return + } + } + return +} + +// Add add proocesser +func (h *Processers) Add(eh Processer) (err error) { + h.handlers = append(h.handlers, eh) + return +} + +// Start start all processers +func (h *Processers) Start() (err error) { + for _, p := range h.handlers { + err = p.Init(h.bus) + if err != nil { + return + } + } + h.bus.Subscribe("Processers", core.EventError, h.onError) + h.bus.Start() + for _, p := range h.handlers { + err = p.Start() + if err != nil { + return + } + } + return +} + +// Stop stop all processers +func (h *Processers) Stop() (err error) { + for _, p := range h.handlers { + err = p.Stop() + if err != nil { + return + } + } + return +} + +// WaitClose wait for duration after bus is empty,and then close +func (h *Processers) WaitClose(duration time.Duration) { + time.Sleep(duration) + h.bus.Close() +} diff --git a/pkg/helper/helper.go b/pkg/helper/helper.go new file mode 100644 index 0000000..f31432c --- /dev/null +++ b/pkg/helper/helper.go @@ -0,0 +1,48 @@ +package helper + +import ( + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/base/engine" +) + +type CandleFn func(candle Candle) +type Engine = engine.Engine +type Param = common.Param +type ParamData = common.ParamData + +var StringParam = common.StringParam +var IntParam = common.IntParam +var FloatParam = common.FloatParam +var BoolParam = common.BoolParam + +func min(a, b float64) float64 { + return 0 +} + +func max(a, b float64) float64 { + return 0 +} + +func formatFloat(n float64, precision int) float64 { + return 0 +} + +// FloatMul return a*b +func FloatMul(a, b float64) float64 { + return 0 +} + +// FloatAdd return a*b +func FloatAdd(a, b float64) float64 { + return 0 +} + +// FloatSub return a-b +func FloatSub(a, b float64) float64 { + return 0 +} + +// FloatDiv return a/b +func FloatDiv(a, b float64) float64 { + return 0 +} diff --git a/pkg/helper/strategy.go b/pkg/helper/strategy.go new file mode 100644 index 0000000..94651d3 --- /dev/null +++ b/pkg/helper/strategy.go @@ -0,0 +1,57 @@ +package helper + +import ( + "fmt" +) + +type DemoStrategy struct { +} + +func NewDemoStrategy() *DemoStrategy { + return new(DemoStrategy) +} + +// Param define you script params here +func (s *DemoStrategy) Param() (paramInfo []Param) { + paramInfo = []Param{ + Param{Name: "symbol", Type: "string", Info: "symbol code"}, + } + return +} + +// Init strategy +func (s *DemoStrategy) Init(engine Engine, params ParamData) { + return +} + +// OnCandle call when 1m candle reached +func (s *DemoStrategy) OnCandle(candle *Candle) { + var param Param + param.Name = "hello" + fmt.Println("candle:", candle, param) + return +} + +// OnPosition call when position is updated +func (s *DemoStrategy) OnPosition(pos, price float64) { + fmt.Println("position:", pos, price) + return +} + +// OnTrade call call you own trade occures +func (s *DemoStrategy) OnTrade(trade *Trade) { + fmt.Println("trade:", trade) + return +} + +// OnTradeMarket call when trade occures +func (s *DemoStrategy) OnTradeMarket(trade *Trade) { + fmt.Println("tradeHistory:", trade) + return +} + +// OnDepth call when orderbook updated +func (s *DemoStrategy) OnDepth(depth *Depth) { + fmt.Println("depth:", depth) + return +} diff --git a/pkg/process/dbstore/db.go b/pkg/process/dbstore/db.go new file mode 100644 index 0000000..07310b7 --- /dev/null +++ b/pkg/process/dbstore/db.go @@ -0,0 +1,155 @@ +package dbstore + +import ( + "fmt" + "git.qtrade.icu/coin-quant/base/common" + "reflect" + "regexp" + "sync" + + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + log "github.com/sirupsen/logrus" + _ "modernc.org/sqlite" + "xorm.io/xorm" +) + +var ( + tblRegexp = regexp.MustCompile(`^([A-Za-z0-9]+)_([A-Za-z0-9\_\-]+)_([A-Za-z0-9]+)$`) +) + +type TableInfo struct { + Exchange string + Symbol string + BinSize string +} + +type DBStore struct { + dbType string + dbPath string + table string + engine *xorm.Engine + tbls sync.Map + + useCache bool + dataCache sync.Map +} + +// NewDBStore support sqlite,mysql,pg +func NewDBStore(dbType, dbURI string) (dr *DBStore, err error) { + dr = new(DBStore) + dr.dbType = dbType + dr.dbPath = dbURI + err = dr.initDB() + return +} + +// Close close db +func (dr *DBStore) Close() (err error) { + if dr.engine != nil { + err = dr.engine.Close() + } + return +} + +// SetDebug set debug mode +func (dr *DBStore) SetDebug(bDebug bool) { + dr.engine.ShowSQL(bDebug) +} + +func (dr *DBStore) initDB() (err error) { + if dr.engine != nil { + dr.engine.Close() + } + dr.engine, err = xorm.NewEngine(dr.dbType, dr.dbPath) + if err != nil { + err = fmt.Errorf("init db failed:%s", err.Error()) + return + } + err = dr.engine.Sync2(&SymbolInfo{}) + return +} + +// GetTableSession get table,if not exsit, create the table +func (dr *DBStore) GetTableSession(tbl string, data TimeData) (sess *xorm.Session) { + bExit, err := dr.engine.IsTableExist(tbl) + if err != nil { + log.Error("dbstore get table failed:", err.Error()) + } + if !bExit { + log.Debugf("create table %s ", dr.table, reflect.TypeOf(data)) + data.SetTable(tbl) + fmt.Println(tbl, reflect.TypeOf(data)) + dr.engine.Sync2(data) + } + sess = dr.engine.NewSession() + sess = sess.Table(tbl) + return +} + +func (dr *DBStore) getTblSess(tbl string) (sess *xorm.Session) { + sess = dr.engine.NewSession() + sess = sess.Table(tbl) + return +} + +// GetKlineTbl get kline table +func (dr *DBStore) GetKlineTbl(exchange, symbol, binSize string) *KlineTbl { + key := fmt.Sprintf("%s_%s_%s", exchange, symbol, binSize) + v, ok := dr.tbls.Load(key) + if ok { + return v.(*KlineTbl) + } + t := NewKlineTbl(dr, exchange, symbol, binSize) + dr.tbls.Store(key, t) + return t +} + +func (dr *DBStore) NewKlineTbl(exchange, symbol, binSize string) *KlineTbl { + t := NewKlineTbl(dr, exchange, symbol, binSize) + return t +} + +func (d *DBStore) SetUseCache(useCache bool) { + d.useCache = useCache +} + +// WriteKlines write klines +func (d *DBStore) WriteKlines(exchange, symbol, binSize string, datas []interface{}) (err error) { + err = d.GetKlineTbl(exchange, symbol, binSize).WriteDatas(datas) + return +} + +func (dr *DBStore) GetTables() (tbls []string, err error) { + allTbls, err := dr.engine.DBMetas() + if err != nil { + return + } + for _, v := range allTbls { + tbls = append(tbls, v.Name) + } + return +} + +func (dr *DBStore) GetKlineTables() (tbls []TableInfo, err error) { + tblNames, err := dr.GetTables() + if err != nil { + return + } + for _, v := range tblNames { + ret := tblRegexp.FindAllStringSubmatch(v, -1) + if len(ret) != 1 { + continue + } + if len(ret[0]) != 4 { + continue + } + _, err = common.GetBinSizeDuration(ret[0][3]) + if err != nil { + err = nil + continue + } + tbls = append(tbls, TableInfo{Exchange: ret[0][1], Symbol: ret[0][2], BinSize: ret[0][3]}) + } + return +} diff --git a/pkg/process/dbstore/kline.go b/pkg/process/dbstore/kline.go new file mode 100644 index 0000000..28c5166 --- /dev/null +++ b/pkg/process/dbstore/kline.go @@ -0,0 +1,99 @@ +package dbstore + +import ( + "fmt" + + log "github.com/sirupsen/logrus" +) + +// KlineTbl kline data table +type KlineTbl struct { + BaseProcesser + TimeTbl + loadData bool +} + +func NewKlineTbl(db *DBStore, exchange, symbol, binSize string) (t *KlineTbl) { + t = new(KlineTbl) + tbl := NewTimeTbl(db, t, exchange, symbol, binSize, "") + t.TimeTbl = *tbl + t.BaseProcesser.Name = "klinetbl:" + t.table + return +} + +func (tbl *KlineTbl) Sing() TimeData { + return new(Candle) +} + +func (tbl *KlineTbl) Slice() interface{} { + return &[]*Candle{} +} +func (tbl *KlineTbl) SetLoadDataMode(bLoad bool) { + tbl.loadData = bLoad +} + +func (tbl *KlineTbl) Init(bus *Bus) (err error) { + tbl.BaseProcesser.Init(bus) + if !tbl.loadData { + tbl.Subscribe(EventCandle, tbl.onEventCandle) + } + tbl.Subscribe(EventWatch, tbl.onEventCandleParam) + return +} + +func (tbl *KlineTbl) GetSlice(data interface{}) (rets []interface{}) { + datas, ok := data.(*[]*Candle) + if !ok { + log.Error("KlineTbl getslice error") + return + } + rets = make([]interface{}, len(*datas)) + for k, v := range *datas { + rets[k] = v + } + return +} + +func (tbl *KlineTbl) emitCandles(param CandleParam) { + candles, err := tbl.DataChan(param.Start, param.End, param.BinSize) + if err != nil { + log.Error("KlineTbl tbl get candles failed:", err.Error()) + return + } + var candle *Candle + for v := range candles { + for _, c := range v { + candle = c.(*Candle) + tbl.Bus.WaitEmpty() + tbl.SendWithExtra("candle", EventCandle, candle, param.BinSize) + } + } + if tbl.closeCh != nil { + log.Info("kline table emitCandles finished") + tbl.closeCh <- true + } +} + +func (tbl *KlineTbl) onEventCandle(e *Event) (err error) { + candle := e.GetData().(*Candle) + err = tbl.WriteData(candle) + if err != nil { + return + } + return +} + +func (tbl *KlineTbl) onEventCandleParam(e *Event) (err error) { + wParam, ok := e.GetData().(*WatchParam) + if !ok { + err = fmt.Errorf("event not watch %s %#v", e.Name, e.Data) + return + } + candleParam, _ := wParam.Data.(*CandleParam) + if candleParam == nil { + err = fmt.Errorf("event not CandleParam %s %#v", e.Name, e.Data) + return + } + go tbl.emitCandles(*candleParam) + return +} diff --git a/pkg/process/dbstore/load.go b/pkg/process/dbstore/load.go new file mode 100644 index 0000000..d80943e --- /dev/null +++ b/pkg/process/dbstore/load.go @@ -0,0 +1,11 @@ +package dbstore + +import "github.com/spf13/viper" + +// LoadDB Load init db from config file +func LoadDB(cfg *viper.Viper) (db *DBStore, err error) { + dbType := cfg.GetString("db.type") + dbURI := cfg.GetString("db.uri") + db, err = NewDBStore(dbType, dbURI) + return +} diff --git a/pkg/process/dbstore/tbl.go b/pkg/process/dbstore/tbl.go new file mode 100644 index 0000000..a9d0aea --- /dev/null +++ b/pkg/process/dbstore/tbl.go @@ -0,0 +1,375 @@ +package dbstore + +import ( + "errors" + "fmt" + "reflect" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "xorm.io/xorm" +) + +// TimeData data with time info +type TimeData interface { + GetStart() int64 + Time() time.Time + GetTable() string + SetTable(string) +} + +type DataCreator interface { + Sing() TimeData + Slice() interface{} + GetSlice(interface{}) []interface{} +} + +// TimeTbl tbl with time info +type TimeTbl struct { + db *DBStore + exchange string + symbol string + binSize string + table string + creator DataCreator + closeCh chan bool + loadOnce int +} + +// NewTimeTbl create new time table +func NewTimeTbl(db *DBStore, creator DataCreator, exchange, symbol, binSize, extName string) (t *TimeTbl) { + t = new(TimeTbl) + t.db = db + t.creator = creator + t.exchange = exchange + t.symbol = symbol + t.binSize = binSize + t.loadOnce = 50000 + + t.table = fmt.Sprintf("%s_%s_%s", exchange, symbol, binSize) + if extName != "" { + t.table += "_" + extName + } + return +} + +func (t *TimeTbl) SetLoadOnce(loadOnce int) { + t.loadOnce = loadOnce +} + +func (t *TimeTbl) SetCloseCh(closeCh chan bool) { + t.closeCh = closeCh +} + +func (t *TimeTbl) getTable() (sess *xorm.Session) { + data := t.creator.Sing() + sess = t.db.GetTableSession(t.table, data) + return +} + +func (t *TimeTbl) GetSymbol() string { + return t.symbol +} + +func (t *TimeTbl) GetTable() string { + return t.table +} + +func (t *TimeTbl) GetDatas(since, end time.Time, limit int) (datas []interface{}, err error) { + return t.getDatasWithParam(since, end, limit, 0) +} + +func (t *TimeTbl) getDatasWithParam(since, end time.Time, limit, offset int) (datas []interface{}, err error) { + ret := t.creator.Slice() + sess := t.getTable() + defer sess.Close() + err = sess.Asc("start").Where("start>=? and start 0 { + isEmpty = false + } + return +} + +func (tbl *TimeTbl) GetNewest() (t time.Time) { + sess := tbl.getTable() + defer sess.Close() + data := tbl.creator.Sing() + _, err := sess.Desc("start").Limit(1, 0).Get(data) + if err != nil { + log.Errorf("TimeTbl get newest %s failed:%s", tbl.table, err.Error()) + return + } + t = data.Time() + return +} + +func (tbl *TimeTbl) GetOldest() (t time.Time) { + sess := tbl.getTable() + defer sess.Close() + data := tbl.creator.Sing() + _, err := sess.Asc("start").Limit(1, 0).Get(data) + if err != nil { + log.Errorf("TimeTbl get newest %s failed:%s", tbl.table, err.Error()) + return + } + t = data.Time() + return +} + +// Exists check if data's time exists +func (t *TimeTbl) Exists(data interface{}) (bRet bool, err error) { + sess := t.getTable() + if sess == nil { + err = errors.New("no such table") + return + } + defer sess.Close() + v, ok := data.(TimeData) + if !ok { + err = fmt.Errorf("UpdateData type error:%s", reflect.TypeOf(v)) + return + } + bRet, err = sess.Table(t.table).Where("start=?", v.GetStart()).Exist() + return +} + +func (t *TimeTbl) AddOrUpdateData(data interface{}) (err error) { + err = t.UpdateData(data) + if err != nil { + err = t.WriteData(data) + } + return +} + +// UpdateData update datas +func (t *TimeTbl) UpdateData(data interface{}) (err error) { + sess := t.getTable() + if sess == nil { + err = errors.New("no such table") + return + } + defer sess.Close() + var v TimeData + v, ok := data.(TimeData) + if !ok { + err = fmt.Errorf("UpdateData type error:%s", reflect.TypeOf(v)) + return + } + v.SetTable(t.table) + n, err := sess.Table(t.table).Where("start=?", v.GetStart()).UseBool().Update(data) + if err != nil { + log.Errorf("TimeTbl update data %s error:%s", v, err.Error()) + return + } + if n == 0 { + err = fmt.Errorf("no such data") + } + return +} + +// WriteData write data +func (t *TimeTbl) WriteData(data interface{}) (err error) { + sess := t.getTable() + if sess == nil { + err = errors.New("no such table") + return + } + defer sess.Close() + + var bRet bool + var v TimeData + + v, ok := data.(TimeData) + if !ok { + err = fmt.Errorf("WriteData type error:%s", reflect.TypeOf(v)) + return + } + v.SetTable(t.table) + bRet, err = sess.Table(t.table).Where("start=?", v.GetStart()).Exist() + if err != nil { + log.Errorf("TimeTbl check exist %s error:%s", v, err.Error()) + return + } + if bRet { + log.Debugf("insert %s exist", v) + return + } + _, err = sess.Insert(data) + if err != nil { + log.Errorf("TimeTbl insert %s error:%s", v, err.Error()) + } + return +} + +// WriteDatas write datas +func (t *TimeTbl) WriteDatas(datas []interface{}) (err error) { + sess := t.getTable() + if sess == nil { + err = errors.New("no such table") + return + } + defer sess.Close() + err = sess.Begin() + if err != nil { + return + } + + var v TimeData + for _, data := range datas { + v = data.(TimeData) + v.SetTable(t.table) + _, err = sess.Insert(data) + if err != nil { + if strings.Contains(err.Error(), "Duplicate entry") || strings.Contains(err.Error(), "UNIQUE constraint") { + _, err = sess.Where("start=?", v.GetStart()).Update(data) + if err != nil { + log.Debugf("TimeTbl insert/update %s error:%#v", v, err) + } + } else { + log.Errorf("TimeTbl insert %s error:%#v", v, err) + } + } + err = nil + } + sess.Commit() + return +} + +func (tbl *TimeTbl) Count() (n int64, err error) { + sess := tbl.getTable() + defer sess.Close() + n, err = sess.Count() + return +} diff --git a/pkg/process/dbstore/trade.go b/pkg/process/dbstore/trade.go new file mode 100644 index 0000000..7f8fae9 --- /dev/null +++ b/pkg/process/dbstore/trade.go @@ -0,0 +1,18 @@ +package dbstore + +// type Trade struct { +// ID string +// Action TradeType +// Time time.Time +// Price float64 +// Amount float64 +// Side string +// Remark string +// } + +type TradeTbl struct { +} + +func NewTradeTbl(db *DBStore, id int64) (t *TradeTbl) { + return +} diff --git a/pkg/process/exchange/err.go b/pkg/process/exchange/err.go new file mode 100644 index 0000000..e06d367 --- /dev/null +++ b/pkg/process/exchange/err.go @@ -0,0 +1,31 @@ +package exchange + +import ( + "errors" + "time" +) + +var ( + ErrCanRetry = errors.New("error but can retry") +) + +type dofn func() (interface{}, error) + +func doOrderWithRetry(nRetry int, fn dofn) (ret interface{}, err error) { + ret, err = fn() + if err == nil { + return + } + for n := 0; n != nRetry; n++ { + if errors.Is(err, ErrCanRetry) { + time.Sleep(time.Millisecond * 500) + ret, err = fn() + if err == nil { + break + } + } else { + break + } + } + return +} diff --git a/pkg/process/exchange/exchange.go b/pkg/process/exchange/exchange.go new file mode 100644 index 0000000..530bf97 --- /dev/null +++ b/pkg/process/exchange/exchange.go @@ -0,0 +1,346 @@ +package exchange + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + + "git.qtrade.icu/coin-quant/exchange" + "git.qtrade.icu/coin-quant/trademodel" + log "github.com/sirupsen/logrus" +) + +type OrderInfo struct { + LocalID string + Order + Action TradeType + Filled bool +} + +type TradeExchange struct { + BaseProcesser + + impl exchange.Exchange + + datas chan interface{} + actChan chan TradeAction + + orders map[string]*OrderInfo + localOrderIndex map[string]*OrderInfo + + closeCh chan bool + + pos Position + positionUpdate int64 + exchangeName string + symbol string + + candleParam CandleParam + + localStopOrder bool + stopOrders sync.Map +} + +func NewTradeExchange(exName string, impl exchange.Exchange, symbol string) *TradeExchange { + te := new(TradeExchange) + te.Name = fmt.Sprintf("exchange-%s", exName) + te.exchangeName = exName + te.impl = impl + te.actChan = make(chan TradeAction, 10) + te.orders = make(map[string]*OrderInfo) + te.localOrderIndex = make(map[string]*OrderInfo) + te.closeCh = make(chan bool) + te.symbol = symbol + te.datas = make(chan interface{}, 1024) + return te +} + +func (b *TradeExchange) UseLocalStopOrder(enable bool) { + b.localStopOrder = enable + if enable { + log.Warnf("%s TradeExchange use local stop order", b.impl.Info().Name) + } +} + +func (b *TradeExchange) Init(bus *Bus) (err error) { + b.BaseProcesser.Init(bus) + b.Subscribe(EventOrder, b.onEventOrder) + b.Subscribe(EventWatch, b.onEventWatch) + return +} + +func (b *TradeExchange) Start() (err error) { + b.impl.Watch(exchange.WatchParam{Type: exchange.WatchTypeBalance}, func(data interface{}) { + b.datas <- data + }) + b.impl.Watch(exchange.WatchParam{Type: exchange.WatchTypePosition}, func(data interface{}) { + b.datas <- data + }) + b.impl.Watch(exchange.WatchParam{Type: exchange.WatchTypeTrade}, func(data interface{}) { + b.datas <- data + }) + err = b.impl.Start() + if err != nil { + return err + } + go b.recvDatas() + go b.orderRoutine() + return +} + +func (b *TradeExchange) Stop() (err error) { + err = b.impl.Stop() + close(b.actChan) + return +} + +func (b *TradeExchange) recvDatas() { + var ok bool + var posTime int64 + var o *OrderInfo + bFirst := true + var err error + var tFirstLastStart int64 +Out: + for data := range b.datas { + switch value := data.(type) { + case *Candle: + if bFirst { + bFirst = false + param := b.candleParam + param.End = value.Time().Add(-1 * time.Second) + tFirstLastStart, err = b.emitRecentCandles(param) + if err != nil { + log.Errorf("TradeExchange recv data:", err.Error()) + panic(err.Error()) + } + if value.Start <= tFirstLastStart { + continue + } + } + b.SendWithExtra("candle", EventCandle, value, b.candleParam.BinSize) + case *Balance: + b.Send(b.exchangeName, EventBalance, value) + case *Position: + if value.Symbol != b.symbol { + log.Infof("TradeExchange ignore event: %#v, exchange symbol: %s, data symbol: %s", value, b.symbol, value.Symbol) + continue + } + b.pos = *value + posTime = time.Now().Unix() + atomic.StoreInt64(&b.positionUpdate, posTime) + b.Send(value.Symbol, EventPosition, value) + case *Order: + if value.Symbol != b.symbol { + log.Infof("TradeExchange ignore event: %#v, exchange symbol: %s, data symbol: %s", value, b.symbol, value.Symbol) + continue + } + o, ok = b.orders[value.OrderID] + if !ok || o.Filled { + continue Out + } + o.Order = *value + if value.Status != OrderStatusFilled { + continue Out + } + o.Filled = true + tr := Trade{ID: o.LocalID, + Action: o.Action, + Time: o.Time, + Price: o.Price, + Amount: o.Amount, + Side: o.Side, + Remark: o.OrderID} + b.Send(o.OrderID, EventTrade, &tr) + case *Depth: + b.Send(b.exchangeName, EventDepth, value) + case *Trade: + b.onEventTradeMarket(value) + b.Send(b.exchangeName, EventTradeMarket, value) + default: + log.Errorf("unsupport exchange data: %##v", value) + } + } +} + +func (b *TradeExchange) onEventCandleParam(e *Event) (err error) { + wParam, ok := e.GetData().(*WatchParam) + if !ok { + err = fmt.Errorf("event not watch %s %#v", e.Name, e.Data) + return + } + cParam, _ := wParam.Data.(*CandleParam) + if cParam == nil { + err = fmt.Errorf("event not CandleParam %s %#v", e.Name, e.Data) + return + } + go b.emitCandles(*cParam) + return +} + +func (b *TradeExchange) onEventOrder(e *Event) (err error) { + act := e.GetData().(*TradeAction) + b.actChan <- *act + return +} + +func (b *TradeExchange) onEventTradeMarket(trade *Trade) { + if !b.localStopOrder || b.pos.Hold == 0 { + return + } + var deleteOrders []string + b.stopOrders.Range(func(key, value any) bool { + id := key.(string) + act := value.(TradeAction) + if b.pos.Hold > 0 && act.Action == StopLong && trade.Price < act.Price { + // do stop long + newAct := TradeAction{ + ID: id + "_stop", + Action: CloseLong, + Amount: act.Amount, + Price: act.Price, + Time: act.Time, + Symbol: act.Symbol, + } + log.Infof("TradeEvent local stopLong order trigger: %#v", newAct) + deleteOrders = append(deleteOrders, id) + b.actChan <- newAct + return true + } + if b.pos.Hold < 0 && act.Action == StopShort && trade.Price > act.Price { + // do stop short + newAct := TradeAction{ + ID: id + "_stop", + Action: CloseShort, + Amount: act.Amount, + Price: act.Price, + Time: act.Time, + Symbol: act.Symbol, + } + log.Infof("TradeEvent local stopShort order trigger: %#v", newAct) + deleteOrders = append(deleteOrders, id) + b.actChan <- newAct + return true + } + return true + }) + for _, v := range deleteOrders { + b.stopOrders.Delete(v) + } +} + +func (b *TradeExchange) onEventWatch(e *Event) (err error) { + if e.Name == "candle" { + return b.onEventCandleParam(e) + } + + param := e.GetData().(*WatchParam) + switch param.Type { + case EventTradeMarket: + b.impl.Watch(exchange.WatchParam{Type: exchange.WatchTypeTradeMarket, Param: map[string]string{"symbol": param.Extra.(string)}}, func(data interface{}) { + b.datas <- data + }) + case EventDepth: + b.impl.Watch(exchange.WatchParam{Type: exchange.WatchTypeDepth, Param: map[string]string{"symbol": param.Extra.(string)}}, func(data interface{}) { + b.datas <- data + }) + default: + log.Errorf("TradeExchange OnEventWatch unsupport type: %s %##v", param.Type, param) + } + return +} + +// orderRoutine process order routine +func (b *TradeExchange) orderRoutine() { + var err error + var ret interface{} + var exist bool + for v := range b.actChan { + // hook the stop order when localStopOrder enabled + if v.Action.IsStop() && b.localStopOrder { + b.stopOrders.Store(v.ID, v) + continue + } else if v.Action == trademodel.CancelAll { + b.cancelAllOrder() + b.stopOrders = sync.Map{} + continue + } else if v.Action == trademodel.CancelOne { + _, exist = b.stopOrders.LoadAndDelete(v.ID) + if exist { + continue + } + oi, ok := b.localOrderIndex[v.ID] + if !ok { + log.Errorf("local order: %s not found", v.ID) + continue + } + _, err = doOrderWithRetry(10, func() (interface{}, error) { + return b.impl.CancelOrder(&oi.Order) + }) + if err != nil { + log.Errorf("cancel order local %s, id %s failed: %s", oi.LocalID, oi.OrderID, err.Error()) + } + continue + } + ret, err = doOrderWithRetry(10, func() (interface{}, error) { + order, e := b.impl.ProcessOrder(v) + return order, e + }) + if err == nil { + od := ret.(*Order) + oi := &OrderInfo{Order: *od, Action: v.Action, LocalID: v.ID} + b.orders[od.OrderID] = oi + b.localOrderIndex[v.ID] = oi + } else { + tr := Trade{ID: v.ID, + Action: v.Action, + Time: v.Time, + Price: v.Price, + Amount: v.Amount, + // Side: v.Action, + Remark: "failed:" + err.Error()} + b.Send(v.ID, EventTrade, &tr) + } + + } +} + +func (b *TradeExchange) cancelAllOrder() { + ret, err := doOrderWithRetry(10, func() (interface{}, error) { + orders, err := b.impl.CancelAllOrders() + return orders, err + }) + if err != nil { + log.Errorf("cancel allorder error %s", err.Error()) + return + } + log.Info("cancel order:", ret) +} + +func (b *TradeExchange) emitCandles(param CandleParam) { + if param.BinSize != "1m" { + log.Info("TradeExchange emit candle binsize not 1m:", param) + return + } + watchParam := exchange.WatchCandle(param.Symbol, param.BinSize) + b.candleParam = param + err := b.impl.Watch(watchParam, func(data interface{}) { + candle := data.(*Candle) + b.datas <- candle + }) + if err != nil { + log.Errorf("emitCandles wathKline failed:", err.Error()) + return + } +} + +func (b *TradeExchange) emitRecentCandles(param CandleParam) (tLast int64, err error) { + klines, errCh := exchange.KlineChan(b.impl, param.Symbol, param.BinSize, param.Start, param.End) + for v := range klines { + tLast = v.Start + b.SendWithExtra("recent", EventCandle, v, param.BinSize) + } + err = <-errCh + return +} diff --git a/pkg/process/exchange/interface.go b/pkg/process/exchange/interface.go new file mode 100644 index 0000000..c345550 --- /dev/null +++ b/pkg/process/exchange/interface.go @@ -0,0 +1,19 @@ +package exchange + +import ( + "fmt" + + "git.qtrade.icu/coin-quant/exchange" + "github.com/spf13/viper" +) + +func GetTradeExchange(name string, cfg *viper.Viper, cltName, symbol string) (t *TradeExchange, err error) { + ex, err := exchange.NewExchange(name, exchange.WrapViper(cfg), cltName) + if err != nil { + return + } + t = NewTradeExchange(name, ex, symbol) + localStop := cfg.GetBool(fmt.Sprintf("exchanges.%s.localstop", cltName)) + t.UseLocalStopOrder(localStop) + return +} diff --git a/pkg/process/goscript/engine/engine.go b/pkg/process/goscript/engine/engine.go new file mode 100644 index 0000000..1bf3dde --- /dev/null +++ b/pkg/process/goscript/engine/engine.go @@ -0,0 +1,187 @@ +package engine + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/indicator" + log "github.com/sirupsen/logrus" +) + +const letterBytes = "abcdefghijklmnopqrstuvwxyz123456789" + +func randStringBytes(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + return string(b) +} + +func init() { + rand.Seed(time.Now().Unix()) +} + +func getActionID() string { + return randStringBytes(8) +} + +type EngineImpl struct { + proc *BaseProcesser + pos float64 + posPrice float64 + balance float64 + merges map[string][]*KlinePlugin + mergesMutex sync.Mutex + symbol string +} + +type UpdateStatusFn func(vm string, status int, msg string) +type EngineWrapper struct { + *EngineImpl + VmID string + Cb UpdateStatusFn +} + +func (e *EngineWrapper) UpdateStatus(status int, msg string) { + e.Cb(e.VmID, status, msg) +} + +func (e *EngineWrapper) Merge(src, dst string, fn common.CandleFn) { + e.EngineImpl.Merge(e.VmID, src, dst, fn) +} + +func (e *EngineWrapper) CleanMerges() { + e.EngineImpl.RemoveMerge(e.VmID) +} + +func NewEngineWrapper(proc *BaseProcesser, cb UpdateStatusFn, symbol string, id string) *EngineWrapper { + return &EngineWrapper{EngineImpl: NewEngineImpl(proc, symbol), Cb: cb, VmID: id} +} + +func NewEngineImpl(proc *BaseProcesser, symbol string) *EngineImpl { + e := new(EngineImpl) + e.merges = make(map[string][]*KlinePlugin) + e.symbol = symbol + e.proc = proc + return e +} + +func (e *EngineImpl) OpenLong(price, amount float64) string { + return e.addOrder(price, amount, OpenLong) +} +func (e *EngineImpl) CloseLong(price, amount float64) string { + return e.addOrder(price, amount, CloseLong) +} +func (e *EngineImpl) OpenShort(price, amount float64) string { + return e.addOrder(price, amount, OpenShort) +} +func (e *EngineImpl) CloseShort(price, amount float64) string { + return e.addOrder(price, amount, CloseShort) +} +func (e *EngineImpl) StopLong(price, amount float64) string { + return e.addOrder(price, amount, StopLong) +} +func (e *EngineImpl) StopShort(price, amount float64) string { + return e.addOrder(price, amount, StopShort) +} + +func (e *EngineImpl) DoOrder(typ TradeType, price, amount float64) string { + return e.addOrder(price, amount, typ) +} + +func (e *EngineImpl) CancelAllOrder() { + e.proc.Send(EventOrder, EventOrder, &TradeAction{Action: CancelAll}) +} + +func (e *EngineImpl) CancelOrder(id string) { + e.proc.Send(EventOrder, EventOrder, &TradeAction{Action: CancelOne, ID: id}) +} + +func (e *EngineImpl) AddIndicator(name string, params ...int) (ind indicator.CommonIndicator) { + var err error + ind, err = indicator.NewCommonIndicator(name, params...) + if err != nil { + log.Errorf("ScriptEngineImpl addIndicator failed %s %v", name, params) + return nil + } + return +} +func (e *EngineImpl) UpdatePosition(pos, price float64) { + e.pos = pos + e.posPrice = price +} + +func (e *EngineImpl) Position() (float64, float64) { + return e.pos, e.posPrice +} + +func (e *EngineImpl) Log(v ...interface{}) { + fmt.Println(v...) +} + +func (e *EngineImpl) addOrder(price, amount float64, orderType TradeType) (id string) { + // FixMe: in backtest, time may be the time of candle + id = getActionID() + act := TradeAction{ID: id, Action: orderType, Symbol: e.symbol, Amount: amount, Price: price, Time: time.Now()} + e.proc.Send(EventOrder, EventOrder, &act) + return +} + +func (e *EngineImpl) Watch(watchType string) { + param := WatchParam{Type: watchType} + e.proc.Send(EventWatch, EventWatch, ¶m) +} + +func (e *EngineImpl) SendNotify(title, content, contentType string) { + if contentType == "" { + contentType = "text" + } + data := NotifyEvent{Title: title, Type: contentType, Content: content} + e.proc.Send("notify", EventNotify, &data) +} + +func (e *EngineImpl) SetBalance(balance float64) { + e.proc.Send("balance", "init_balance", map[string]interface{}{"balance": balance}) +} + +func (e *EngineImpl) Balance() (balance float64) { + return e.balance +} + +func (e *EngineImpl) Merge(vmID, src, dst string, fn common.CandleFn) { + e.mergesMutex.Lock() + defer e.mergesMutex.Unlock() + kp := NewKlinePlugin(src, dst, fn) + ms, ok := e.merges[vmID] + if ok { + e.merges[vmID] = append(ms, kp) + } else { + e.merges[vmID] = []*KlinePlugin{kp} + } +} + +func (e *EngineImpl) RemoveMerge(vmID string) { + e.mergesMutex.Lock() + defer e.mergesMutex.Unlock() + delete(e.merges, vmID) +} + +func (e *EngineImpl) OnCandle(candle *Candle) { + for _, kls := range e.merges { + for _, v := range kls { + v.Update(candle) + } + } +} + +func (e *EngineImpl) UpdateBalance(balance float64) { + e.balance = balance +} + +func (e *EngineImpl) UpdateStatus(status int, msg string) { + log.Error("EngineImpl UpdateStatus, never called") +} diff --git a/pkg/process/goscript/engine/kline.go b/pkg/process/goscript/engine/kline.go new file mode 100644 index 0000000..4b6666c --- /dev/null +++ b/pkg/process/goscript/engine/kline.go @@ -0,0 +1,42 @@ +package engine + +import ( + "git.qtrade.icu/coin-quant/base/common" + + log "github.com/sirupsen/logrus" +) + +type KlinePlugin struct { + kl *common.KlineMerge + cb common.CandleFn + bRecent bool +} + +func NewKlinePlugin(src, dst string, fn common.CandleFn) (kp *KlinePlugin) { + kp = new(KlinePlugin) + kp.cb = fn + kp.kl = common.NewKlineMergeStr(src, dst) + return +} + +func (kp *KlinePlugin) Update(candle *Candle) { + if candle.ID == -1 { + kp.bRecent = true + } else { + kp.bRecent = false + } + ret := kp.kl.Update(candle) + if ret == nil { + return + } + if kp.cb == nil { + log.Error("KlinePlugin callback is nil") + return + } + if kp.bRecent { + temp := ret.(*Candle) + temp.ID = -1 + } + newCandle := ret.(*Candle) + kp.cb(newCandle) +} diff --git a/pkg/process/goscript/engine/runner.go b/pkg/process/goscript/engine/runner.go new file mode 100644 index 0000000..6e41bcc --- /dev/null +++ b/pkg/process/goscript/engine/runner.go @@ -0,0 +1,42 @@ +package engine + +import ( + "fmt" + "path/filepath" + + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/base/engine" +) + +type NewRunnerFn func(file string) (r Runner, err error) + +var ( + factory = map[string]NewRunnerFn{} +) + +func Register(ext string, fn NewRunnerFn) { + factory[ext] = fn +} + +type Runner interface { + Param() (paramInfo []common.Param, err error) + Init(engine engine.Engine, params common.ParamData) (err error) + OnCandle(candle *Candle) (err error) + OnPosition(pos, price float64) (err error) + OnTrade(trade *Trade) (err error) + OnTradeMarket(trade *Trade) (err error) + OnDepth(depth *Depth) (err error) + OnEvent(e *Event) (err error) + GetName() string +} + +func NewRunner(file string) (r Runner, err error) { + ext := filepath.Ext(file) + f, ok := factory[ext] + if !ok { + err = fmt.Errorf("unsupport file format: %s", ext) + return + } + r, err = f(file) + return +} diff --git a/pkg/process/goscript/goengine.go b/pkg/process/goscript/goengine.go new file mode 100644 index 0000000..18a12fe --- /dev/null +++ b/pkg/process/goscript/goengine.go @@ -0,0 +1,283 @@ +package goscript + +import ( + "fmt" + "sync" + "sync/atomic" + + "git.qtrade.icu/coin-quant/base/common" + bengine "git.qtrade.icu/coin-quant/base/engine" + "git.qtrade.icu/coin-quant/trade/pkg/process/goscript/engine" + + log "github.com/sirupsen/logrus" +) + +type scriptInfo struct { + engine.Runner + params common.ParamData + wrap *engine.EngineWrapper +} + +type Status struct { + Name string + Status int + Msg string +} + +type GoEngine struct { + BaseProcesser + engine *engine.EngineWrapper + vms map[string]*scriptInfo + mutex sync.Mutex + started int32 + statusCh chan *Status +} + +func NewDefaultGoEngine() (s *GoEngine, err error) { + return NewGoEngine("") +} +func NewGoEngine(symbol string) (s *GoEngine, err error) { + s = new(GoEngine) + s.Name = "multi_script" + s.vms = make(map[string]*scriptInfo) + s.engine = engine.NewEngineWrapper(&s.BaseProcesser, nil, symbol, "") + return +} + +func (s *GoEngine) SetStatusCh(ch chan *Status) { + s.statusCh = ch +} + +func (s *GoEngine) Init(bus *Bus) (err error) { + s.BaseProcesser.Init(bus) + s.Subscribe(EventCandle, s.onEventCandle) + s.Subscribe(EventTrade, s.onEventTrade) + s.Subscribe(EventPosition, s.onEventPosition) + s.Subscribe(EventTradeMarket, s.onEventTradeMarket) + s.Subscribe(EventDepth, s.onEventDepth) + s.Subscribe(EventBalance, s.onEventBalance) + return +} + +func (s *GoEngine) Start() (err error) { + atomic.StoreInt32(&s.started, 1) + for k, v := range s.vms { + tempEng := engine.EngineWrapper{EngineImpl: s.engine.EngineImpl, VmID: k} + tempEng.Cb = s.updateScriptStatus + v.wrap = &tempEng + err = v.Init(&tempEng, v.params) + if err != nil { + return err + } + } + return +} + +func (s *GoEngine) ScriptCount() int { + return len(s.vms) +} + +func (s *GoEngine) Stop() (err error) { + return +} + +func (s *GoEngine) RemoveScript(name string) (err error) { + s.mutex.Lock() + defer s.mutex.Unlock() + return s.doRemoveScript(name) +} + +func (s *GoEngine) doRemoveScript(name string) (err error) { + vm, ok := s.vms[name] + if !ok { + log.Warnf("%s script not exist", name) + return + } + vm.wrap.CleanMerges() + delete(s.vms, name) + return +} + +func (s *GoEngine) AddScript(name, src, param string) (err error) { + err = s.doAddScript(name, src, param) + return +} +func (s *GoEngine) doAddScript(name, src, param string) (err error) { + log.Info("GoEngine doAddScript:", name, src, param) + s.mutex.Lock() + defer s.mutex.Unlock() + _, ok := s.vms[name] + if ok { + err = fmt.Errorf("%s script aleady exist", name) + return + } + r, err := engine.NewRunner(src) + if err != nil { + err = fmt.Errorf("AddScript %s %s error: %w", name, src, err) + return + } + paramInfo, err := r.Param() + if err != nil { + err = fmt.Errorf("AddScript %s %s get Params error: %w", name, src, err) + return + } + paramData := make(common.ParamData) + if param != "" { + paramData, err = common.ParseParams(param, paramInfo) + if err != nil { + err = fmt.Errorf("AddScript %s %s ParseParams error: %w", name, src, err) + return + } + } + // var fnName string + si := scriptInfo{Runner: r, params: paramData} + s.vms[name] = &si + isStart := atomic.LoadInt32(&s.started) + if isStart == 1 { + tempEng := engine.EngineWrapper{EngineImpl: s.engine.EngineImpl, VmID: name} + tempEng.Cb = s.updateScriptStatus + si.wrap = &tempEng + err = si.Runner.Init(&tempEng, paramData) + if err != nil { + log.Errorf("GoEngine doAddScript Init failed:", err.Error()) + return err + } + } + return +} + +func (s *GoEngine) onTrade(trade *Trade) { + s.mutex.Lock() + defer s.mutex.Unlock() + for _, vm := range s.vms { + vm.OnTrade(trade) + } + +} + +func (s *GoEngine) onPosition(pos *Position) { + log.Debug("on position:", pos.Hold) + posHold, _ := s.engine.Position() + if posHold == pos.Hold { + return + } + s.mutex.Lock() + defer s.mutex.Unlock() + s.engine.UpdatePosition(pos.Hold, pos.Price) + for _, vm := range s.vms { + vm.OnPosition(pos.Hold, pos.Price) + } +} + +func (s *GoEngine) onBalance(balance float64) { + if s.engine.Balance() == balance { + return + } + s.mutex.Lock() + defer s.mutex.Unlock() + s.engine.UpdateBalance(balance) +} + +func (s *GoEngine) onCandle(name, binSize string, candle *Candle) { + s.mutex.Lock() + defer s.mutex.Unlock() + for _, vm := range s.vms { + vm.OnCandle(candle) + } + s.engine.OnCandle(candle) +} + +func (s *GoEngine) onTradeMarket(th *Trade) { + s.mutex.Lock() + defer s.mutex.Unlock() + for _, vm := range s.vms { + vm.OnTradeMarket(th) + } +} + +func (s *GoEngine) onDepth(depth *Depth) { + s.mutex.Lock() + defer s.mutex.Unlock() + for _, vm := range s.vms { + vm.OnDepth(depth) + } +} + +func (s *GoEngine) onEventCandle(e *Event) (err error) { + ret, ok := e.GetData().(*Candle) + if !ok { + log.Errorf("onEventCandle type error: %##v", e.GetData()) + return + } + + name := e.GetName() + binSize := e.GetExtra().(string) + if name == "recent" { + ret.ID = -1 + } + s.onCandle(name, binSize, ret) + return +} + +func (s *GoEngine) onEventTrade(e *Event) (err error) { + tr, ok := e.GetData().(*Trade) + if !ok { + log.Errorf("onEventTrade type error: %##v", e.GetData()) + return + } + s.onTrade(tr) + return +} + +func (s *GoEngine) onEventPosition(e *Event) (err error) { + pos, ok := e.GetData().(*Position) + if !ok { + log.Errorf("onEventPosition type error: %##v", e.GetData()) + return + } + s.onPosition(pos) + return +} +func (s *GoEngine) onEventTradeMarket(e *Event) (err error) { + th, ok := e.GetData().(*Trade) + if !ok { + log.Errorf("onEventTradeMarket type error: %##v", e.GetData()) + return + } + s.onTradeMarket(th) + return +} + +func (s *GoEngine) onEventDepth(e *Event) (err error) { + depth, ok := e.GetData().(*Depth) + if !ok { + log.Errorf("onEventDepth type error: %##v", e.GetData()) + return + } + s.onDepth(depth) + return +} + +func (s *GoEngine) onEventBalance(e *Event) (err error) { + balance, ok := e.GetData().(*Balance) + if !ok { + log.Errorf("onEventBalance type error: %##v", e.GetData()) + return + } + s.onBalance(balance.Balance) + return +} + +func (s *GoEngine) updateScriptStatus(name string, status int, msg string) { + // call in script, no need lock + switch status { + case bengine.StatusRunning: + case bengine.StatusSuccess, bengine.StatusFail: + s.doRemoveScript(name) + default: + log.Errorf("GoEngine updateScriptStatus script %s unknown status: %d,", name, status) + } + if s.statusCh != nil { + s.statusCh <- &Status{Name: name, Status: status, Msg: msg} + } +} diff --git a/pkg/process/goscript/igo/common.go b/pkg/process/goscript/igo/common.go new file mode 100644 index 0000000..42211f4 --- /dev/null +++ b/pkg/process/goscript/igo/common.go @@ -0,0 +1,81 @@ +// export by github.com/goplus/igop/cmd/qexp + +package igo + +import ( + q "git.qtrade.icu/coin-quant/base/common" + + "go/constant" + "reflect" + + "github.com/goplus/igop" +) + +func init() { + igop.RegisterPackage(&igop.Package{ + Name: "common", + Path: "git.qtrade.icu/coin-quant/base/common", + Deps: map[string]string{ + "bufio": "bufio", + "errors": "errors", + "fmt": "fmt", + "github.com/bitly/go-simplejson": "simplejson", + "github.com/shopspring/decimal": "decimal", + "github.com/sirupsen/logrus": "logrus", + "git.qtrade.icu/coin-quant/trademodel": "trademodel", + "io": "io", + "os": "os", + "os/exec": "exec", + "path/filepath": "filepath", + "regexp": "regexp", + "runtime": "runtime", + "strconv": "strconv", + "strings": "strings", + "time": "time", + }, + Interfaces: map[string]reflect.Type{}, + NamedTypes: map[string]reflect.Type{ + "CandleFn": reflect.TypeOf((*q.CandleFn)(nil)).Elem(), + "Entry": reflect.TypeOf((*q.Entry)(nil)).Elem(), + "KlineMerge": reflect.TypeOf((*q.KlineMerge)(nil)).Elem(), + "LeverBalance": reflect.TypeOf((*q.LeverBalance)(nil)).Elem(), + "Param": reflect.TypeOf((*q.Param)(nil)).Elem(), + "ParamData": reflect.TypeOf((*q.ParamData)(nil)).Elem(), + "VBalance": reflect.TypeOf((*q.VBalance)(nil)).Elem(), + }, + AliasTypes: map[string]reflect.Type{}, + Vars: map[string]reflect.Value{ + "Day": reflect.ValueOf(&q.Day), + "ErrNoBalance": reflect.ValueOf(&q.ErrNoBalance), + "Week": reflect.ValueOf(&q.Week), + }, + Funcs: map[string]reflect.Value{ + "BoolParam": reflect.ValueOf(q.BoolParam), + "Copy": reflect.ValueOf(q.Copy), + "CopyWithMainPkg": reflect.ValueOf(q.CopyWithMainPkg), + "FloatAdd": reflect.ValueOf(q.FloatAdd), + "FloatDiv": reflect.ValueOf(q.FloatDiv), + "FloatMul": reflect.ValueOf(q.FloatMul), + "FloatParam": reflect.ValueOf(q.FloatParam), + "FloatSub": reflect.ValueOf(q.FloatSub), + "FormatFloat": reflect.ValueOf(q.FormatFloat), + "GetBinSizeDuration": reflect.ValueOf(q.GetBinSizeDuration), + "GetExecDir": reflect.ValueOf(q.GetExecDir), + "IntParam": reflect.ValueOf(q.IntParam), + "MergeKlineChan": reflect.ValueOf(q.MergeKlineChan), + "NewKlineMerge": reflect.ValueOf(q.NewKlineMerge), + "NewKlineMergeStr": reflect.ValueOf(q.NewKlineMergeStr), + "NewLeverBalance": reflect.ValueOf(q.NewLeverBalance), + "NewVBalance": reflect.ValueOf(q.NewVBalance), + "OpenURL": reflect.ValueOf(q.OpenURL), + "ParseBinSizes": reflect.ValueOf(q.ParseBinSizes), + "ParseBinStrs": reflect.ValueOf(q.ParseBinStrs), + "ParseParams": reflect.ValueOf(q.ParseParams), + "StringParam": reflect.ValueOf(q.StringParam), + }, + TypedConsts: map[string]igop.TypedConst{}, + UntypedConsts: map[string]igop.UntypedConst{ + "DefaultBinSizes": {"untyped string", constant.MakeString(string(q.DefaultBinSizes))}, + }, + }) +} diff --git a/pkg/process/goscript/igo/engine.go b/pkg/process/goscript/igo/engine.go new file mode 100644 index 0000000..5bc0cc9 --- /dev/null +++ b/pkg/process/goscript/igo/engine.go @@ -0,0 +1,37 @@ +// export by github.com/goplus/igop/cmd/qexp + +package igo + +import ( + q "git.qtrade.icu/coin-quant/base/engine" + + "go/constant" + "reflect" + + "github.com/goplus/igop" +) + +func init() { + igop.RegisterPackage(&igop.Package{ + Name: "engine", + Path: "git.qtrade.icu/coin-quant/base/engine", + Deps: map[string]string{ + "git.qtrade.icu/coin-quant/base/common": "common", + "git.qtrade.icu/coin-quant/indicator": "indicator", + "git.qtrade.icu/coin-quant/trademodel": "trademodel", + }, + Interfaces: map[string]reflect.Type{ + "Engine": reflect.TypeOf((*q.Engine)(nil)).Elem(), + }, + NamedTypes: map[string]reflect.Type{}, + AliasTypes: map[string]reflect.Type{}, + Vars: map[string]reflect.Value{}, + Funcs: map[string]reflect.Value{}, + TypedConsts: map[string]igop.TypedConst{}, + UntypedConsts: map[string]igop.UntypedConst{ + "StatusFail": {"untyped int", constant.MakeInt64(int64(q.StatusFail))}, + "StatusRunning": {"untyped int", constant.MakeInt64(int64(q.StatusRunning))}, + "StatusSuccess": {"untyped int", constant.MakeInt64(int64(q.StatusSuccess))}, + }, + }) +} diff --git a/pkg/process/goscript/igo/fsm.go b/pkg/process/goscript/igo/fsm.go new file mode 100644 index 0000000..905e968 --- /dev/null +++ b/pkg/process/goscript/igo/fsm.go @@ -0,0 +1,35 @@ +// export by github.com/goplus/igop/cmd/qexp + +package igo + +import ( + q "git.qtrade.icu/coin-quant/base/fsm" + + "reflect" + + "github.com/goplus/igop" +) + +func init() { + igop.RegisterPackage(&igop.Package{ + Name: "fsm", + Path: "git.qtrade.icu/coin-quant/base/fsm", + Deps: map[string]string{ + "fmt": "fmt", + }, + Interfaces: map[string]reflect.Type{}, + NamedTypes: map[string]reflect.Type{ + "Callback": reflect.TypeOf((*q.Callback)(nil)).Elem(), + "EventDesc": reflect.TypeOf((*q.EventDesc)(nil)).Elem(), + "FSM": reflect.TypeOf((*q.FSM)(nil)).Elem(), + "Rule": reflect.TypeOf((*q.Rule)(nil)).Elem(), + }, + AliasTypes: map[string]reflect.Type{}, + Vars: map[string]reflect.Value{}, + Funcs: map[string]reflect.Value{ + "NewFSM": reflect.ValueOf(q.NewFSM), + }, + TypedConsts: map[string]igop.TypedConst{}, + UntypedConsts: map[string]igop.UntypedConst{}, + }) +} diff --git a/pkg/process/goscript/igo/igo.go b/pkg/process/goscript/igo/igo.go new file mode 100644 index 0000000..505a610 --- /dev/null +++ b/pkg/process/goscript/igo/igo.go @@ -0,0 +1,152 @@ +package igo + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/trade/pkg/process/goscript/engine" + "github.com/goplus/igop" + _ "github.com/goplus/igop/pkg/encoding/json" + _ "github.com/goplus/igop/pkg/errors" + _ "github.com/goplus/igop/pkg/fmt" + _ "github.com/goplus/igop/pkg/math" + _ "github.com/goplus/igop/pkg/net/http" + _ "github.com/goplus/igop/pkg/time" + "golang.org/x/tools/go/ssa" +) + +func init() { + igop.RegisterCustomBuiltin("min", min) + igop.RegisterCustomBuiltin("max", max) + igop.RegisterCustomBuiltin("formatFloat", common.FormatFloat) + igop.RegisterCustomBuiltin("FloatAdd", common.FloatAdd) + igop.RegisterCustomBuiltin("FloatSub", common.FloatSub) + igop.RegisterCustomBuiltin("FloatMul", common.FloatMul) + igop.RegisterCustomBuiltin("FloatDiv", common.FloatDiv) + igop.RegisterCustomBuiltin("StringParam", common.StringParam) + igop.RegisterCustomBuiltin("FloatParam", common.FloatParam) + igop.RegisterCustomBuiltin("IntParam", common.IntParam) + igop.RegisterCustomBuiltin("BoolParam", common.BoolParam) +} +func min(a, b float64) float64 { + if a < b { + return a + } + return b +} + +func max(a, b float64) float64 { + if a > b { + return a + } + return b +} + +var ( + fixImport = ` +import ( + "git.qtrade.icu/coin-quant/base/engine" + "git.qtrade.icu/coin-quant/base/common" +) +` + + fixType = ` +type Engine = engine.Engine +type Param = common.Param +type ParamData = common.ParamData +` +) + +func fixSource(file string) (ret string, err error) { + f, err := os.Open(file) + if err != nil { + return + } + defer f.Close() + r := bufio.NewReader(f) + var temp bytes.Buffer + var l []byte + var bFixType bool + for { + l, err = r.ReadBytes('\n') + if err != nil { + break + } + + if bytes.HasPrefix(l, []byte("package")) { + temp.Write(l) + temp.WriteString(fixImport) + } else if !bFixType && bytes.HasPrefix(l, []byte("type")) { + temp.WriteString(fixType) + temp.Write(l) + bFixType = true + } else { + temp.Write(l) + } + } + if err == io.EOF { + err = nil + } + ret = temp.String() + return +} + +func NewRunner(file string) (r engine.Runner, err error) { + source, err := fixSource(file) + if err != nil { + return + } + ctx := igop.NewContext(0) + pkg, err := ctx.LoadFile(filepath.Base(file), source) + if err != nil { + err = fmt.Errorf("igop parse file failed: %s", err.Error()) + return + } + // pkg.Members["Engine"] = s + var typs []string + for k, v := range pkg.Members { + _, ok := v.(*ssa.Type) + if !ok { + continue + } + typs = append(typs, k) + } + + interp, err := ctx.NewInterp(pkg) + if err != nil { + err = fmt.Errorf("igop NewInterp failed: %s", err.Error()) + return + } + + var ok bool + var temp igoImpl + var fn interface{} + var name string + for _, v := range typs { + fn, ok = interp.GetFunc(fmt.Sprintf("New%s", v)) + if !ok { + continue + } + name = v + rets := reflect.ValueOf(fn).Call([]reflect.Value{}) + if len(rets) != 1 { + continue + } + temp, ok = rets[0].Interface().(igoImpl) + if ok { + break + } + } + if temp == nil { + err = fmt.Errorf("no available script impl") + return + } + r = &igoRunner{impl: temp, name: name} + return +} diff --git a/pkg/process/goscript/igo/indicator.go b/pkg/process/goscript/igo/indicator.go new file mode 100644 index 0000000..4fd97e7 --- /dev/null +++ b/pkg/process/goscript/igo/indicator.go @@ -0,0 +1,71 @@ +// export by github.com/goplus/igop/cmd/qexp + +package igo + +import ( + q "git.qtrade.icu/coin-quant/indicator" + + "reflect" + + "github.com/goplus/igop" +) + +func init() { + igop.RegisterPackage(&igop.Package{ + Name: "indicator", + Path: "git.qtrade.icu/coin-quant/indicator", + Deps: map[string]string{ + "encoding/json": "json", + "fmt": "fmt", + "github.com/shopspring/decimal": "decimal", + "math": "math", + "reflect": "reflect", + "strings": "strings", + }, + Interfaces: map[string]reflect.Type{ + "CommonIndicator": reflect.TypeOf((*q.CommonIndicator)(nil)).Elem(), + "Crosser": reflect.TypeOf((*q.Crosser)(nil)).Elem(), + "Indicator": reflect.TypeOf((*q.Indicator)(nil)).Elem(), + "Updater": reflect.TypeOf((*q.Updater)(nil)).Elem(), + }, + NamedTypes: map[string]reflect.Type{ + "Boll": reflect.TypeOf((*q.Boll)(nil)).Elem(), + "CrossTool": reflect.TypeOf((*q.CrossTool)(nil)).Elem(), + "EMA": reflect.TypeOf((*q.EMA)(nil)).Elem(), + "JsonIndicator": reflect.TypeOf((*q.JsonIndicator)(nil)).Elem(), + "MABase": reflect.TypeOf((*q.MABase)(nil)).Elem(), + "MACD": reflect.TypeOf((*q.MACD)(nil)).Elem(), + "MAGroup": reflect.TypeOf((*q.MAGroup)(nil)).Elem(), + "Mixed": reflect.TypeOf((*q.Mixed)(nil)).Elem(), + "NewCommonIndicatorFunc": reflect.TypeOf((*q.NewCommonIndicatorFunc)(nil)).Elem(), + "RSI": reflect.TypeOf((*q.RSI)(nil)).Elem(), + "SMA": reflect.TypeOf((*q.SMA)(nil)).Elem(), + "SMMA": reflect.TypeOf((*q.SMMA)(nil)).Elem(), + "Stoch": reflect.TypeOf((*q.Stoch)(nil)).Elem(), + "StochRSI": reflect.TypeOf((*q.StochRSI)(nil)).Elem(), + }, + AliasTypes: map[string]reflect.Type{}, + Vars: map[string]reflect.Value{ + "ExtraIndicators": reflect.ValueOf(&q.ExtraIndicators), + }, + Funcs: map[string]reflect.Value{ + "NewBoll": reflect.ValueOf(q.NewBoll), + "NewCommonIndicator": reflect.ValueOf(q.NewCommonIndicator), + "NewCrossTool": reflect.ValueOf(q.NewCrossTool), + "NewEMA": reflect.ValueOf(q.NewEMA), + "NewJsonIndicator": reflect.ValueOf(q.NewJsonIndicator), + "NewMACD": reflect.ValueOf(q.NewMACD), + "NewMACDWithSMA": reflect.ValueOf(q.NewMACDWithSMA), + "NewMAGroup": reflect.ValueOf(q.NewMAGroup), + "NewMixed": reflect.ValueOf(q.NewMixed), + "NewRSI": reflect.ValueOf(q.NewRSI), + "NewSMA": reflect.ValueOf(q.NewSMA), + "NewSMMA": reflect.ValueOf(q.NewSMMA), + "NewStoch": reflect.ValueOf(q.NewStoch), + "NewStochRSI": reflect.ValueOf(q.NewStochRSI), + "RegisterIndicator": reflect.ValueOf(q.RegisterIndicator), + }, + TypedConsts: map[string]igop.TypedConst{}, + UntypedConsts: map[string]igop.UntypedConst{}, + }) +} diff --git a/pkg/process/goscript/igo/init.go b/pkg/process/goscript/igo/init.go new file mode 100644 index 0000000..d83102c --- /dev/null +++ b/pkg/process/goscript/igo/init.go @@ -0,0 +1,10 @@ +package igo + +import ( + "git.qtrade.icu/coin-quant/trade/pkg/process/goscript/engine" +) + +func init() { + engine.Register(".go", NewRunner) + engine.Register(".gop", NewRunner) +} diff --git a/pkg/process/goscript/igo/runner.go b/pkg/process/goscript/igo/runner.go new file mode 100644 index 0000000..3a368ab --- /dev/null +++ b/pkg/process/goscript/igo/runner.go @@ -0,0 +1,67 @@ +package igo + +import ( + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/base/engine" + + . "git.qtrade.icu/coin-quant/trademodel" +) + +type igoImpl interface { + Param() (paramInfo []common.Param) + Init(engine engine.Engine, params common.ParamData) error + OnCandle(candle *Candle) + OnPosition(pos, price float64) + OnTrade(trade *Trade) + OnTradeMarket(trade *Trade) + OnDepth(depth *Depth) + // OnEvent(e *Event) + // GetName() string +} + +type igoRunner struct { + name string + impl igoImpl +} + +func (r *igoRunner) Param() (paramInfo []common.Param, err error) { + paramInfo = r.impl.Param() + return +} + +func (r *igoRunner) Init(engine engine.Engine, params common.ParamData) (err error) { + return r.impl.Init(engine, params) +} + +func (r *igoRunner) OnCandle(candle *Candle) (err error) { + r.impl.OnCandle(candle) + return +} + +func (r *igoRunner) OnPosition(pos, price float64) (err error) { + r.impl.OnPosition(pos, price) + return +} + +func (r *igoRunner) OnTrade(trade *Trade) (err error) { + r.impl.OnTrade(trade) + return +} + +func (r *igoRunner) OnTradeMarket(trade *Trade) (err error) { + r.impl.OnTradeMarket(trade) + return +} + +func (r *igoRunner) OnDepth(depth *Depth) (err error) { + r.impl.OnDepth(depth) + return +} + +func (r *igoRunner) OnEvent(e *Event) (err error) { + return +} + +func (r *igoRunner) GetName() string { + return r.name +} diff --git a/pkg/process/goscript/igo/trademodel.go b/pkg/process/goscript/igo/trademodel.go new file mode 100644 index 0000000..1e14589 --- /dev/null +++ b/pkg/process/goscript/igo/trademodel.go @@ -0,0 +1,73 @@ +// export by github.com/goplus/igop/cmd/qexp + +package igo + +import ( + q "git.qtrade.icu/coin-quant/trademodel" + + "go/constant" + "reflect" + + "github.com/goplus/igop" +) + +func init() { + igop.RegisterPackage(&igop.Package{ + Name: "trademodel", + Path: "git.qtrade.icu/coin-quant/trademodel", + Deps: map[string]string{ + "fmt": "fmt", + "strings": "strings", + "time": "time", + }, + Interfaces: map[string]reflect.Type{}, + NamedTypes: map[string]reflect.Type{ + "Balance": reflect.TypeOf((*q.Balance)(nil)).Elem(), + "Candle": reflect.TypeOf((*q.Candle)(nil)).Elem(), + "CandleList": reflect.TypeOf((*q.CandleList)(nil)).Elem(), + "Currency": reflect.TypeOf((*q.Currency)(nil)).Elem(), + "Depth": reflect.TypeOf((*q.Depth)(nil)).Elem(), + "DepthInfo": reflect.TypeOf((*q.DepthInfo)(nil)).Elem(), + "Order": reflect.TypeOf((*q.Order)(nil)).Elem(), + "Orderbook": reflect.TypeOf((*q.OrderBook)(nil)).Elem(), + "Position": reflect.TypeOf((*q.Position)(nil)).Elem(), + "Symbol": reflect.TypeOf((*q.Symbol)(nil)).Elem(), + "Ticker": reflect.TypeOf((*q.Ticker)(nil)).Elem(), + "Trade": reflect.TypeOf((*q.Trade)(nil)).Elem(), + "TradeAction": reflect.TypeOf((*q.TradeAction)(nil)).Elem(), + "TradeType": reflect.TypeOf((*q.TradeType)(nil)).Elem(), + }, + AliasTypes: map[string]reflect.Type{}, + Vars: map[string]reflect.Value{ + "OrderStatusCanceled": reflect.ValueOf(&q.OrderStatusCanceled), + "OrderStatusFilled": reflect.ValueOf(&q.OrderStatusFilled), + }, + Funcs: map[string]reflect.Value{ + "NewTradeType": reflect.ValueOf(q.NewTradeType), + }, + TypedConsts: map[string]igop.TypedConst{ + "CancelAll": {reflect.TypeOf(q.CancelAll), constant.MakeInt64(int64(q.CancelAll))}, + "CancelOne": {reflect.TypeOf(q.CancelOne), constant.MakeInt64(int64(q.CancelOne))}, + "Close": {reflect.TypeOf(q.Close), constant.MakeInt64(int64(q.Close))}, + "CloseLong": {reflect.TypeOf(q.CloseLong), constant.MakeInt64(int64(q.CloseLong))}, + "CloseShort": {reflect.TypeOf(q.CloseShort), constant.MakeInt64(int64(q.CloseShort))}, + "DirectLong": {reflect.TypeOf(q.DirectLong), constant.MakeInt64(int64(q.DirectLong))}, + "DirectShort": {reflect.TypeOf(q.DirectShort), constant.MakeInt64(int64(q.DirectShort))}, + "Limit": {reflect.TypeOf(q.Limit), constant.MakeInt64(int64(q.Limit))}, + "Market": {reflect.TypeOf(q.Market), constant.MakeInt64(int64(q.Market))}, + "Open": {reflect.TypeOf(q.Open), constant.MakeInt64(int64(q.Open))}, + "OpenLong": {reflect.TypeOf(q.OpenLong), constant.MakeInt64(int64(q.OpenLong))}, + "OpenShort": {reflect.TypeOf(q.OpenShort), constant.MakeInt64(int64(q.OpenShort))}, + "Stop": {reflect.TypeOf(q.Stop), constant.MakeInt64(int64(q.Stop))}, + "StopLong": {reflect.TypeOf(q.StopLong), constant.MakeInt64(int64(q.StopLong))}, + "StopShort": {reflect.TypeOf(q.StopShort), constant.MakeInt64(int64(q.StopShort))}, + }, + UntypedConsts: map[string]igop.UntypedConst{ + "Long": {"untyped int", constant.MakeInt64(int64(q.Long))}, + "Short": {"untyped int", constant.MakeInt64(int64(q.Short))}, + "SymbolTypeFutures": {"untyped string", constant.MakeString(string(q.SymbolTypeFutures))}, + "SymbolTypeIndex": {"untyped string", constant.MakeString(string(q.SymbolTypeIndex))}, + "SymbolTypeSpot": {"untyped string", constant.MakeString(string(q.SymbolTypeSpot))}, + }, + }) +} diff --git a/pkg/process/goscript/include.go b/pkg/process/goscript/include.go new file mode 100644 index 0000000..a83411d --- /dev/null +++ b/pkg/process/goscript/include.go @@ -0,0 +1,6 @@ +package goscript + +import ( + _ "git.qtrade.icu/coin-quant/trade/pkg/process/goscript/igo" + _ "git.qtrade.icu/coin-quant/trade/pkg/process/goscript/plugin" +) diff --git a/pkg/process/goscript/plugin/plugin.go b/pkg/process/goscript/plugin/plugin.go new file mode 100644 index 0000000..e1f0607 --- /dev/null +++ b/pkg/process/goscript/plugin/plugin.go @@ -0,0 +1,88 @@ +package plugin + +import ( + "fmt" + "path/filepath" + "plugin" + "reflect" + + "git.qtrade.icu/coin-quant/base/common" + bengine "git.qtrade.icu/coin-quant/base/engine" + "git.qtrade.icu/coin-quant/trade/pkg/process/goscript/engine" +) + +func init() { + engine.Register(".so", NewPlugin) + engine.Register(".dll", NewPlugin) + engine.Register(".dylib", NewPlugin) +} + +type newFn func() interface{} + +type StrategyPlugin struct { + name string + Runner +} + +func NewPlugin(file string) (r engine.Runner, err error) { + pl, err := plugin.Open(file) + if err != nil { + return + } + v, err := pl.Lookup("NewStrategy") + if err != nil { + return + } + + rValue := reflect.ValueOf(v).Elem() + ret := rValue.Call([]reflect.Value{}) + if len(ret) == 0 { + err = fmt.Errorf("%s constructor error %#v", file, v) + return + } + value := ret[0].Interface() + temp, ok := value.(Runner) + if !ok { + err = fmt.Errorf("%s not impl func() Runner %#v", file, value) + return + } + sp := new(StrategyPlugin) + sp.name = filepath.Base(file) + sp.Runner = temp + r = sp + return +} +func (sp *StrategyPlugin) GetName() string { + return sp.name +} + +func (sp *StrategyPlugin) Param() (paramInfo []common.Param, err error) { + paramInfo = sp.Runner.Param() + return +} +func (sp *StrategyPlugin) Init(engine bengine.Engine, params common.ParamData) (err error) { + return sp.Runner.Init(engine, params) +} +func (sp *StrategyPlugin) OnCandle(candle *Candle) (err error) { + sp.Runner.OnCandle(candle) + return +} +func (sp *StrategyPlugin) OnPosition(pos, price float64) (err error) { + sp.Runner.OnPosition(pos, price) + return +} +func (sp *StrategyPlugin) OnTrade(trade *Trade) (err error) { + sp.Runner.OnTrade(trade) + return +} +func (sp *StrategyPlugin) OnTradeMarket(trade *Trade) (err error) { + sp.Runner.OnTradeMarket(trade) + return +} +func (sp *StrategyPlugin) OnDepth(depth *Depth) (err error) { + sp.Runner.OnDepth(depth) + return +} +func (sp *StrategyPlugin) OnEvent(e *Event) (err error) { + return +} diff --git a/pkg/process/goscript/plugin/runner.go b/pkg/process/goscript/plugin/runner.go new file mode 100644 index 0000000..cea5340 --- /dev/null +++ b/pkg/process/goscript/plugin/runner.go @@ -0,0 +1,18 @@ +package plugin + +import ( + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/base/engine" + . "git.qtrade.icu/coin-quant/trademodel" +) + +type Runner interface { + Param() (paramInfo []common.Param) + Init(engine engine.Engine, params common.ParamData) error + OnCandle(candle *Candle) + OnPosition(pos, price float64) + OnTrade(trade *Trade) + OnTradeMarket(trade *Trade) + OnDepth(depth *Depth) + // OnEvent(e Event) +} diff --git a/pkg/process/notify/notify.go b/pkg/process/notify/notify.go new file mode 100644 index 0000000..289fce6 --- /dev/null +++ b/pkg/process/notify/notify.go @@ -0,0 +1,147 @@ +package notify + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "text/template" + + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +var ( + defaultNotifyConfig NotifyConfig +) + +type NotifyConfig struct { + Headers map[string]string + Url string + Method string + Body string + Notify struct { + Trade bool + Order bool + Blance bool + } +} + +type Notify struct { + BaseProcesser + clt http.Client + cfg *NotifyConfig + bodyTmpl *template.Template +} + +func NewNotify(cfg *viper.Viper) (n *Notify, err error) { + var nCfg = defaultNotifyConfig + err = cfg.UnmarshalKey("notify", &nCfg) + if err != nil { + return + } + tpl, err := template.New("notify").Parse(nCfg.Body) + if err != nil { + return + } + fmt.Println(nCfg) + n = new(Notify) + n.cfg = &nCfg + n.bodyTmpl = tpl + return +} + +func (n *Notify) Init(bus *Bus) (err error) { + n.BaseProcesser.Init(bus) + n.Subscribe(EventTrade, n.OnEventTrade) + n.Subscribe(EventOrder, n.OnEventOrder) + n.Subscribe(EventBalance, n.OnEventBalance) + n.Subscribe(EventNotify, n.OnEventNotify) + return +} + +func (n *Notify) Start() (err error) { + return +} + +func (n *Notify) Stop() (err error) { + return +} + +func (n *Notify) OnEventTrade(e *Event) (err error) { + if !n.cfg.Notify.Trade { + return nil + } + t := e.GetData().(*Trade) + if t == nil { + err = fmt.Errorf("notify OnEventTrade type error:%#v", e.GetData()) + log.Error(err.Error()) + return + } + msg := fmt.Sprintf("%s price: %f, amount: %f", t.Action.String(), t.Price, t.Amount) + return n.SendNotify(&NotifyEvent{Title: "Trade", Content: msg}) +} + +func (n *Notify) OnEventOrder(e *Event) (err error) { + if !n.cfg.Notify.Order { + return nil + } + act := e.GetData().(*TradeAction) + if act == nil { + log.Errorf("Notify decode tradeaction error: %##v", e.GetData()) + return + } + msg := fmt.Sprintf("%s %s price: %f, amount: %f", act.Symbol, act.Action.String(), act.Price, act.Amount) + return n.SendNotify(&NotifyEvent{Title: "Order", Content: msg}) +} + +func (n *Notify) OnEventBalance(e *Event) (err error) { + if !n.cfg.Notify.Blance { + return nil + } + balance, ok := e.GetData().(*Balance) + if !ok { + log.Errorf("Notify OnEventBalance type error: %##v", e.GetData()) + return + } + msg := fmt.Sprintf("%s: %f", balance.Currency, balance.Balance) + return n.SendNotify(&NotifyEvent{Title: "Blance", Content: msg}) +} + +func (n *Notify) OnEventNotify(e *Event) (err error) { + nEvent, ok := e.GetData().(*NotifyEvent) + if !ok { + log.Errorf("Notify OnEventNotify type error: %##v", e.GetData()) + return + } + return n.SendNotify(nEvent) +} + +func (n *Notify) SendNotify(evt *NotifyEvent) (err error) { + var b bytes.Buffer + err = n.bodyTmpl.Execute(&b, evt) + if err != nil { + return + } + req, err := http.NewRequest(n.cfg.Method, n.cfg.Url, &b) + if err != nil { + return + } + for k, v := range n.cfg.Headers { + req.Header.Set(k, v) + } + resp, err := n.clt.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return + } + if resp.StatusCode != 200 { + err = errors.New(string(body)) + } + return +} diff --git a/pkg/process/notify/notify_test.go b/pkg/process/notify/notify_test.go new file mode 100644 index 0000000..566def3 --- /dev/null +++ b/pkg/process/notify/notify_test.go @@ -0,0 +1,31 @@ +package notify + +import ( + "os" + "path" + "testing" + + "github.com/spf13/viper" +) + +func TestSendNotify(t *testing.T) { + home, _ := os.UserHomeDir() + viper.AddConfigPath(path.Join(home, ".config")) + viper.SetConfigName("trade") + viper.SetConfigType("yaml") + err := viper.ReadInConfig() + if err != nil { + t.Fatal(err) + } + n, err := NewNotify(viper.GetViper()) + if err != nil { + t.Fatal(err.Error()) + } + err = n.SendNotify(&NotifyEvent{ + Title: "hello", + Content: "just a test", + }) + if err != nil { + t.Fatal(err.Error()) + } +} diff --git a/pkg/process/rpt/rpt.go b/pkg/process/rpt/rpt.go new file mode 100644 index 0000000..439a23a --- /dev/null +++ b/pkg/process/rpt/rpt.go @@ -0,0 +1,79 @@ +package rpt + +import ( + "fmt" + + log "github.com/sirupsen/logrus" +) + +// Reporter report generater +type Reporter interface { + OnTrade(Trade) + OnBalanceInit(balance, fee float64) (err error) + SetLever(float64) +} + +type Rpt struct { + BaseProcesser + rpt Reporter +} + +func NewRpt(rpt Reporter) *Rpt { + r := new(Rpt) + r.rpt = rpt + return r +} + +func (rpt *Rpt) Init(bus *Bus) (err error) { + rpt.BaseProcesser.Init(bus) + rpt.Subscribe(EventTrade, rpt.OnEventTrade) + rpt.Subscribe(EventBalanceInit, rpt.OnEventBalanceInit) + rpt.Subscribe(EventRiskLimit, rpt.OnEventRiskLimit) + return +} + +func (rpt *Rpt) Start() (err error) { + return +} + +func (rpt *Rpt) Stop() (err error) { + return +} + +func (rpt *Rpt) OnEventTrade(e *Event) (err error) { + t := e.GetData().(*Trade) + if t == nil { + err = fmt.Errorf("rpt OnEventTrade type error:%#v", e.GetData()) + log.Error(err.Error()) + return + } + if rpt.rpt != nil { + rpt.rpt.OnTrade(*t) + } + return +} + +func (rpt *Rpt) OnEventBalanceInit(e *Event) (err error) { + balance := e.GetData().(*BalanceInfo) + if balance == nil { + err = fmt.Errorf("Rpt onEventBalanceInit error %w", err) + log.Error(err.Error()) + return + } + if rpt.rpt != nil { + rpt.rpt.OnBalanceInit(balance.Balance, balance.Fee) + } + return +} +func (rpt *Rpt) OnEventRiskLimit(e *Event) (err error) { + info := e.GetData().(*RiskLimit) + if info == nil { + err = fmt.Errorf("Rpt OnEventRiskLimit error %w", err) + log.Error(err.Error()) + return + } + if rpt.rpt != nil { + rpt.rpt.SetLever(info.Lever) + } + return +} diff --git a/pkg/process/vex/vexchange.go b/pkg/process/vex/vexchange.go new file mode 100644 index 0000000..44e2ff8 --- /dev/null +++ b/pkg/process/vex/vexchange.go @@ -0,0 +1,291 @@ +package vex + +import ( + "container/list" + "fmt" + "math" + "reflect" + "sync" + "time" + + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/trademodel" + + log "github.com/sirupsen/logrus" +) + +// VExchange Virtual exchange impl FuturesBaseExchanger +type VExchange struct { + BaseProcesser + candle *Candle + trades []Trade + orders *list.List + position float64 + symbol string + balance *common.LeverBalance + // order index in same candle + orderIndex int + orderMutex sync.Mutex +} + +func NewVExchange(symbol string) *VExchange { + ex := new(VExchange) + ex.Name = "VExchange" + ex.orders = list.New() + ex.symbol = symbol + ex.balance = common.NewLeverBalance() + return ex +} + +func (b *VExchange) Init(bus *Bus) (err error) { + b.BaseProcesser.Init(bus) + b.Subscribe(EventCandle, b.onEventCandle) + b.Subscribe(EventOrder, b.onEventOrder) + b.Subscribe(EventBalanceInit, b.onEventBalanceInit) + b.Subscribe(EventRiskLimit, b.onEventRiskLimit) + return +} + +func (ex *VExchange) Start() (err error) { + ex.Send(ex.symbol, EventBalance, &Balance{Balance: ex.balance.Get()}) + return +} +func (ex *VExchange) processCandle(candle Candle) (err error) { + // TODO: check if liq + if ex.orders.Len() == 0 { + return + } + ex.orderMutex.Lock() + defer ex.orderMutex.Unlock() + var posChange bool + var deleteElems []*list.Element + virtualTime := candle.Time() + var trades []*Event + var pos Position + var orderFilled bool + var side string + var price float64 + for elem := ex.orders.Front(); elem != nil; elem = elem.Next() { + orderFilled = false + v, ok := elem.Value.(TradeAction) + if !ok { + log.Errorf("order items type error:%##v", elem.Value) + continue + } + if !v.Action.IsOpen() { + // stop order not works if position is zero + if ex.position == 0 { + continue + } else if ex.position > 0 && v.Action.IsLong() { + continue + } else if ex.position < 0 && !v.Action.IsLong() { + continue + } + } + // order can only be filled after next candle + price = v.Price + + switch v.Action { + case StopShort: + if v.Price <= candle.High { + side = "buy" + orderFilled = true + if v.Price < candle.Low { + price = candle.Low + } + } + case StopLong: + if v.Price >= candle.Low { + side = "sell" + orderFilled = true + if v.Price > candle.High { + price = candle.High + } + } + case OpenLong, CloseShort: + if v.Price >= candle.Low { + side = "buy" + orderFilled = true + if v.Price > candle.High { + price = candle.High + } + } + case OpenShort, CloseLong: + if v.Price <= candle.High { + side = "sell" + orderFilled = true + if v.Price < candle.Low { + price = candle.Low + } + } + default: + log.Warnf("unsupport ActionType: %s", v.Action.String()) + continue + } + + // fmt.Println("action:", v.Action.String(), v.Price, candle.High, candle.Low, candle.Time(), orderFilled) + if !orderFilled { + continue + } + + virtualTime = virtualTime.Add(time.Second) + tr := Trade{ID: fmt.Sprintf("%d", len(ex.trades)), + Action: v.Action, + Time: virtualTime, + Price: price, + Amount: v.Amount, + Side: side, + Remark: ""} + if v.ID != "" { + tr.ID = v.ID + } + // fix size + _, _, err = ex.balance.AddTrade(tr) + if err != nil { + // log.Errorf("vexchange balance AddTrade error:%s %f %f", err.Error(), v.Price, v.Amount) + return + } + ex.trades = append(ex.trades, tr) + tradeEvent := ex.CreateEvent("trade", EventTrade, &tr) + trades = append(trades, tradeEvent) + + posChange = true + ex.position = ex.balance.Pos() + pos.Price = tr.Price + deleteElems = append(deleteElems, elem) + } + for _, v := range deleteElems { + ex.orders.Remove(v) + } + // keep trade time order + if len(trades) != 0 { + for i := len(trades) - 1; i >= 0; i-- { + ex.Bus.Send(trades[i]) + } + } + if posChange { + pos.Symbol = ex.symbol + pos.Hold = ex.position + // ex.Send(ex.symbol, EventCurPosition, pos) + ex.Send(ex.symbol, EventPosition, &pos) + if pos.Hold == 0 { + ex.Send(ex.symbol, EventBalance, &Balance{Currency: ex.symbol, Balance: ex.balance.Get()}) + } + } + return nil +} + +func (ex *VExchange) onEventCandle(e *Event) (err error) { + candle, ok := e.GetData().(*Candle) + if !ok { + err = fmt.Errorf("VExchange candle type error:%s", reflect.TypeOf(e.GetData())) + return + } + // fmt.Println("candle:", e.Name, e.GetType(), e.GetData()) + binSize := e.GetExtra().(string) + if binSize != "1m" { + return + } + + ex.candle = candle + ex.orderIndex = 0 + err = ex.processCandle(*candle) + return +} + +func (ex *VExchange) onEventOrder(e *Event) (err error) { + ex.orderMutex.Lock() + defer ex.orderMutex.Unlock() + act := e.GetData().(*TradeAction) + if act == nil { + log.Errorf("decode tradeaction error: %##v", e.GetData()) + return + } + if act.Action == trademodel.CancelAll { + ex.orders = list.New() + return + } else if act.Action == trademodel.CancelOne { + for item := ex.orders.Front(); item != nil; item.Next() { + od := item.Value.(TradeAction) + if od.ID == act.ID { + ex.orders.Remove(item) + return + } + } + return + } + if ex.candle != nil { + act.Time = ex.candle.Time().Add(time.Second * time.Duration(ex.orderIndex)) + if act.Action == StopLong && act.Price >= ex.candle.Close { + log.Warnf("invalid stop long order,action: %#v, candle: %s", *act, *ex.candle) + return + } else if act.Action == StopShort && act.Price <= ex.candle.Close { + log.Warnf("invalid stop short order,action: %#v, candle: %s", *act, *ex.candle) + return + } + } + ex.orderIndex++ + ex.orders.PushBack(*act) + return +} + +func (ex *VExchange) onEventBalanceInit(e *Event) (err error) { + balance := e.GetData().(*BalanceInfo) + ex.balance.Set(balance.Balance) + ex.balance.SetFee(balance.Fee) + ex.Send(ex.symbol, EventBalance, &Balance{Currency: ex.symbol, Balance: ex.balance.Get()}) + return +} + +func (ex *VExchange) onEventRiskLimit(e *Event) (err error) { + info := e.GetData().(*RiskLimit) + if info == nil { + err = fmt.Errorf("VExchange OnEventRiskLimit error %w", err) + log.Error(err.Error()) + return + } + ex.balance.SetLever(info.Lever) + return +} + +func (ex *VExchange) CloseAll() (err error) { + if ex.position == 0 { + return + } + virtualTime := ex.candle.Time().Add(time.Second) + var tr Trade + if ex.position > 0 { + tr = Trade{ID: fmt.Sprintf("%d", len(ex.trades)), + Action: CloseLong, + Time: virtualTime, + Price: ex.candle.Close, + Amount: math.Abs(ex.position), + Side: "sell", + Remark: ""} + } else { + tr = Trade{ID: fmt.Sprintf("%d", len(ex.trades)), + Action: CloseShort, + Time: virtualTime, + Price: ex.candle.Close, + Amount: math.Abs(ex.position), + Side: "buy", + Remark: ""} + } + tradeEvent := ex.CreateEvent("trade", EventTrade, &tr) + ex.Bus.Send(tradeEvent) + _, _, err = ex.balance.AddTrade(tr) + if err != nil { + log.Errorf("vexchange CloseALll balance AddTrade error:%s %f %f", err.Error(), tr.Price, tr.Amount) + return + } + ex.position = ex.balance.Pos() + var pos Position + pos.Symbol = ex.symbol + pos.Hold = ex.position + // ex.Send(ex.symbol, EventCurPosition, pos) + ex.Send(ex.symbol, EventPosition, &pos) + if pos.Hold == 0 { + ex.Send(ex.symbol, EventBalance, &Balance{Currency: ex.symbol, Balance: ex.balance.Get()}) + } + return +} diff --git a/pkg/report/report.go b/pkg/report/report.go new file mode 100644 index 0000000..2b770a1 --- /dev/null +++ b/pkg/report/report.go @@ -0,0 +1,385 @@ +package report + +import ( + "bytes" + _ "embed" + "fmt" + "html/template" + "io" + "math" + "os" + "sort" + + "git.qtrade.icu/coin-quant/base/common" + jsoniter "github.com/json-iterator/go" + "github.com/montanaflynn/stats" + "github.com/shopspring/decimal" + log "github.com/sirupsen/logrus" + "xorm.io/xorm" +) + +//go:embed report.tmpl +var reportTmpl string + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +type Report struct { + actions []TradeAction + trades []Trade + balanceInit float64 + balanceEnd float64 + profit float64 + maxLose float64 + winRate float64 + profitHistory []float64 + tmplDatas []*RptAct + totalAction int + maxDrawdown float64 + maxDrawdownValue float64 + fee float64 + profitLoseRatio float64 + + profitVariance float64 + loseVariance float64 + + lever float64 +} + +type RptAct struct { + Trade `xorm:"extends"` + Total float64 + TotalProfit float64 // total profit,sum of all history profits,if action is open, total profit is zero + Profit float64 // profit, if action is open, profit is zero + Fee float64 + IsFinish bool +} + +func NewReportSimple() *Report { + rep := new(Report) + return rep +} + +func NewReport(trades []Trade, balanceInit float64) *Report { + rep := new(Report) + rep.trades = trades + rep.balanceInit = balanceInit + return rep +} + +func (r *Report) SetFee(fee float64) { + r.fee = fee +} + +func (r *Report) SetLever(lever float64) { + r.lever = lever +} + +func (r *Report) Analyzer() (err error) { + nLen := len(r.trades) + if nLen == 0 { + return + } + i := nLen + for ; i > 0; i-- { + if !r.trades[i-1].Action.IsOpen() { + break + } + } + r.trades = r.trades[0:i] + profitTotal := decimal.New(0, 0) + loseTotal := decimal.New(0, 0) + var longAmount, costOnce float64 + var shortAmount float64 + var actTotal, lose float64 + var success, total int + var tmplData, lastTmplData *RptAct + var lastMaxTotal, lastMinTotal, drawdown, drawdownValue float64 + var profit, fee float64 + var profitArray, loseArray []float64 + bal := common.NewLeverBalance() + bal.Set(r.balanceInit) + bal.SetFee(r.fee) + bal.SetLever(r.lever) + // startBalance := bal.Get() + + for _, v := range r.trades { + profit, fee, err = bal.AddTrade(v) + if err != nil { + log.Error("Report add trade error:", err.Error()) + return + } + actTotal = common.FloatMul(v.Price, v.Amount) + if v.Action.IsLong() { + longAmount = common.FloatAdd(longAmount, v.Amount) + // log.Println("buy action", v.Time, v.Action, v.Price, v.Amount) + } else { + // log.Println("sell action", v.Time, v.Action, v.Price, v.Amount) + shortAmount = common.FloatAdd(shortAmount, v.Amount) + } + if v.Action.IsOpen() { + costOnce = common.FloatAdd(costOnce, actTotal) + } + + r.totalAction++ + tmplData = &RptAct{Trade: v, + Total: bal.Get(), + Profit: profit, + Fee: fee, + IsFinish: false, + } + r.tmplDatas = append(r.tmplDatas, tmplData) + // one round finish + if longAmount == shortAmount { + tmplData.IsFinish = true + if lastTmplData != nil { + tmplData.TotalProfit = lastTmplData.TotalProfit + } + tmplData.Profit = profit + if profit > 0 { + profitArray = append(profitArray, profit) + profitTotal = profitTotal.Add(decimal.NewFromFloat(profit)) + } else { + loseArray = append(loseArray, profit) + loseTotal = loseTotal.Add(decimal.NewFromFloat(profit)) + } + tmplData.TotalProfit = common.FloatAdd(tmplData.TotalProfit, tmplData.Profit) + r.profitHistory = append(r.profitHistory, profit) + total++ + if profit > 0 { + success++ + } else { + if costOnce != 0 { + // profit / cost + lose = common.FloatDiv(common.FloatMul(profit, 100), costOnce) + } + if math.Abs(lose) > math.Abs(r.maxLose) { + r.maxLose = lose + } + } + costOnce = 0 + r.balanceEnd = bal.Get() + } + if tmplData.TotalProfit != 0 { + lastTmplData = tmplData + if tmplData.TotalProfit > lastMaxTotal { + lastMaxTotal = tmplData.TotalProfit + // update lastMinTotal + lastMinTotal = lastMaxTotal + } + if tmplData.TotalProfit < lastMinTotal { + lastMinTotal = tmplData.TotalProfit + } + drawdownValue = common.FloatSub(lastMaxTotal, lastMinTotal) + if drawdownValue > r.maxDrawdownValue { + r.maxDrawdownValue = drawdownValue + } + drawdown = common.FloatDiv(common.FloatMul(drawdownValue, 100), lastMaxTotal+r.balanceInit) + if drawdown > r.maxDrawdown { + r.maxDrawdown = drawdown + } + } + } + // endBalance := bal.Get() + if lastTmplData != nil { + r.profit = lastTmplData.TotalProfit + } + // endBalance - startBalance + if total > 0 { + r.winRate = common.FloatDiv(float64(success), float64(total)) + } + if !loseTotal.IsZero() { + r.profitLoseRatio, _ = profitTotal.Div(loseTotal.Abs()).Float64() + } else { + r.profitLoseRatio, _ = profitTotal.Float64() + } + r.profitVariance, err = stats.Variance(profitArray) + if err != nil { + return err + } + r.loseVariance, err = stats.Variance(loseArray) + return +} + +func (r *Report) WinRate() (rate float64) { + rate = common.FormatFloat(r.winRate, 2) + return +} + +func (r *Report) Profit() (profit float64) { + profit = common.FormatFloat(r.profit, 4) + return +} + +func (r *Report) ProfitPercent() float64 { + return common.FormatFloat((r.EndBalance()*100)/r.balanceInit, 4) +} + +func (r *Report) ProfitVariance() float64 { + return common.FormatFloat(r.profitVariance, 4) +} + +func (r *Report) LoseVariance() float64 { + return common.FormatFloat(r.loseVariance, 4) +} + +func (r *Report) EndBalance() float64 { + return common.FormatFloat(r.balanceEnd, 4) +} + +func (r *Report) ProfitLoseRatio() float64 { + return common.FormatFloat(r.profitLoseRatio, 4) +} + +// MaxLose max total lose +func (r *Report) MaxLose() (lose float64) { + lose = common.FormatFloat(r.maxLose, 2) + return +} + +// MaxDrawdown get max drawdown percent +func (r *Report) MaxDrawdown() float64 { + return common.FormatFloat(r.maxDrawdown, 2) +} + +// MaxDrawdown get max drawdown value +func (r *Report) MaxDrawdownValue() float64 { + return common.FormatFloat(r.maxDrawdownValue, 4) +} + +func (r *Report) GetReport() (report string) { + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("Total action:%d\n", len(r.actions))) + buf.WriteString(fmt.Sprintf("Win rate:%f\n", r.WinRate())) + buf.WriteString(fmt.Sprintf("Profit:%f\n", r.Profit())) + buf.WriteString(fmt.Sprintf("Max lose percent:%f\n", r.MaxLose())) + buf.WriteString(fmt.Sprintf("Max drawdown percent:%f%%\n", r.MaxDrawdown())) + buf.WriteString(fmt.Sprintf("Max drawdown value :%f\n", r.MaxDrawdown())) + buf.WriteString(fmt.Sprintf("Profit lose ratio: %f\n", r.ProfitLoseRatio())) + buf.WriteString(fmt.Sprintf("StartBalance: %f\n", r.balanceInit)) + buf.WriteString(fmt.Sprintf("EndBalance: %f\n", r.EndBalance())) + buf.WriteString(fmt.Sprintf("ProfitPercent:%f\n", r.ProfitPercent())) + buf.WriteString(fmt.Sprintf("ProfitVariance:%f\n", r.ProfitVariance())) + buf.WriteString(fmt.Sprintf("LoseVariance:%f\n", r.LoseVariance())) + data, _ := json.Marshal(r.profitHistory) + buf.WriteString(string(data)) + report = buf.String() + return +} + +func (r *Report) GenHTMLReport(fPath string) (err error) { + f, err := os.OpenFile(fPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm) + if err != nil { + return + } + defer f.Close() + err = r.GenHTML(f) + return +} + +func (r *Report) GenHTML(w io.Writer) (err error) { + tmpl, err := template.New("report").Parse(reportTmpl) + if err != nil { + log.Println("tmpl parse failed:", err.Error()) + return + } + data := make(map[string]interface{}) + data["totalAction"] = r.totalAction + data["winRate"] = r.WinRate() + data["profit"] = r.Profit() + + data["maxLose"] = r.MaxLose() + data["actions"] = r.tmplDatas + data["maxDrawdown"] = r.MaxDrawdown() + data["maxDrawdownValue"] = r.MaxDrawdownValue() + data["profitLoseRatio"] = r.ProfitLoseRatio() + data["startBalance"] = r.balanceInit + data["endBalance"] = r.EndBalance() + data["profitPercent"] = r.ProfitPercent() + data["profitVariance"] = r.ProfitVariance() + data["loseVariance"] = r.LoseVariance() + err = tmpl.Execute(w, data) + return +} + +func (r *Report) OnBalanceInit(balance, fee float64) (err error) { + r.balanceInit = balance + r.fee = fee + return +} + +func (r *Report) OnTrade(t Trade) { + r.trades = append(r.trades, t) +} + +func (r *Report) GenRPT(fPath string) (err error) { + sort.Slice(r.trades, func(i int, j int) bool { + return r.trades[i].Time.Unix() < r.trades[j].Time.Unix() + }) + err = r.Analyzer() + if err != nil { + return + } + err = r.GenHTMLReport(fPath) + if err != nil { + return + } + return +} + +func (r *Report) GetResult() (ret ReportResult, err error) { + sort.Slice(r.trades, func(i int, j int) bool { + return r.trades[i].Time.Unix() < r.trades[j].Time.Unix() + }) + err = r.Analyzer() + if err != nil { + return + } + ret.TotalAction = r.totalAction + ret.Actions = r.tmplDatas + ret.WinRate = r.WinRate() + ret.Profit = r.Profit() + ret.MaxLose = r.MaxLose() + ret.MaxDrawdown = r.MaxDrawdown() + ret.MaxDrawDownValue = r.MaxDrawdownValue() + ret.ProfitPercent = r.ProfitPercent() + ret.ProfitVariance = r.ProfitVariance() + ret.LoseVariance = r.LoseVariance() + return +} + +func (r *Report) ExportToDB(dbPath string) (err error) { + eng, err := xorm.NewEngine("sqlite", dbPath) + if err != nil { + return + } + var data RptAct + err = eng.Sync2(&data) + if err != nil { + return + } + defer eng.Close() + fmt.Println("tmpl len:", len(r.tmplDatas)) + for _, v := range r.tmplDatas { + _, err = eng.Insert(v) + if err != nil { + return + } + } + return +} + +type ReportResult struct { + TotalAction int + WinRate float64 + Profit float64 + MaxLose float64 + MaxDrawdown float64 + MaxDrawDownValue float64 + Actions []*RptAct `json:"-"` + TotalFee float64 + StartBalance float64 + EndBalance float64 + ProfitPercent float64 + ProfitVariance float64 + LoseVariance float64 +} diff --git a/pkg/report/report.tmpl b/pkg/report/report.tmpl new file mode 100644 index 0000000..95f1bb6 --- /dev/null +++ b/pkg/report/report.tmpl @@ -0,0 +1,203 @@ + + + + + Backtest Report + + + + + + + + +
+

Trade backtest report

+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + + + +

Trade detail

+ + + + + + + + + + + + + + {{range .actions}} + + + + + + + + + + {{end}} +
TimeActionPriceAmountTotalProfitFee
{{.Time}}{{.Action}}{{.Price}}{{.Amount}}{{.Total}}{{.Profit}}{{.Fee}}
+
+ + +