first commit
This commit is contained in:
commit
33d670ab30
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
*.yaml
|
62
README.md
Normal file
62
README.md
Normal file
|
@ -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)
|
61
README_cn.md
Normal file
61
README_cn.md
Normal file
|
@ -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)
|
119
cmd/backtest.go
Normal file
119
cmd/backtest.go
Normal file
|
@ -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
|
||||||
|
}
|
39
cmd/build.go
Normal file
39
cmd/build.go
Normal file
|
@ -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)
|
||||||
|
}
|
54
cmd/download.go
Normal file
54
cmd/download.go
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
50
cmd/list.go
Normal file
50
cmd/list.go
Normal file
|
@ -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()
|
||||||
|
}
|
169
cmd/root.go
Normal file
169
cmd/root.go
Normal file
|
@ -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
|
||||||
|
}
|
88
cmd/trade.go
Normal file
88
cmd/trade.go
Normal file
|
@ -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
|
||||||
|
}
|
12
configs/trade.yaml.bak
Normal file
12
configs/trade.yaml.bak
Normal file
|
@ -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
|
217
doc/strategy.md
Normal file
217
doc/strategy.md
Normal file
|
@ -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方法
|
89
go.mod
Normal file
89
go.mod
Normal file
|
@ -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
|
||||||
|
)
|
416
go.sum
Normal file
416
go.sum
Normal file
|
@ -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=
|
7
main.go
Normal file
7
main.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "git.qtrade.icu/coin-quant/trade/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
121
pkg/core/events.go
Normal file
121
pkg/core/events.go
Normal file
|
@ -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
|
||||||
|
}
|
34
pkg/core/risk.go
Normal file
34
pkg/core/risk.go
Normal file
|
@ -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
|
||||||
|
}
|
16
pkg/core/symbol.go
Normal file
16
pkg/core/symbol.go
Normal file
|
@ -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, ",")
|
||||||
|
}
|
9
pkg/core/watch.go
Normal file
9
pkg/core/watch.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
// CandleInfo candle data with symbol info
|
||||||
|
type CandleInfo struct {
|
||||||
|
Exchange string
|
||||||
|
Symbol string
|
||||||
|
BinSize string
|
||||||
|
Data interface{}
|
||||||
|
}
|
172
pkg/ctl/back.go
Normal file
172
pkg/ctl/back.go
Normal file
|
@ -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
|
||||||
|
}
|
207
pkg/ctl/build.go
Normal file
207
pkg/ctl/build.go
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
163
pkg/ctl/download.go
Normal file
163
pkg/ctl/download.go
Normal file
|
@ -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
|
||||||
|
}
|
113
pkg/ctl/list.go
Normal file
113
pkg/ctl/list.go
Normal file
|
@ -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
|
||||||
|
}
|
25
pkg/ctl/script.go
Normal file
25
pkg/ctl/script.go
Normal file
|
@ -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
|
||||||
|
}
|
57
pkg/ctl/tmpl/define.go
Normal file
57
pkg/ctl/tmpl/define.go
Normal file
|
@ -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
|
4
pkg/ctl/tmpl/export.go
Normal file
4
pkg/ctl/tmpl/export.go
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
var NewStrategy = New{{.Name}}
|
||||||
|
var _ Runner = NewStrategy()
|
167
pkg/ctl/trade.go
Normal file
167
pkg/ctl/trade.go
Normal file
|
@ -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
|
||||||
|
}
|
183
pkg/event/bus.go
Normal file
183
pkg/event/bus.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
72
pkg/event/event.go
Normal file
72
pkg/event/event.go
Normal file
|
@ -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
|
||||||
|
}
|
21
pkg/event/event_test.go
Normal file
21
pkg/event/event_test.go
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
65
pkg/event/processer.go
Normal file
65
pkg/event/processer.go
Normal file
|
@ -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)
|
||||||
|
}
|
97
pkg/event/processers.go
Normal file
97
pkg/event/processers.go
Normal file
|
@ -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()
|
||||||
|
}
|
48
pkg/helper/helper.go
Normal file
48
pkg/helper/helper.go
Normal file
|
@ -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
|
||||||
|
}
|
57
pkg/helper/strategy.go
Normal file
57
pkg/helper/strategy.go
Normal file
|
@ -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
|
||||||
|
}
|
155
pkg/process/dbstore/db.go
Normal file
155
pkg/process/dbstore/db.go
Normal file
|
@ -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
|
||||||
|
}
|
99
pkg/process/dbstore/kline.go
Normal file
99
pkg/process/dbstore/kline.go
Normal file
|
@ -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
|
||||||
|
}
|
11
pkg/process/dbstore/load.go
Normal file
11
pkg/process/dbstore/load.go
Normal file
|
@ -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
|
||||||
|
}
|
375
pkg/process/dbstore/tbl.go
Normal file
375
pkg/process/dbstore/tbl.go
Normal file
|
@ -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<?", since.Unix(), end.Unix()).Limit(limit, offset).Find(ret)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
datas = t.creator.GetSlice(ret)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TimeTbl) DataRecent(recent int32, bSize string) (klines []interface{}, err error) {
|
||||||
|
if bSize != t.binSize {
|
||||||
|
err = fmt.Errorf("kline tbl %s binsize error: %s", t.table, bSize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ret := t.creator.Slice()
|
||||||
|
sess := t.getTable()
|
||||||
|
defer sess.Close()
|
||||||
|
err = sess.Desc("start").Limit(int(recent), 0).Find(ret)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
datas := t.creator.GetSlice(ret)
|
||||||
|
klines = make([]interface{}, len(datas))
|
||||||
|
for k, v := range datas {
|
||||||
|
klines[len(klines)-k-1] = v
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheData load datas and store to cache
|
||||||
|
func (t *TimeTbl) CacheData(start, end time.Time, bSize string) (err error) {
|
||||||
|
if !t.db.useCache {
|
||||||
|
err = errors.New("db not enable cache")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if bSize != t.binSize {
|
||||||
|
err = fmt.Errorf("kline tbl %s binsize error: %s", t.table, bSize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("%s-%s-%s-%d-%d-%s", t.exchange, t.symbol, t.binSize, start.UnixNano(), end.UnixNano(), bSize)
|
||||||
|
_, ok := t.db.dataCache.Load(key)
|
||||||
|
if ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nOffset := 0
|
||||||
|
once := t.loadOnce
|
||||||
|
var data []interface{}
|
||||||
|
var caches [][]interface{}
|
||||||
|
for {
|
||||||
|
data, err = t.getDatasWithParam(start, end, once, nOffset)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if t.db.useCache {
|
||||||
|
caches = append(caches, data)
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nOffset += len(data)
|
||||||
|
if len(data) < once {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("TimeTbl DataChan getDatas failed:", err.Error())
|
||||||
|
} else {
|
||||||
|
t.db.dataCache.Store(key, caches)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TimeTbl) DataChan(start, end time.Time, bSize string) (klines chan []interface{}, err error) {
|
||||||
|
if bSize != t.binSize {
|
||||||
|
err = fmt.Errorf("kline tbl %s binsize error: %s", t.table, bSize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
klines = make(chan []interface{}, 10240)
|
||||||
|
key := fmt.Sprintf("%s-%s-%s-%d-%d-%s", t.exchange, t.symbol, t.binSize, start.UnixNano(), end.UnixNano(), bSize)
|
||||||
|
cacheDatas, ok := t.db.dataCache.Load(key)
|
||||||
|
if ok {
|
||||||
|
go func() {
|
||||||
|
datas := cacheDatas.([][]interface{})
|
||||||
|
for _, v := range datas {
|
||||||
|
klines <- v
|
||||||
|
}
|
||||||
|
close(klines)
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
nOffset := 0
|
||||||
|
once := t.loadOnce
|
||||||
|
var err1 error
|
||||||
|
var data []interface{}
|
||||||
|
var caches [][]interface{}
|
||||||
|
for {
|
||||||
|
data, err1 = t.getDatasWithParam(start, end, once, nOffset)
|
||||||
|
if err1 != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if t.db.useCache {
|
||||||
|
caches = append(caches, data)
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nOffset += len(data)
|
||||||
|
klines <- data
|
||||||
|
if len(data) < once {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.db.useCache {
|
||||||
|
t.db.dataCache.Store(key, caches)
|
||||||
|
}
|
||||||
|
if err1 != nil {
|
||||||
|
log.Error("TimeTbl DataChan getDatas failed:", err1.Error())
|
||||||
|
}
|
||||||
|
close(klines)
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tbl *TimeTbl) IsEmpty() (isEmpty bool) {
|
||||||
|
isEmpty = true
|
||||||
|
sess := tbl.getTable()
|
||||||
|
defer sess.Close()
|
||||||
|
n, err := sess.Count()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("table:%s count failed:%s", tbl.table, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n > 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
|
||||||
|
}
|
18
pkg/process/dbstore/trade.go
Normal file
18
pkg/process/dbstore/trade.go
Normal file
|
@ -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
|
||||||
|
}
|
31
pkg/process/exchange/err.go
Normal file
31
pkg/process/exchange/err.go
Normal file
|
@ -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
|
||||||
|
}
|
346
pkg/process/exchange/exchange.go
Normal file
346
pkg/process/exchange/exchange.go
Normal file
|
@ -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
|
||||||
|
}
|
19
pkg/process/exchange/interface.go
Normal file
19
pkg/process/exchange/interface.go
Normal file
|
@ -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
|
||||||
|
}
|
187
pkg/process/goscript/engine/engine.go
Normal file
187
pkg/process/goscript/engine/engine.go
Normal file
|
@ -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")
|
||||||
|
}
|
42
pkg/process/goscript/engine/kline.go
Normal file
42
pkg/process/goscript/engine/kline.go
Normal file
|
@ -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)
|
||||||
|
}
|
42
pkg/process/goscript/engine/runner.go
Normal file
42
pkg/process/goscript/engine/runner.go
Normal file
|
@ -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
|
||||||
|
}
|
283
pkg/process/goscript/goengine.go
Normal file
283
pkg/process/goscript/goengine.go
Normal file
|
@ -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}
|
||||||
|
}
|
||||||
|
}
|
81
pkg/process/goscript/igo/common.go
Normal file
81
pkg/process/goscript/igo/common.go
Normal file
|
@ -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))},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
37
pkg/process/goscript/igo/engine.go
Normal file
37
pkg/process/goscript/igo/engine.go
Normal file
|
@ -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))},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
35
pkg/process/goscript/igo/fsm.go
Normal file
35
pkg/process/goscript/igo/fsm.go
Normal file
|
@ -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{},
|
||||||
|
})
|
||||||
|
}
|
152
pkg/process/goscript/igo/igo.go
Normal file
152
pkg/process/goscript/igo/igo.go
Normal file
|
@ -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
|
||||||
|
}
|
71
pkg/process/goscript/igo/indicator.go
Normal file
71
pkg/process/goscript/igo/indicator.go
Normal file
|
@ -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{},
|
||||||
|
})
|
||||||
|
}
|
10
pkg/process/goscript/igo/init.go
Normal file
10
pkg/process/goscript/igo/init.go
Normal file
|
@ -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)
|
||||||
|
}
|
67
pkg/process/goscript/igo/runner.go
Normal file
67
pkg/process/goscript/igo/runner.go
Normal file
|
@ -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
|
||||||
|
}
|
73
pkg/process/goscript/igo/trademodel.go
Normal file
73
pkg/process/goscript/igo/trademodel.go
Normal file
|
@ -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))},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
6
pkg/process/goscript/include.go
Normal file
6
pkg/process/goscript/include.go
Normal file
|
@ -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"
|
||||||
|
)
|
88
pkg/process/goscript/plugin/plugin.go
Normal file
88
pkg/process/goscript/plugin/plugin.go
Normal file
|
@ -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
|
||||||
|
}
|
18
pkg/process/goscript/plugin/runner.go
Normal file
18
pkg/process/goscript/plugin/runner.go
Normal file
|
@ -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)
|
||||||
|
}
|
147
pkg/process/notify/notify.go
Normal file
147
pkg/process/notify/notify.go
Normal file
|
@ -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
|
||||||
|
}
|
31
pkg/process/notify/notify_test.go
Normal file
31
pkg/process/notify/notify_test.go
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
79
pkg/process/rpt/rpt.go
Normal file
79
pkg/process/rpt/rpt.go
Normal file
|
@ -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
|
||||||
|
}
|
291
pkg/process/vex/vexchange.go
Normal file
291
pkg/process/vex/vexchange.go
Normal file
|
@ -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
|
||||||
|
}
|
385
pkg/report/report.go
Normal file
385
pkg/report/report.go
Normal file
|
@ -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
|
||||||
|
}
|
203
pkg/report/report.tmpl
Normal file
203
pkg/report/report.tmpl
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Backtest Report</title>
|
||||||
|
<meta name="author" content="https://coolplay.website"/>
|
||||||
|
<meta name="description" content="Trade Backtest Report"/>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
|
<script language="javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js"></script>
|
||||||
|
<script language="javascript">
|
||||||
|
|
||||||
|
function drawTotalProfit(domID, source) {
|
||||||
|
let datas = [];
|
||||||
|
let labels = []
|
||||||
|
let totalProfit = 0;
|
||||||
|
let i = 0;
|
||||||
|
for (d in source){
|
||||||
|
totalProfit = source[d].TotalProfit;
|
||||||
|
if (totalProfit === 0){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
datas.push(totalProfit);
|
||||||
|
labels.push(source[d].Time);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
var ctx = document.getElementById(domID).getContext('2d');
|
||||||
|
var myChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Total Profit',
|
||||||
|
data: datas,
|
||||||
|
backgroundColor: ['rgba(54, 162, 235, 0.2)',
|
||||||
|
'rgba(255, 206, 86, 0.2)',
|
||||||
|
'rgba(75, 192, 192, 0.2)',
|
||||||
|
'rgba(153, 102, 255, 0.2)',
|
||||||
|
'rgba(255, 159, 64, 0.2)']
|
||||||
|
}],
|
||||||
|
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawChart(domID, source, attr, title) {
|
||||||
|
let datas = [];
|
||||||
|
let labels = []
|
||||||
|
let i = 0;
|
||||||
|
for (d in source){
|
||||||
|
let data = source[d];
|
||||||
|
if (!data.IsFinish){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let value = data[attr];
|
||||||
|
datas.push(value);
|
||||||
|
labels.push(source[d].Time);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
var ctx = document.getElementById(domID).getContext('2d');
|
||||||
|
var myChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: attr,
|
||||||
|
data: datas,
|
||||||
|
backgroundColor: ['rgba(255, 99, 132, 0.2)',
|
||||||
|
'rgba(54, 162, 235, 0.2)',
|
||||||
|
'rgba(255, 206, 86, 0.2)',
|
||||||
|
'rgba(75, 192, 192, 0.2)',
|
||||||
|
'rgba(153, 102, 255, 0.2)',
|
||||||
|
'rgba(255, 159, 64, 0.2)']
|
||||||
|
}],
|
||||||
|
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="text-center">Trade backtest report</h1>
|
||||||
|
<div class="container">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="totalAction" class="col-sm-6 col-form-label text-right">Total Actions:</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="totalAction" value="{{.totalAction}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="winRate" class="col-sm-6 col-form-label text-right">Win Rate:</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="winRate" value="{{.winRate}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="profit" class="col-sm-6 col-form-label text-right">Profit:</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="profit" value="{{.profit}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="maxDrawdown" class="col-sm-6 col-form-label text-right">Max drawdown percent: </label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="maxLose" value="{{.maxDrawdown}}%">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="maxDrawdownValue" class="col-sm-6 col-form-label text-right">Max drawdown value: </label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="maxLose" value="{{.maxDrawdownValue}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="maxLose" class="col-sm-6 col-form-label text-right">Max lose percent per round: </label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="maxLose" value="{{.maxLose}}%">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="profitLoseRatio" class="col-sm-6 col-form-label text-right">Profit lose ratio: </label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="maxLose" value="{{.profitLoseRatio}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="startBalance" class="col-sm-6 col-form-label text-right">Start Balance: </label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="startBalance" value="{{.startBalance}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="endBalance" class="col-sm-6 col-form-label text-right">End Balance: </label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="endBalance" value="{{.endBalance}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="profitPercent" class="col-sm-6 col-form-label text-right">Profit Percent: </label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="profitPercent" value="{{.profitPercent}}%">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="profitVariance" class="col-sm-6 col-form-label text-right">Profit Variance: </label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="profitVariance" value="{{.profitVariance}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="loseVariance" class="col-sm-6 col-form-label text-right">Lose Variance: </label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="loseVariance" value="{{.loseVariance}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas id="profitChart" width="400" height="100"></canvas>
|
||||||
|
<canvas id="totalProfitChart" width="400" height="100"></canvas>
|
||||||
|
<canvas id="fundsChart" width="400" height="100"></canvas>
|
||||||
|
|
||||||
|
<h3 class="text-center">Trade detail</h3>
|
||||||
|
<table class="table">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Time</th>
|
||||||
|
<th scope="col">Action</th>
|
||||||
|
<th scope="col">Price</th>
|
||||||
|
<th scope="col">Amount</th>
|
||||||
|
<th scope="col">Total</th>
|
||||||
|
<th scope="col">Profit</th>
|
||||||
|
<th scope="col">Fee</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .actions}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Time}}</td>
|
||||||
|
<td>{{.Action}}</td>
|
||||||
|
<td>{{.Price}}</td>
|
||||||
|
<td>{{.Amount}}</td>
|
||||||
|
<td>{{.Total}}</td>
|
||||||
|
<td>{{.Profit}}</td>
|
||||||
|
<td>{{.Fee}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var actions = {{.actions}};
|
||||||
|
// drawProfit("profitChat", actions);
|
||||||
|
drawChart("profitChart", actions, "Profit", "Profit");
|
||||||
|
drawChart("totalProfitChart", actions, "TotalProfit", "TotalProfit");
|
||||||
|
drawChart("fundsChart", actions, "Total", "Funds");
|
||||||
|
// drawTotalProfit("totalProfitChat", actions);
|
||||||
|
// drawTotalProfit("fundsChat", actions);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user