first commit

This commit is contained in:
lychiyu 2024-06-26 00:59:13 +08:00
commit 6194adcc21
19 changed files with 1386 additions and 0 deletions

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# base
base module of coin-quant

114
common/balance.go Normal file
View File

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

122
common/balance_lever.go Normal file
View File

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

View File

@ -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())
}

254
common/balance_test.go Normal file
View File

@ -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())
}
}

84
common/binsize.go Normal file
View File

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

39
common/binsize_test.go Normal file
View File

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

23
common/browser.go Normal file
View File

@ -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()
}

129
common/engine.go Normal file
View File

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

37
common/engine_test.go Normal file
View File

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

47
common/float.go Normal file
View File

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

153
common/merge.go Normal file
View File

@ -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()
}

76
common/merge_test.go Normal file
View File

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

69
common/path.go Normal file
View File

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

34
engine/interface.go Normal file
View File

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

67
fsm/fsm.go Normal file
View File

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

30
fsm/fsm_test.go Normal file
View File

@ -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")
}

19
go.mod Normal file
View File

@ -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
)

26
go.sum Normal file
View File

@ -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=