From 6194adcc21cf00762512b59f403096dd8a4472b4 Mon Sep 17 00:00:00 2001 From: lychiyu Date: Wed, 26 Jun 2024 00:59:13 +0800 Subject: [PATCH] first commit --- README.md | 2 + common/balance.go | 114 ++++++++++++++++ common/balance_lever.go | 122 +++++++++++++++++ common/balance_lever_test.go | 61 +++++++++ common/balance_test.go | 254 +++++++++++++++++++++++++++++++++++ common/binsize.go | 84 ++++++++++++ common/binsize_test.go | 39 ++++++ common/browser.go | 23 ++++ common/engine.go | 129 ++++++++++++++++++ common/engine_test.go | 37 +++++ common/float.go | 47 +++++++ common/merge.go | 153 +++++++++++++++++++++ common/merge_test.go | 76 +++++++++++ common/path.go | 69 ++++++++++ engine/interface.go | 34 +++++ fsm/fsm.go | 67 +++++++++ fsm/fsm_test.go | 30 +++++ go.mod | 19 +++ go.sum | 26 ++++ 19 files changed, 1386 insertions(+) create mode 100644 README.md create mode 100644 common/balance.go create mode 100644 common/balance_lever.go create mode 100644 common/balance_lever_test.go create mode 100644 common/balance_test.go create mode 100644 common/binsize.go create mode 100644 common/binsize_test.go create mode 100644 common/browser.go create mode 100644 common/engine.go create mode 100644 common/engine_test.go create mode 100644 common/float.go create mode 100644 common/merge.go create mode 100644 common/merge_test.go create mode 100644 common/path.go create mode 100644 engine/interface.go create mode 100644 fsm/fsm.go create mode 100644 fsm/fsm_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9edd50 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# base +base module of coin-quant diff --git a/common/balance.go b/common/balance.go new file mode 100644 index 0000000..caae35f --- /dev/null +++ b/common/balance.go @@ -0,0 +1,114 @@ +package common + +import ( + "errors" + + . "git.qtrade.icu/coin-quant/trademodel" + "github.com/shopspring/decimal" +) + +var ( + ErrNoBalance = errors.New("no balance") + Zero = decimal.NewFromInt(0) +) + +type VBalance struct { + total decimal.Decimal + prevRoundTotal decimal.Decimal + position decimal.Decimal + feeTotal decimal.Decimal + // 开仓的总价值 + longCost decimal.Decimal + shortCost decimal.Decimal + fee decimal.Decimal + prevFee decimal.Decimal +} + +func NewVBalance() *VBalance { + b := new(VBalance) + b.total = decimal.NewFromFloat(100000) + b.prevRoundTotal = b.total + b.fee = decimal.NewFromFloat(0.00075) + return b +} + +func (b *VBalance) Set(total float64) { + b.total = decimal.NewFromFloat(total) + b.prevRoundTotal = b.total +} + +func (b *VBalance) SetFee(fee float64) { + b.fee = decimal.NewFromFloat(fee) +} + +func (b *VBalance) Pos() (pos float64) { + pos, _ = b.position.Float64() + return +} + +func (b *VBalance) Get() (total float64) { + // return b.total + b.costTotal + total, _ = b.total.Float64() + return +} + +func (b *VBalance) GetFeeTotal() (fee float64) { + fee, _ = b.feeTotal.Float64() + return +} + +func (b *VBalance) AvgOpenPrice() (price float64) { + switch b.position.Sign() { + case -1: + price, _ = b.shortCost.Div(b.position.Abs()).Float64() + case 0: + return + case 1: + price, _ = b.longCost.Div(b.position.Abs()).Float64() + } + + return +} + +func (b *VBalance) AddTrade(tr Trade) (profit, onceFee float64, err error) { + amount := decimal.NewFromFloat(tr.Amount).Abs() + // 仓位价值 + cost := amount.Mul(decimal.NewFromFloat(tr.Price)).Abs() + fee := cost.Mul(b.fee) + onceFee, _ = fee.Float64() + costAll, _ := cost.Add(fee).Float64() + if tr.Action.IsOpen() && costAll >= b.Get() { + err = ErrNoBalance + return + } + // close/stop just return if no position + if b.position.Equal(Zero) && !tr.Action.IsOpen() { + return + } + if tr.Action.IsLong() { + b.position = b.position.Add(amount) + b.longCost = b.longCost.Add(cost) + } else { + b.position = b.position.Sub(amount) + b.shortCost = b.shortCost.Add(cost) + } + isPositionZero := b.position.Equal(Zero) + if tr.Action.IsOpen() && !isPositionZero { + b.total = b.total.Sub(cost).Sub(fee) + } + b.feeTotal = b.feeTotal.Add(fee) + // 计算盈利 + if isPositionZero { + totalFee := fee.Add(b.prevFee) + prof := b.shortCost.Sub(b.longCost).Sub(totalFee) + b.total = b.prevRoundTotal.Add(prof) + profit, _ = prof.Float64() + b.longCost = decimal.NewFromInt(0) + b.shortCost = decimal.NewFromInt(0) + b.prevRoundTotal = b.total + b.prevFee = decimal.Zero + } else { + b.prevFee = b.prevFee.Add(fee) + } + return +} diff --git a/common/balance_lever.go b/common/balance_lever.go new file mode 100644 index 0000000..a269656 --- /dev/null +++ b/common/balance_lever.go @@ -0,0 +1,122 @@ +package common + +import ( + . "git.qtrade.icu/coin-quant/trademodel" + "github.com/shopspring/decimal" +) + +var ( + numOne = decimal.NewFromInt(1) +) + +type LeverBalance struct { + vBalance *VBalance + total decimal.Decimal + + // 开仓的总价值 + lever decimal.Decimal +} + +func NewLeverBalance() *LeverBalance { + lb := new(LeverBalance) + lb.vBalance = NewVBalance() + lb.lever = decimal.NewFromFloat(1) + return lb +} + +func (b *LeverBalance) Set(total float64) { + b.total = decimal.NewFromFloat(total) + vTotal, _ := b.total.Mul(b.lever).Float64() + b.vBalance.Set(vTotal) +} + +func (b *LeverBalance) SetFee(fee float64) { + b.vBalance.SetFee(fee) +} + +func (b *LeverBalance) SetLever(lever float64) { + b.lever = decimal.NewFromFloat(lever) + vTotal, _ := b.total.Mul(b.lever).Float64() + b.vBalance.Set(vTotal) +} + +func (b *LeverBalance) Pos() (pos float64) { + return b.vBalance.Pos() +} + +// func (b *LeverBalance) LiquidationPrice() (price float64, valid bool) { +// pos, _ := b.position.Float64() +// if pos == 0 { +// return +// } +// valid = true +// if pos > 0 { +// price, _ = b.openPrice.Sub(b.openPrice.Div(b.lever)).Float64() +// } else { +// price, _ = b.openPrice.Add(b.openPrice.Div(b.lever)).Float64() +// } +// return +// } + +func (b *LeverBalance) CheckLiquidation(price float64) (liqPrice float64, isLiq bool) { + openPrice := decimal.NewFromFloat(b.vBalance.AvgOpenPrice()) + fee := b.vBalance.fee + switch b.vBalance.position.Sign() { + // <0 + case -1: + // liqPrice + liqPrice * fee = openPrice + openPrice/lever + // liqPrice *(1 + fee) = openPrice * (1 + 1/lever) + // liqPrice = (openPrice * (1 + 1/lever))/(1-fee) + liqPrice, _ = openPrice.Add(openPrice.Div(b.lever)).Div(numOne.Add(fee)).Float64() + if price >= liqPrice { + isLiq = true + } + // =0 + case 0: + return + // >0 + case 1: + // liqPrice - liqPrice * fee = openPrice - openPrice/lever + // (1-fee) * liqPrice = openPrice * (1 - 1/lever) + // liqPrice = (openPrice * (1 - 1/lever))/(1-fee) + liqPrice, _ = openPrice.Sub(openPrice.Div(b.lever)).Div(numOne.Sub(fee)).Float64() + if price <= liqPrice { + isLiq = true + } + } + return +} + +func (b *LeverBalance) Get() (total float64) { + total, _ = b.total.Float64() + return +} + +func (b *LeverBalance) GetFeeTotal() float64 { + return b.vBalance.GetFeeTotal() +} + +func (b *LeverBalance) AddTrade(tr Trade) (profit, onceFee float64, err error) { + if tr.Action.IsOpen() { + // check balance enough when open order + amount := decimal.NewFromFloat(tr.Amount).Abs() + cost := amount.Mul(decimal.NewFromFloat(tr.Price)).Abs() + fee := cost.Mul(b.vBalance.fee) + onceCost := cost.Div(b.lever).Add(fee) + if b.total.LessThan(onceCost) { + err = ErrNoBalance + return + } + } else { + liqPrice, isLiq := b.CheckLiquidation(tr.Price) + if isLiq { + tr.Price = liqPrice + } + } + profit, onceFee, err = b.vBalance.AddTrade(tr) + if err != nil { + return + } + b.total = b.total.Add(decimal.NewFromFloat(profit)).Sub(decimal.NewFromFloat(onceFee)) + return +} diff --git a/common/balance_lever_test.go b/common/balance_lever_test.go new file mode 100644 index 0000000..2f3b153 --- /dev/null +++ b/common/balance_lever_test.go @@ -0,0 +1,61 @@ +package common + +import ( + "testing" + + . "git.qtrade.icu/coin-quant/trademodel" +) + +func TestCheckLiquidationLong(t *testing.T) { + lb := NewLeverBalance() + lb.Set(100) + lb.SetFee(0.0002) + lb.SetLever(10) + _, _, err := lb.AddTrade(Trade{Action: OpenLong, Price: 100, Amount: 9}) + if err != nil { + t.Fatal("Liq lever AddTrade failed:" + err.Error()) + } + liqPrice, isLiq := lb.CheckLiquidation(90.1) + if isLiq { + t.Fatal("Liq cal too large") + } + t.Log(liqPrice, isLiq) + liqPrice, isLiq = lb.CheckLiquidation(90) + if !isLiq { + t.Fatal("Liq cal too small") + } + t.Log(liqPrice, isLiq) +} + +func TestCheckLiquidationShort(t *testing.T) { + lb := NewLeverBalance() + lb.Set(100) + lb.SetFee(0.0002) + lb.SetLever(10) + _, _, err := lb.AddTrade(Trade{Action: OpenShort, Price: 100, Amount: 9}) + if err != nil { + t.Fatal("Liq lever AddTrade failed:" + err.Error()) + } + liqPrice, isLiq := lb.CheckLiquidation(109) + if isLiq { + t.Fatal("Liq cal too small") + } + t.Log(liqPrice, isLiq) + liqPrice, isLiq = lb.CheckLiquidation(109.99) + if !isLiq { + t.Fatal("Liq cal too large") + } + t.Log(liqPrice, isLiq) +} + +func TestCheckLeverBalance(t *testing.T) { + lb := NewLeverBalance() + lb.Set(100) + lb.SetFee(0.0002) + lb.SetLever(10) + _, _, err := lb.AddTrade(Trade{Action: OpenLong, Price: 100, Amount: 10}) + if err == nil { + t.Fatal("Liq not work") + } + t.Log(err.Error()) +} diff --git a/common/balance_test.go b/common/balance_test.go new file mode 100644 index 0000000..a0d5fe5 --- /dev/null +++ b/common/balance_test.go @@ -0,0 +1,254 @@ +package common + +import ( + "testing" + "time" + + . "git.qtrade.icu/coin-quant/trademodel" + "github.com/shopspring/decimal" +) + +func calFee(fee decimal.Decimal, trades ...Trade) float64 { + var cost decimal.Decimal + for _, v := range trades { + dec := decimal.NewFromFloat(v.Price).Mul(decimal.NewFromFloat(v.Amount)) + cost = cost.Add(dec) + } + realFee, _ := cost.Mul(fee).Float64() + return realFee +} + +func TestLong(t *testing.T) { + tm := time.Now() + openTrade := Trade{ + ID: "1", + Action: OpenLong, + Time: tm, + Price: 100, + Amount: 1, + } + closeTrade := Trade{ + ID: "2", + Action: CloseLong, + Time: tm.Add(time.Second), + Price: 110, + Amount: 1, + } + stopTrade := Trade{ + ID: "3", + Action: StopLong, + Time: tm.Add(time.Second * 2), + Price: 90, + Amount: 1, + } + + b := NewVBalance() + b.Set(1000) + b.AddTrade(openTrade) + b.AddTrade(closeTrade) + fee := calFee(b.fee, openTrade, closeTrade) + if b.Get() != 1010-fee { + t.Fatal("balance close error:", b.Get(), 1010-fee) + } + b.Set(1000) + b.AddTrade(openTrade) + b.AddTrade(stopTrade) + + fee = calFee(b.fee, openTrade, stopTrade) + if b.Get() != 990-fee { + t.Fatal("balance stop error:", b.Get()) + } +} + +func TestMultiLong(t *testing.T) { + tm := time.Now() + openTrade := Trade{ + ID: "1", + Action: OpenLong, + Time: tm, + Price: 100, + Amount: 1, + } + openTrade2 := Trade{ + ID: "1", + Action: OpenLong, + Time: tm, + Price: 105, + Amount: 1, + } + closeTrade := Trade{ + ID: "2", + Action: CloseLong, + Time: tm.Add(time.Second), + Price: 110, + Amount: 2, + } + stopTrade := Trade{ + ID: "3", + Action: StopLong, + Time: tm.Add(time.Second * 2), + Price: 90, + Amount: 2, + } + + b := NewVBalance() + b.Set(1000) + b.AddTrade(openTrade) + b.AddTrade(openTrade2) + b.AddTrade(closeTrade) + fee := calFee(b.fee, openTrade, openTrade2, closeTrade) + if b.Get() != 1015-fee { + t.Fatal("balance close error:", b.Get(), fee) + } + b.Set(1000) + b.AddTrade(openTrade) + b.AddTrade(openTrade2) + b.AddTrade(stopTrade) + fee = calFee(b.fee, openTrade, openTrade2, stopTrade) + if b.Get() != 975-fee { + t.Fatal("balance stop error:", b.Get()) + } +} + +func TestShort(t *testing.T) { + tm := time.Now() + openTrade := Trade{ + ID: "1", + Action: OpenShort, + Time: tm, + Price: 110, + Amount: 1, + } + closeTrade := Trade{ + ID: "2", + Action: CloseShort, + Time: tm.Add(time.Second), + Price: 100, + Amount: 1, + } + stopTrade := Trade{ + ID: "3", + Action: StopShort, + Time: tm.Add(time.Second * 2), + Price: 120, + Amount: 1, + } + + b := NewVBalance() + b.Set(1000) + b.AddTrade(openTrade) + b.AddTrade(closeTrade) + fee := calFee(b.fee, openTrade, closeTrade) + if b.Get() != 1010-fee { + t.Fatal("balance close error:", b.Get(), 1010-fee) + } + b.Set(1000) + b.AddTrade(openTrade) + b.AddTrade(stopTrade) + fee = calFee(b.fee, openTrade, stopTrade) + if b.Get() != 990-fee { + t.Fatal("balance stop error:", b.Get()) + } +} + +func TestMultiShort(t *testing.T) { + tm := time.Now() + openTrade := Trade{ + ID: "1", + Action: OpenShort, + Time: tm, + Price: 110, + Amount: 1, + } + openTrade2 := Trade{ + ID: "1", + Action: OpenShort, + Time: tm, + Price: 120, + Amount: 1, + } + closeTrade := Trade{ + ID: "2", + Action: CloseShort, + Time: tm.Add(time.Second), + Price: 100, + Amount: 2, + } + stopTrade := Trade{ + ID: "3", + Action: StopShort, + Time: tm.Add(time.Second * 2), + Price: 130, + Amount: 2, + } + + b := NewVBalance() + b.Set(1000) + b.AddTrade(openTrade) + b.AddTrade(openTrade2) + b.AddTrade(closeTrade) + fee := calFee(b.fee, openTrade, openTrade2, closeTrade) + if b.Get() != 1030-fee { + t.Fatal("balance close error:", b.Get()) + } + b.Set(1000) + b.AddTrade(openTrade) + b.AddTrade(openTrade2) + b.AddTrade(stopTrade) + fee = calFee(b.fee, openTrade, openTrade2, stopTrade) + if b.Get() != 970-fee { + t.Fatal("balance stop error:", b.Get()) + } +} + +func TestAvgPriceLong(t *testing.T) { + tm := time.Now() + openTrade := Trade{ + ID: "1", + Action: OpenLong, + Time: tm, + Price: 110, + Amount: 1, + } + openTrade2 := Trade{ + ID: "1", + Action: OpenLong, + Time: tm, + Price: 120, + Amount: 3, + } + + b := NewVBalance() + b.Set(1000) + b.AddTrade(openTrade) + b.AddTrade(openTrade2) + if b.AvgOpenPrice() != 117.5 { + t.Fatalf("cal avg failed: %f", b.AvgOpenPrice()) + } +} + +func TestAvgPriceShort(t *testing.T) { + tm := time.Now() + openTrade := Trade{ + ID: "1", + Action: OpenShort, + Time: tm, + Price: 110, + Amount: 1, + } + openTrade2 := Trade{ + ID: "1", + Action: OpenShort, + Time: tm, + Price: 120, + Amount: 3, + } + + b := NewVBalance() + b.Set(1000) + b.AddTrade(openTrade) + b.AddTrade(openTrade2) + if b.AvgOpenPrice() != 117.5 { + t.Fatalf("cal avg failed: %f", b.AvgOpenPrice()) + } +} diff --git a/common/binsize.go b/common/binsize.go new file mode 100644 index 0000000..6ec2feb --- /dev/null +++ b/common/binsize.go @@ -0,0 +1,84 @@ +package common + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" +) + +const ( + DefaultBinSizes = "1m, 5m, 15m, 30m, 1h, 4h, 1d" +) + +var ( + Day = time.Hour * 24 + Week = time.Hour * 24 * 7 +) + +// ParseBinStrs parse binsizes to strs +func ParseBinStrs(str string) (strs []string) { + bins := strings.Split(str, ",") + var temp string + for _, v := range bins { + temp = strings.Trim(v, " ") + strs = append(strs, temp) + } + return +} + +// ParseBinSizes parse binsizes +func ParseBinSizes(str string) (durations []time.Duration, err error) { + strs := ParseBinStrs(str) + var t time.Duration + for _, v := range strs { + t, err = GetBinSizeDuration(v) + if err != nil { + return + } + durations = append(durations, t) + } + return +} + +// GetBinSizeDuration get duration of the binsize +func GetBinSizeDuration(str string) (duration time.Duration, err error) { + if len(str) == 0 { + err = errors.New("binsize is empty") + return + } + n, err := strconv.ParseInt(str, 10, 64) + if err == nil { + duration = time.Duration(n) * time.Minute + return + } + err = nil + char := str[len(str)-1] + switch char { + case 's', 'S': + duration = time.Second + case 'm': + duration = time.Minute + case 'h': + duration = time.Hour + case 'd', 'D': + duration = Day + case 'w', 'W': + duration = Week + default: + err = fmt.Errorf("unsupport binsize: %s", str) + return + } + if len(str) == 1 { + return + } + value := str[0 : len(str)-1] + n, err = strconv.ParseInt(value, 10, 64) + if err != nil { + err = fmt.Errorf("parse binsize error:%s", err.Error()) + return + } + duration = time.Duration(n) * duration + return +} diff --git a/common/binsize_test.go b/common/binsize_test.go new file mode 100644 index 0000000..494e0fa --- /dev/null +++ b/common/binsize_test.go @@ -0,0 +1,39 @@ +package common + +import ( + "testing" + "time" +) + +func TestGetBinSizeDuration(t *testing.T) { + testMap := map[string]time.Duration{ + "1s": time.Second, + "5s": 5 * time.Second, + "m": time.Minute, + "1m": time.Minute, + "5m": 5 * time.Minute, + "15m": 15 * time.Minute, + "30m": 30 * time.Minute, + "1h": time.Hour, + "4h": 4 * time.Hour, + "6h": 6 * time.Hour, + "1d": Day, + "7d": Week, + "1w": Week, + "1": time.Minute, + "15": 15 * time.Minute, + "60": time.Hour, + } + + var temp time.Duration + var err error + for k, v := range testMap { + temp, err = GetBinSizeDuration(k) + if err != nil { + t.Fatalf("parse %s failed:%s", k, err.Error()) + } + if temp != v { + t.Fatalf("GetBinSizeDuration failed:%s %s", temp, v) + } + } +} diff --git a/common/browser.go b/common/browser.go new file mode 100644 index 0000000..38627cf --- /dev/null +++ b/common/browser.go @@ -0,0 +1,23 @@ +package common + +import ( + "os/exec" + "runtime" +) + +func OpenURL(strURL string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + args = append(args, strURL) + return exec.Command(cmd, args...).Start() +} diff --git a/common/engine.go b/common/engine.go new file mode 100644 index 0000000..7ad6723 --- /dev/null +++ b/common/engine.go @@ -0,0 +1,129 @@ +package common + +import ( + "fmt" + . "git.qtrade.icu/coin-quant/trademodel" + "github.com/bitly/go-simplejson" +) + +type CandleFn func(candle *Candle) + +type Entry struct { + Value interface{} + Label string +} + +type Param struct { + Name string + Type string + Label string + Info string + DefValue interface{} + Enums []Entry + ptr interface{} +} + +func StringParam(name, label, info, defValue string, ptr *string, enums ...Entry) Param { + *ptr = defValue + return Param{Name: name, Type: "string", Label: label, Info: info, DefValue: defValue, Enums: enums, ptr: ptr} +} + +func IntParam(name, label, info string, defValue int, ptr *int, enums ...Entry) Param { + *ptr = defValue + return Param{Name: name, Type: "int", Label: label, Info: info, DefValue: defValue, Enums: enums, ptr: ptr} +} + +func FloatParam(name, label, info string, defValue float64, ptr *float64, enums ...Entry) Param { + *ptr = defValue + return Param{Name: name, Type: "float", Label: label, Info: info, DefValue: defValue, Enums: enums, ptr: ptr} +} + +func BoolParam(name, label, info string, defValue bool, ptr *bool, enums ...Entry) Param { + *ptr = defValue + return Param{Name: name, Type: "bool", Label: label, Info: info, DefValue: defValue, Enums: enums, ptr: ptr} +} + +func ParseParams(str string, params []Param) (data ParamData, err error) { + data = make(ParamData) + sj := simplejson.New() + err = sj.UnmarshalJSON([]byte(str)) + if err != nil { + return + } + + var temp *simplejson.Json + var ok, boolV bool + var strV string + var intV int + var floatV float64 + for _, v := range params { + if v.ptr == nil { + data[v.Name] = sj.Get(v.Name).Interface() + return + } + temp, ok = sj.CheckGet(v.Name) + if !ok { + continue + } + + switch ptr := v.ptr.(type) { + case *string: + strV, err = temp.String() + if err != nil { + return + } + *ptr = strV + data[v.Name] = strV + case *float64: + floatV, err = temp.Float64() + if err != nil { + return + } + *ptr = floatV + data[v.Name] = floatV + case *int: + intV, err = temp.Int() + if err != nil { + return + } + *ptr = intV + data[v.Name] = intV + case *bool: + boolV, err = temp.Bool() + if err != nil { + return + } + *ptr = boolV + data[v.Name] = boolV + default: + err = fmt.Errorf("unsupport value type: %##v", ptr) + return + } + } + return +} + +type ParamData map[string]interface{} + +func (d ParamData) GetString(key, defaultValue string) string { + v, ok := d[key] + if !ok { + return defaultValue + } + ret := v.(string) + if ret == "" { + return defaultValue + } + return ret +} +func (d ParamData) GetFloat(key string, defaultValue float64) float64 { + v, ok := d[key] + if !ok { + return defaultValue + } + ret := v.(float64) + if ret == 0 { + return defaultValue + } + return ret +} diff --git a/common/engine_test.go b/common/engine_test.go new file mode 100644 index 0000000..1025ebc --- /dev/null +++ b/common/engine_test.go @@ -0,0 +1,37 @@ +package common + +import ( + "testing" +) + +func TestParam(t *testing.T) { + var str1, str2 string + var int1, int2 int + var float1, float2 float64 + params := []Param{ + StringParam("str1", "str test", "just a simple string", "a", &str1), + StringParam("str2", "str test", "enum string", "B", &str2, + Entry{Label: "A", Value: "A"}, + Entry{Label: "B", Value: "B"}), + IntParam("int1", "int1 test", "just a simple int", 1, &int1), + IntParam("int2", "int2 test", "enum int", 1, &int2, + Entry{Label: "A", Value: 1}, + Entry{Label: "B", Value: 2}), + FloatParam("float1", "float1 test", "just a simple int", 1, &float1), + FloatParam("float2", "float2 test", "enum float", 1, &float2, + Entry{Label: "A", Value: 1.0}, + Entry{Label: "B", Value: 2.0}), + } + + str := `{"str1": "str1", "str2":"A", "int1": 10, "int2": 1, "float1": 3, "float2": 2.0}` + rets, err := ParseParams(str, params) + if err != nil { + t.Fatal(err.Error()) + } + if str1 != "str1" || str2 != "A" || int1 != 10 || int2 != 1 || float1 != 3 || float2 != 2.0 { + t.Fatal("value not match", str1, str2, int1, int2, float1, float2) + } + if rets["str1"] != str1 || rets["str2"] != str2 || rets["int1"] != int1 || rets["int2"] != int2 || rets["float1"] != float1 || rets["float2"] != float2 { + t.Fatal("value not match", str1, str2, int1, int2, float1, float2, rets) + } +} diff --git a/common/float.go b/common/float.go new file mode 100644 index 0000000..3f6baba --- /dev/null +++ b/common/float.go @@ -0,0 +1,47 @@ +package common + +import ( + "fmt" + "strconv" + + "github.com/shopspring/decimal" +) + +// FloatMul return a*b +func FloatMul(a, b float64) float64 { + aDec := decimal.NewFromFloat(a) + bDec := decimal.NewFromFloat(b) + ret, _ := aDec.Mul(bDec).Float64() + return ret +} + +// FloatAdd return a*b +func FloatAdd(a, b float64) float64 { + aDec := decimal.NewFromFloat(a) + bDec := decimal.NewFromFloat(b) + ret, _ := aDec.Add(bDec).Float64() + return ret +} + +// FloatSub return a-b +func FloatSub(a, b float64) float64 { + aDec := decimal.NewFromFloat(a) + bDec := decimal.NewFromFloat(b) + ret, _ := aDec.Sub(bDec).Float64() + return ret +} + +// FloatDiv return a/b +func FloatDiv(a, b float64) float64 { + aDec := decimal.NewFromFloat(a) + bDec := decimal.NewFromFloat(b) + ret, _ := aDec.Div(bDec).Float64() + return ret +} + +// FormatFloat format float with precision +func FormatFloat(n float64, precision int) float64 { + str := fmt.Sprintf("%df", precision) + n2, _ := strconv.ParseFloat(fmt.Sprintf("%."+str, n), 64) + return n2 +} diff --git a/common/merge.go b/common/merge.go new file mode 100644 index 0000000..59b839d --- /dev/null +++ b/common/merge.go @@ -0,0 +1,153 @@ +package common + +import ( + "fmt" + "time" + + . "git.qtrade.icu/coin-quant/trademodel" + log "github.com/sirupsen/logrus" +) + +// MergeKlineChan merge kline data +func MergeKlineChan(klines chan []interface{}, srcDuration, dstDuration time.Duration) (rets chan []interface{}) { + rets = make(chan []interface{}, len(klines)) + go func() { + km := NewKlineMerge(srcDuration, dstDuration) + var temp interface{} + for v := range klines { + tempDatas := []interface{}{} + for _, d := range v { + temp = km.Update(d) + if temp != nil { + tempDatas = append(tempDatas, temp) + } + } + if len(tempDatas) != 0 { + rets <- tempDatas + } + } + close(rets) + }() + return +} + +// KlineMerge merge kline to new duration +type KlineMerge struct { + src int64 // src kline seconds + dst int64 // dst kline seconds + ratio int // dst/src kline ration + cache CandleList // kline cache + bFirst bool + nextStart int64 +} + +// NewKlineMergeStr new KlineMerge with string duration +func NewKlineMergeStr(src, dst string) *KlineMerge { + srcDur, err := time.ParseDuration(src) + if err != nil { + log.Errorf("NewKlineMergeStr parse src %s error: %s", src, err.Error()) + return nil + } + dstDur, err := time.ParseDuration(dst) + if err != nil { + log.Errorf("NewKlineMergeStr parse dst %s error: %s", dst, err.Error()) + return nil + } + return NewKlineMerge(srcDur, dstDur) +} + +// NewKlineMerge merge kline constructor +func NewKlineMerge(src, dst time.Duration) *KlineMerge { + km := new(KlineMerge) + km.src = int64(src / time.Second) + km.dst = int64(dst / time.Second) + km.ratio = int(dst / src) + km.bFirst = true + return km +} + +// IsFirst is first time +func (km *KlineMerge) IsFirst() bool { + return km.bFirst +} + +// NeedMerge is kline need merge +func (km *KlineMerge) NeedMerge() bool { + return km.ratio != 1 +} + +// GetSrc return kline source duration secs +func (km *KlineMerge) GetSrc() int64 { + return km.src +} + +// GetSrcDuration get kline source duration +func (km *KlineMerge) GetSrcDuration() time.Duration { + return time.Duration(km.src) * time.Second +} + +// GetDstDuration get kline dst duration +func (km *KlineMerge) GetDstDuration() time.Duration { + return time.Duration(km.dst) * time.Second +} + +// GetDst return kline dst duration secs +func (km *KlineMerge) GetDst() int64 { + return km.dst +} + +// Update update candle, and return new kline candle +// return nil if no new kline candle +func (km *KlineMerge) Update(data interface{}) (ret interface{}) { + // return if no need to merge + if km.ratio == 1 { + ret = data + return + } + candle, ok := data.(*Candle) + if !ok { + panic(fmt.Sprintf("KlineMerge data type error:%#v", data)) + return + } + n := len(km.cache) + if n > 0 && candle.Start <= km.cache[n-1].Start { + return + } + if km.bFirst && candle.Start%km.dst != 0 { + return + } + km.bFirst = false + var bNew bool + if candle.Start >= km.nextStart { + km.nextStart = (candle.Start/km.dst + 1) * km.dst + if n != 0 { + ret = km.cache.Merge() + km.cache = CandleList{} + bNew = true + } + } + // add current candle to cache + index := int(candle.Start%km.dst)/int(km.src) + 1 + km.cache = append(km.cache, candle) + if bNew || index != km.ratio { + return + } + defer func() { + // reset cache after kline merged + km.cache = CandleList{} + }() + // cache length not match,just skip + if len(km.cache) != km.ratio { + log.Warnf("cache length not match,real:%d, want:%d", len(km.cache), km.ratio) + // return + } + ret = km.cache.Merge() + return +} + +func (km *KlineMerge) GetUnFinished() (ret interface{}) { + if len(km.cache) == 0 { + return nil + } + return km.cache.Merge() +} diff --git a/common/merge_test.go b/common/merge_test.go new file mode 100644 index 0000000..ecddcb5 --- /dev/null +++ b/common/merge_test.go @@ -0,0 +1,76 @@ +package common + +import ( + "testing" + "time" + + . "git.qtrade.icu/coin-quant/trademodel" +) + +func getTestData(source, dst time.Duration) (candles CandleList) { + nSourceSec := int64(source / time.Second) + nStart := nSourceSec * (time.Now().Add(0-source*20).Unix() / nSourceSec) + candle := Candle{ + Start: nStart, + Open: 100, + High: 200, + Low: 50, + Close: 110, + Turnover: 1, + Volume: 1, + Trades: 10, + } + for i := 0; i != 10; i++ { + temp := candle + temp.Open = candle.Open + float64(i) + temp.High = candle.High + float64(i) + temp.Low = candle.Low + float64(i) + temp.Close = candle.Close + float64(i) + temp.Turnover = candle.Turnover + float64(i) + temp.Volume = candle.Volume + float64(i) + temp.Trades = candle.Trades + int64(i) + candles = append(candles, &temp) + candle.Start += nSourceSec + } + return +} + +func TestMergeKline(t *testing.T) { + source := time.Minute * 5 + dst := time.Minute * 15 + candles := getTestData(source, dst) + m := NewKlineMerge(source, dst) + for _, v := range candles { + t.Log("candle:", v) + } + var ret interface{} + for _, v := range candles { + ret = m.Update(v) + if ret == nil { + continue + } + t.Log("ret:", ret) + } +} + +func TestMergeKlineChan(t *testing.T) { + klines := make(chan []interface{}, 10) + source := time.Minute * 5 + dst := time.Minute * 15 + candles := getTestData(source, dst) + go func() { + datas := make([]interface{}, len(candles)) + for k, v := range candles { + t.Log(v) + datas[k] = v + } + klines <- datas + close(klines) + }() + ret := MergeKlineChan(klines, source, dst) + for v := range ret { + for _, d := range v { + t.Log("ret:", d) + } + } +} diff --git a/common/path.go b/common/path.go new file mode 100644 index 0000000..0d21a52 --- /dev/null +++ b/common/path.go @@ -0,0 +1,69 @@ +package common + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + "regexp" +) + +var ( + pkgRegexp = regexp.MustCompile(`^package \w+\n$`) +) + +// GetExecDir return exec dir +func GetExecDir() string { + dir, _ := os.Executable() + exPath := filepath.Dir(dir) + return exPath +} + +func CopyWithMainPkg(dst, src string) (err error) { + fSrc, err := os.Open(src) + if err != nil { + err = fmt.Errorf("open %s file failed:%w", src, err) + return + } + defer fSrc.Close() + fDst, err := os.Create(dst) + if err != nil { + err = fmt.Errorf("create %s file failed:%w", dst, err) + return + } + defer fDst.Close() + r := bufio.NewReader(fSrc) + var line string + for err == nil { + line, err = r.ReadString('\n') + if err != nil && err != io.EOF { + break + } + if pkgRegexp.MatchString(line) { + line = "package main" + } + fDst.Write([]byte(line)) + } + if err == io.EOF { + err = nil + } + return +} + +func Copy(dst, src string) (err error) { + fSrc, err := os.Open(src) + if err != nil { + err = fmt.Errorf("open %s file failed:%w", src, err) + return + } + defer fSrc.Close() + fDst, err := os.Create(dst) + if err != nil { + err = fmt.Errorf("create %s file failed:%w", dst, err) + return + } + defer fDst.Close() + _, err = io.Copy(fDst, fSrc) + return +} diff --git a/engine/interface.go b/engine/interface.go new file mode 100644 index 0000000..152ea33 --- /dev/null +++ b/engine/interface.go @@ -0,0 +1,34 @@ +package engine + +import ( + "git.qtrade.icu/coin-quant/base/common" + "git.qtrade.icu/coin-quant/indicator" + "git.qtrade.icu/coin-quant/trademodel" +) + +const ( + StatusRunning = 0 + StatusSuccess = 1 + StatusFail = -1 +) + +type Engine interface { + OpenLong(price, amount float64) string + CloseLong(price, amount float64) string + OpenShort(price, amount float64) string + CloseShort(price, amount float64) string + StopLong(price, amount float64) string + StopShort(price, amount float64) string + 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) + SendNotify(title, content, contentType string) + Merge(src, dst string, fn common.CandleFn) + SetBalance(balance float64) + UpdateStatus(status int, msg string) +} diff --git a/fsm/fsm.go b/fsm/fsm.go new file mode 100644 index 0000000..6c55ba2 --- /dev/null +++ b/fsm/fsm.go @@ -0,0 +1,67 @@ +package fsm + +import "fmt" + +type Rule struct { + Name string + Src []string + Dst string +} + +type EventDesc struct { + Name string + Src string + Dst string + Args []interface{} +} + +type Callback func(event EventDesc) + +type FSM struct { + state string + rules []Rule + callbacks map[string]Callback +} + +func NewFSM(initia string, rules []Rule) *FSM { + f := new(FSM) + f.callbacks = make(map[string]Callback) + f.state = initia + f.rules = rules + return f +} + +func (f *FSM) SetCallback(state string, cb Callback) { + f.callbacks[state] = cb +} + +func (f *FSM) Event(event string, args ...interface{}) (err error) { + var src, dst string +Out: + for _, v := range f.rules { + if v.Name == event { + for _, s := range v.Src { + if s == f.state { + dst = v.Dst + src = s + break Out + } + } + } + } + if dst == "" { + err = fmt.Errorf("current state: %s,skip event:%s", f.state, event) + return + } + f.state = dst + cb, ok := f.callbacks[dst] + if !ok { + return + } + cb(EventDesc{Name: event, Src: src, Dst: dst, Args: args}) + return +} + +func (f *FSM) Current() string { + return f.state +} diff --git a/fsm/fsm_test.go b/fsm/fsm_test.go new file mode 100644 index 0000000..878aa2f --- /dev/null +++ b/fsm/fsm_test.go @@ -0,0 +1,30 @@ +package fsm + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFSM(t *testing.T) { + rules := []Rule{ + {"openOrder", []string{"init"}, "open"}, + {"stopOrder", []string{"open", "openMore"}, "init"}, + {"closeOrder", []string{"open", "openMore"}, "init"}, + {"openOrder", []string{"open"}, "openMore"}, + } + args := []interface{}{"Hello", 1, 2, 3} + cb := func(event EventDesc) { + assert.Equal(t, event, EventDesc{Name: "openOrder", Src: "init", Dst: "open", Args: args}, "state error") + t.Log(event.Name, event.Src, event.Dst, event.Args) + } + fsm := NewFSM("init", rules) + fsm.SetCallback("open", cb) + assert.Equal(t, fsm.Current(), "init", "init state error") + fsm.Event("openOrder", args...) + assert.Equal(t, fsm.Current(), "open", "open state error") + err := fsm.Event("test") + assert.NotNil(t, err, "error check failed") + fsm.Event("closeOrder") + assert.Equal(t, fsm.Current(), "init", "close state error") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ab5a977 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module git.qtrade.icu/coin-quant/base + +go 1.22.0 + +require ( + git.qtrade.icu/coin-quant/indicator v0.0.0-20240625151736-c23020eee562 + git.qtrade.icu/coin-quant/trademodel v0.0.0-20240625151548-cef4b6fc28b9 + github.com/bitly/go-simplejson v0.5.1 + github.com/shopspring/decimal v1.4.0 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..672a10f --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +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= +github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= +github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=