first commit

This commit is contained in:
lychiyu 2024-06-26 00:19:25 +08:00
commit 33d670ab30
62 changed files with 6399 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea
*.log
*.yaml

62
README.md Normal file
View 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
View 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
```
# 使用
## 在配置文件中填写你的secretkey
## 下载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
View 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(&param, "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
View 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
View 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
View 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
View 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
View 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(&param, "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), &paramData)
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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
package main
var NewStrategy = New{{.Name}}
var _ Runner = NewStrategy()

167
pkg/ctl/trade.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

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

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

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

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

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

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

View 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, &param)
}
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")
}

View 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)
}

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

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

View 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))},
},
})
}

View 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))},
},
})
}

View 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{},
})
}

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

View 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{},
})
}

View 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)
}

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

View 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))},
},
})
}

View 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"
)

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

View 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)
}

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

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

View 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
View 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
View 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>