diff --git a/pkg/risk/riskcontrol/price.go b/pkg/risk/riskcontrol/price.go new file mode 100644 index 000000000..79ee26f5e --- /dev/null +++ b/pkg/risk/riskcontrol/price.go @@ -0,0 +1,40 @@ +package riskcontrol + +import ( + "fmt" + + "github.com/c9s/bbgo/pkg/fixedpoint" + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" + "github.com/c9s/bbgo/pkg/types" +) + +type PriceRiskControl struct { + referencePrice *indicatorv2.EWMAStream + lossThreshold fixedpoint.Value +} + +func NewPriceRiskControl(refPrice *indicatorv2.EWMAStream, threshold fixedpoint.Value) *PriceRiskControl { + return &PriceRiskControl{ + referencePrice: refPrice, + lossThreshold: threshold, + } +} + +func (r *PriceRiskControl) IsSafe(side types.SideType, price fixedpoint.Value, quantity fixedpoint.Value) bool { + refPrice := fixedpoint.NewFromFloat(r.referencePrice.Last(0)) + // calculate profit + var profit fixedpoint.Value + if side == types.SideTypeBuy { + profit = refPrice.Sub(price).Mul(quantity) + } else if side == types.SideTypeSell { + profit = price.Sub(refPrice).Mul(quantity) + } + return profit.Compare(r.lossThreshold) > 0 +} + +func (r *PriceRiskControl) IsSafeLimitOrder(o types.SubmitOrder) (bool, error) { + if !(o.Type == types.OrderTypeLimit || o.Type == types.OrderTypeLimitMaker) { + return false, fmt.Errorf("order type is not limit order") + } + return r.IsSafe(o.Side, o.Price, o.Quantity), nil +} diff --git a/pkg/risk/riskcontrol/price_test.go b/pkg/risk/riskcontrol/price_test.go new file mode 100644 index 000000000..7ae038583 --- /dev/null +++ b/pkg/risk/riskcontrol/price_test.go @@ -0,0 +1,63 @@ +package riskcontrol + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" + "github.com/c9s/bbgo/pkg/types" +) + +func Test_IsSafe(t *testing.T) { + refPrice := 30000.00 + lossThreshold := fixedpoint.NewFromFloat(-100) + + window := types.IntervalWindow{Window: 30, Interval: types.Interval1m} + refPriceEWMA := indicatorv2.EWMA2(nil, window.Window) + refPriceEWMA.PushAndEmit(refPrice) + + cases := []struct { + name string + side types.SideType + price fixedpoint.Value + quantity fixedpoint.Value + isSafe bool + }{ + { + name: "BuyingHighSafe", + side: types.SideTypeBuy, + price: fixedpoint.NewFromFloat(30040.0), + quantity: fixedpoint.NewFromFloat(1.0), + isSafe: true, + }, + { + name: "SellingLowSafe", + side: types.SideTypeSell, + price: fixedpoint.NewFromFloat(29960.0), + quantity: fixedpoint.NewFromFloat(1.0), + isSafe: true, + }, + { + name: "BuyingHighLoss", + side: types.SideTypeBuy, + price: fixedpoint.NewFromFloat(30040.0), + quantity: fixedpoint.NewFromFloat(10.0), + isSafe: false, + }, + { + name: "SellingLowLoss", + side: types.SideTypeSell, + price: fixedpoint.NewFromFloat(29960.0), + quantity: fixedpoint.NewFromFloat(10.0), + isSafe: false, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var riskControl = NewPriceRiskControl(refPriceEWMA, lossThreshold) + assert.Equal(t, tc.isSafe, riskControl.IsSafe(tc.side, tc.price, tc.quantity)) + }) + } +}