mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge pull request #698 from c9s/strategy/pivot
strategy pivotshort: refactor and add stop EMA
This commit is contained in:
commit
6e3c060728
|
@ -12,28 +12,51 @@ exchangeStrategies:
|
||||||
pivotshort:
|
pivotshort:
|
||||||
symbol: ETHUSDT
|
symbol: ETHUSDT
|
||||||
interval: 5m
|
interval: 5m
|
||||||
|
pivotLength: 200
|
||||||
|
|
||||||
pivotLength: 120
|
# breakLow settings are used for shorting when the current price break the previous low
|
||||||
|
breakLow:
|
||||||
entry:
|
ratio: 0.1%
|
||||||
quantity: 10.0
|
quantity: 10.0
|
||||||
marginOrderSideEffect: borrow
|
stopEMARange: 5%
|
||||||
|
stopEMA:
|
||||||
|
interval: 1h
|
||||||
|
window: 99
|
||||||
|
|
||||||
|
bounceShort:
|
||||||
|
quantity: 10.0
|
||||||
|
# stopLossPercentage: 1%
|
||||||
|
numOfLayers: 10
|
||||||
|
layerSpread: 0.1%
|
||||||
|
pivotRatio: 0.1%
|
||||||
|
|
||||||
exit:
|
exit:
|
||||||
takeProfitPercentage: 25%
|
# roiStopLossPercentage is the stop loss percentage of the position ROI (currently the price change)
|
||||||
stopLossPercentage: 1%
|
roiStopLossPercentage: 1%
|
||||||
lowerShadowRatio: 0.95
|
|
||||||
marginOrderSideEffect: repay
|
|
||||||
|
|
||||||
|
# roiTakeProfitPercentage is the take profit percentage of the position ROI (currently the price change)
|
||||||
|
# force to take the profit ROI exceeded the percentage.
|
||||||
|
roiTakeProfitPercentage: 25%
|
||||||
|
|
||||||
|
# lowerShadowRatio is used to force taking profit when the (lower shadow height / low price) > lowerShadowRatio
|
||||||
|
# you can grab a simple stats by the following SQL:
|
||||||
|
# SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20;
|
||||||
|
lowerShadowRatio: 3%
|
||||||
|
|
||||||
|
cumulatedVolume:
|
||||||
|
minVolume: 50_000
|
||||||
|
window: 5
|
||||||
|
|
||||||
|
marginOrderSideEffect: repay
|
||||||
|
|
||||||
backtest:
|
backtest:
|
||||||
sessions:
|
sessions:
|
||||||
- binance
|
- binance
|
||||||
startTime: "2022-04-01"
|
startTime: "2022-04-01"
|
||||||
endTime: "2022-06-03"
|
endTime: "2022-06-08"
|
||||||
symbols:
|
symbols:
|
||||||
- ETHUSDT
|
- ETHUSDT
|
||||||
account:
|
accounts:
|
||||||
binance:
|
binance:
|
||||||
balances:
|
balances:
|
||||||
ETH: 10.0
|
ETH: 10.0
|
||||||
|
|
|
@ -37,7 +37,7 @@ backtest:
|
||||||
endTime: "2022-06-03"
|
endTime: "2022-06-03"
|
||||||
symbols:
|
symbols:
|
||||||
- GMTBUSD
|
- GMTBUSD
|
||||||
account:
|
accounts:
|
||||||
binance:
|
binance:
|
||||||
balances:
|
balances:
|
||||||
GMT: 3_000.0
|
GMT: 3_000.0
|
||||||
|
|
|
@ -16,15 +16,33 @@ exchangeStrategies:
|
||||||
|
|
||||||
pivotLength: 120
|
pivotLength: 120
|
||||||
|
|
||||||
entry:
|
# breakLow settings are used for shorting when the current price break the previous low
|
||||||
quantity: 3000.0
|
breakLow:
|
||||||
# marginOrderSideEffect: borrow
|
ratio: 0.1%
|
||||||
|
quantity: 10.0
|
||||||
|
stopEMARange: 5%
|
||||||
|
stopEMA:
|
||||||
|
interval: 1h
|
||||||
|
window: 99
|
||||||
|
|
||||||
exit:
|
exit:
|
||||||
takeProfitPercentage: 25%
|
# roiStopLossPercentage is the stop loss percentage of the position ROI (currently the price change)
|
||||||
stopLossPercentage: 1%
|
roiStopLossPercentage: 1%
|
||||||
lowerShadowRatio: 0.95
|
|
||||||
# marginOrderSideEffect: repay
|
# roiTakeProfitPercentage is the take profit percentage of the position ROI (currently the price change)
|
||||||
|
# force to take the profit ROI exceeded the percentage.
|
||||||
|
roiTakeProfitPercentage: 25%
|
||||||
|
|
||||||
|
# lowerShadowRatio is used to force taking profit when the (lower shadow height / low price) > lowerShadowRatio
|
||||||
|
# you can grab a simple stats by the following SQL:
|
||||||
|
# SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20;
|
||||||
|
lowerShadowRatio: 3%
|
||||||
|
|
||||||
|
cumulatedVolume:
|
||||||
|
minVolume: 50_000
|
||||||
|
window: 5
|
||||||
|
|
||||||
|
marginOrderSideEffect: repay
|
||||||
|
|
||||||
|
|
||||||
backtest:
|
backtest:
|
||||||
|
@ -34,7 +52,7 @@ backtest:
|
||||||
endTime: "2022-06-03"
|
endTime: "2022-06-03"
|
||||||
symbols:
|
symbols:
|
||||||
- GMTUSDT
|
- GMTUSDT
|
||||||
account:
|
accounts:
|
||||||
binance:
|
binance:
|
||||||
balances:
|
balances:
|
||||||
GMT: 3010.0
|
GMT: 3010.0
|
||||||
|
|
26
config/pivotshort_optimizer.yaml
Normal file
26
config/pivotshort_optimizer.yaml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# usage:
|
||||||
|
#
|
||||||
|
# go run ./cmd/bbgo optimize --config bollmaker_ethusdt.yaml --optimizer-config optimizer.yaml --debug
|
||||||
|
#
|
||||||
|
---
|
||||||
|
matrix:
|
||||||
|
- type: iterate
|
||||||
|
label: interval
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/interval'
|
||||||
|
values: [ "5m", "30m", "1h" ]
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/pivotLength'
|
||||||
|
label: pivotLength
|
||||||
|
min: 20.0
|
||||||
|
max: 200.0
|
||||||
|
step: 10.0
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/breakLow/stopEMARange'
|
||||||
|
label: pivotLength
|
||||||
|
min: 0%
|
||||||
|
max: 10%
|
||||||
|
step: 0.5%
|
||||||
|
|
||||||
|
|
|
@ -323,12 +323,13 @@ func (e *Exchange) SubscribeMarketData(extraIntervals ...types.Interval) (chan t
|
||||||
loadedIntervals[it] = struct{}{}
|
loadedIntervals[it] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collect subscriptions
|
||||||
for _, sub := range e.marketDataStream.Subscriptions {
|
for _, sub := range e.marketDataStream.Subscriptions {
|
||||||
loadedSymbols[sub.Symbol] = struct{}{}
|
loadedSymbols[sub.Symbol] = struct{}{}
|
||||||
|
|
||||||
switch sub.Channel {
|
switch sub.Channel {
|
||||||
case types.KLineChannel:
|
case types.KLineChannel:
|
||||||
loadedIntervals[types.Interval(sub.Options.Interval)] = struct{}{}
|
loadedIntervals[sub.Options.Interval] = struct{}{}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Since Environment is not yet been injected at this point, no hard error
|
// Since Environment is not yet been injected at this point, no hard error
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const CancelOrderWaitTime = 20 * time.Millisecond
|
const CancelOrderWaitTime = 10 * time.Millisecond
|
||||||
|
|
||||||
// ActiveOrderBook manages the local active order books.
|
// ActiveOrderBook manages the local active order books.
|
||||||
//go:generate callbackgen -type ActiveOrderBook
|
//go:generate callbackgen -type ActiveOrderBook
|
||||||
|
@ -69,6 +69,8 @@ func (b *ActiveOrderBook) waitAllClear(ctx context.Context, waitTime, timeout ti
|
||||||
|
|
||||||
// GracefulCancel cancels the active orders gracefully
|
// GracefulCancel cancels the active orders gracefully
|
||||||
func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange) error {
|
func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange) error {
|
||||||
|
waitTime := CancelOrderWaitTime
|
||||||
|
|
||||||
log.Debugf("[ActiveOrderBook] gracefully cancelling %s orders...", b.Symbol)
|
log.Debugf("[ActiveOrderBook] gracefully cancelling %s orders...", b.Symbol)
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
@ -86,9 +88,9 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange)
|
||||||
log.WithError(err).Errorf("[ActiveOrderBook] can not cancel %s orders", b.Symbol)
|
log.WithError(err).Errorf("[ActiveOrderBook] can not cancel %s orders", b.Symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("[ActiveOrderBook] waiting %s for %s orders to be cancelled...", CancelOrderWaitTime, b.Symbol)
|
log.Debugf("[ActiveOrderBook] waiting %s for %s orders to be cancelled...", waitTime, b.Symbol)
|
||||||
|
|
||||||
clear, err := b.waitAllClear(ctx, CancelOrderWaitTime, 5*time.Second)
|
clear, err := b.waitAllClear(ctx, waitTime, 5*time.Second)
|
||||||
if clear || err != nil {
|
if clear || err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package bbgo
|
package bbgo
|
||||||
|
|
||||||
import "github.com/sirupsen/logrus"
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
)
|
||||||
|
|
||||||
type Notifier interface {
|
type Notifier interface {
|
||||||
NotifyTo(channel string, obj interface{}, args ...interface{})
|
NotifyTo(channel string, obj interface{}, args ...interface{})
|
||||||
|
@ -69,7 +73,7 @@ func (m *Notifiability) NotifyTo(channel string, obj interface{}, args ...interf
|
||||||
func filterSimpleArgs(args []interface{}) (simpleArgs []interface{}) {
|
func filterSimpleArgs(args []interface{}) (simpleArgs []interface{}) {
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
switch arg.(type) {
|
switch arg.(type) {
|
||||||
case int, int64, int32, uint64, uint32, string, []byte, float64, float32:
|
case int, int64, int32, uint64, uint32, string, []byte, float64, float32, fixedpoint.Value:
|
||||||
simpleArgs = append(simpleArgs, arg)
|
simpleArgs = append(simpleArgs, arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,7 +128,7 @@ func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSessi
|
||||||
|
|
||||||
log.Infof("current %s position: %s", c.Symbol, c.position.String())
|
log.Infof("current %s position: %s", c.Symbol, c.position.String())
|
||||||
|
|
||||||
marketOrder := c.position.NewClosePositionOrder(c.ClosePosition)
|
marketOrder := c.position.NewMarketCloseOrder(c.ClosePosition)
|
||||||
if marketOrder != nil {
|
if marketOrder != nil {
|
||||||
log.Infof("submitting %s market order to stop: %+v", c.Symbol, marketOrder)
|
log.Infof("submitting %s market order to stop: %+v", c.Symbol, marketOrder)
|
||||||
|
|
||||||
|
|
|
@ -127,7 +127,6 @@ var BacktestCmd = &cobra.Command{
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if userConfig.Backtest == nil {
|
if userConfig.Backtest == nil {
|
||||||
return errors.New("backtest config is not defined")
|
return errors.New("backtest config is not defined")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"runtime/pprof"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -22,6 +23,8 @@ import (
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var cpuProfileFile *os.File
|
||||||
|
|
||||||
var userConfig *bbgo.Config
|
var userConfig *bbgo.Config
|
||||||
|
|
||||||
var RootCmd = &cobra.Command{
|
var RootCmd = &cobra.Command{
|
||||||
|
@ -32,7 +35,7 @@ var RootCmd = &cobra.Command{
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
|
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := cobraLoadDotenv(cmd, args) ; err != nil {
|
if err := cobraLoadDotenv(cmd, args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,8 +56,34 @@ var RootCmd = &cobra.Command{
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cpuProfile, err := cmd.Flags().GetString("cpu-profile")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cpuProfile != "" {
|
||||||
|
log.Infof("starting cpu profiler...")
|
||||||
|
|
||||||
|
cpuProfileFile, err = os.Create(cpuProfile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("could not create CPU profile: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pprof.StartCPUProfile(cpuProfileFile); err != nil {
|
||||||
|
log.Fatal("could not start CPU profile: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cobraLoadConfig(cmd, args)
|
return cobraLoadConfig(cmd, args)
|
||||||
},
|
},
|
||||||
|
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
pprof.StopCPUProfile()
|
||||||
|
if cpuProfileFile != nil {
|
||||||
|
return cpuProfileFile.Close() // error handling omitted for example
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
@ -139,6 +168,7 @@ func init() {
|
||||||
RootCmd.PersistentFlags().String("ftx-api-key", "", "ftx api key")
|
RootCmd.PersistentFlags().String("ftx-api-key", "", "ftx api key")
|
||||||
RootCmd.PersistentFlags().String("ftx-api-secret", "", "ftx api secret")
|
RootCmd.PersistentFlags().String("ftx-api-secret", "", "ftx api secret")
|
||||||
RootCmd.PersistentFlags().String("ftx-subaccount", "", "subaccount name. Specify it if the credential is for subaccount.")
|
RootCmd.PersistentFlags().String("ftx-subaccount", "", "subaccount name. Specify it if the credential is for subaccount.")
|
||||||
|
RootCmd.PersistentFlags().String("cpu-profile", "", "cpu profile")
|
||||||
|
|
||||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,6 @@ func init() {
|
||||||
RunCmd.Flags().Bool("enable-grpc", false, "enable grpc server")
|
RunCmd.Flags().Bool("enable-grpc", false, "enable grpc server")
|
||||||
RunCmd.Flags().String("grpc-bind", ":50051", "grpc server binding")
|
RunCmd.Flags().String("grpc-bind", ":50051", "grpc server binding")
|
||||||
|
|
||||||
RunCmd.Flags().String("cpu-profile", "", "cpu profile")
|
|
||||||
RunCmd.Flags().Bool("setup", false, "use setup mode")
|
RunCmd.Flags().Bool("setup", false, "use setup mode")
|
||||||
RootCmd.AddCommand(RunCmd)
|
RootCmd.AddCommand(RunCmd)
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,7 @@ func (v Value) String() string {
|
||||||
func (v Value) FormatString(prec int) string {
|
func (v Value) FormatString(prec int) string {
|
||||||
pow := math.Pow10(prec)
|
pow := math.Pow10(prec)
|
||||||
return strconv.FormatFloat(
|
return strconv.FormatFloat(
|
||||||
math.Trunc(float64(v)/DefaultPow * pow) / pow, 'f', prec, 64)
|
math.Trunc(float64(v)/DefaultPow*pow)/pow, 'f', prec, 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v Value) Percentage() string {
|
func (v Value) Percentage() string {
|
||||||
|
@ -114,7 +114,7 @@ func (v Value) FormatPercentage(prec int) string {
|
||||||
}
|
}
|
||||||
pow := math.Pow10(prec)
|
pow := math.Pow10(prec)
|
||||||
result := strconv.FormatFloat(
|
result := strconv.FormatFloat(
|
||||||
math.Trunc(float64(v)/DefaultPow * pow * 100.) / pow, 'f', prec, 64)
|
math.Trunc(float64(v)/DefaultPow*pow*100.)/pow, 'f', prec, 64)
|
||||||
return result + "%"
|
return result + "%"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,6 +222,10 @@ func (v *Value) UnmarshalYAML(unmarshal func(a interface{}) error) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v Value) MarshalYAML() (interface{}, error) {
|
||||||
|
return v.FormatString(DefaultPrecision), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (v Value) MarshalJSON() ([]byte, error) {
|
func (v Value) MarshalJSON() ([]byte, error) {
|
||||||
return []byte(v.FormatString(DefaultPrecision)), nil
|
return []byte(v.FormatString(DefaultPrecision)), nil
|
||||||
}
|
}
|
||||||
|
@ -326,7 +330,7 @@ func NewFromString(input string) (Value, error) {
|
||||||
// if is decimal, we don't need this
|
// if is decimal, we don't need this
|
||||||
hasScientificNotion := false
|
hasScientificNotion := false
|
||||||
scIndex := -1
|
scIndex := -1
|
||||||
for i, c := range(input) {
|
for i, c := range input {
|
||||||
if hasDecimal {
|
if hasDecimal {
|
||||||
if c <= '9' && c >= '0' {
|
if c <= '9' && c >= '0' {
|
||||||
decimalCount++
|
decimalCount++
|
||||||
|
@ -345,7 +349,7 @@ func NewFromString(input string) (Value, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasDecimal {
|
if hasDecimal {
|
||||||
after := input[dotIndex+1:len(input)]
|
after := input[dotIndex+1 : len(input)]
|
||||||
if decimalCount >= 8 {
|
if decimalCount >= 8 {
|
||||||
after = after[0:8] + "." + after[8:len(after)]
|
after = after[0:8] + "." + after[8:len(after)]
|
||||||
} else {
|
} else {
|
||||||
|
@ -368,7 +372,7 @@ func NewFromString(input string) (Value, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
v, err := strconv.ParseFloat(input[0:scIndex+1] + strconv.FormatInt(exp + 8, 10), 64)
|
v, err := strconv.ParseFloat(input[0:scIndex+1]+strconv.FormatInt(exp+8, 10), 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
@ -385,7 +389,6 @@ func NewFromString(input string) (Value, error) {
|
||||||
}
|
}
|
||||||
return Value(v), nil
|
return Value(v), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustNewFromString(input string) Value {
|
func MustNewFromString(input string) Value {
|
||||||
|
|
|
@ -188,21 +188,27 @@ func (s *BacktestService) QueryKLinesBackward(exchange types.ExchangeName, symbo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BacktestService) QueryKLinesCh(since, until time.Time, exchange types.Exchange, symbols []string, intervals []types.Interval) (chan types.KLine, chan error) {
|
func (s *BacktestService) QueryKLinesCh(since, until time.Time, exchange types.Exchange, symbols []string, intervals []types.Interval) (chan types.KLine, chan error) {
|
||||||
|
|
||||||
if len(symbols) == 0 {
|
if len(symbols) == 0 {
|
||||||
return returnError(errors.Errorf("symbols is empty when querying kline, plesae check your strategy setting. "))
|
return returnError(errors.Errorf("symbols is empty when querying kline, plesae check your strategy setting. "))
|
||||||
}
|
}
|
||||||
|
|
||||||
tableName := targetKlineTable(exchange.Name())
|
tableName := targetKlineTable(exchange.Name())
|
||||||
sql := "SELECT * FROM `binance_klines` WHERE `end_time` BETWEEN :since AND :until AND `symbol` IN (:symbols) AND `interval` IN (:intervals) and exchange = :exchange ORDER BY end_time ASC"
|
var query string
|
||||||
sql = strings.ReplaceAll(sql, "binance_klines", tableName)
|
|
||||||
|
|
||||||
sql, args, err := sqlx.Named(sql, map[string]interface{}{
|
if len(symbols) == 1 {
|
||||||
|
query = "SELECT * FROM `binance_klines` WHERE `end_time` BETWEEN :since AND :until AND `symbol` = :symbols AND `interval` IN (:intervals) ORDER BY end_time ASC"
|
||||||
|
} else {
|
||||||
|
query = "SELECT * FROM `binance_klines` WHERE `end_time` BETWEEN :since AND :until AND `symbol` IN (:symbols) AND `interval` IN (:intervals) ORDER BY end_time ASC"
|
||||||
|
}
|
||||||
|
|
||||||
|
query = strings.ReplaceAll(query, "binance_klines", tableName)
|
||||||
|
|
||||||
|
sql, args, err := sqlx.Named(query, map[string]interface{}{
|
||||||
"since": since,
|
"since": since,
|
||||||
"until": until,
|
"until": until,
|
||||||
|
"symbol": symbols[0],
|
||||||
"symbols": symbols,
|
"symbols": symbols,
|
||||||
"intervals": types.IntervalSlice(intervals),
|
"intervals": types.IntervalSlice(intervals),
|
||||||
"exchange": exchange.Name().String(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
sql, args, err = sqlx.In(sql, args...)
|
sql, args, err = sqlx.In(sql, args...)
|
||||||
|
|
|
@ -613,13 +613,13 @@ func (s *Strategy) PlaceSellOrder(ctx context.Context, price fixedpoint.Value) (
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClosePosition(context.Context) -> (closeOrder *types.Order, ok bool)
|
// ClosePosition(context.Context) -> (closeOrder *types.Order, ok bool)
|
||||||
// this will decorate the generated order from NewClosePositionOrder
|
// this will decorate the generated order from NewMarketCloseOrder
|
||||||
// add do necessary checks
|
// add do necessary checks
|
||||||
// if available quantity is zero, will return (nil, true)
|
// if available quantity is zero, will return (nil, true)
|
||||||
// if any of the checks failed, will return (nil, false)
|
// if any of the checks failed, will return (nil, false)
|
||||||
// otherwise, return the created close order and true
|
// otherwise, return the created close order and true
|
||||||
func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) {
|
func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) {
|
||||||
order := s.Position.NewClosePositionOrder(fixedpoint.One)
|
order := s.Position.NewMarketCloseOrder(fixedpoint.One)
|
||||||
// no position exists
|
// no position exists
|
||||||
if order == nil {
|
if order == nil {
|
||||||
// no base
|
// no base
|
||||||
|
|
|
@ -3,8 +3,10 @@ package pivotshort
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo"
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
@ -12,6 +14,39 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TradeStats struct {
|
||||||
|
WinningRatio fixedpoint.Value `json:"winningRatio" yaml:"winningRatio"`
|
||||||
|
NumOfLossTrade int `json:"numOfLossTrade" yaml:"numOfLossTrade"`
|
||||||
|
NumOfProfitTrade int `json:"numOfProfitTrade" yaml:"numOfProfitTrade"`
|
||||||
|
GrossProfit fixedpoint.Value `json:"grossProfit" yaml:"grossProfit"`
|
||||||
|
GrossLoss fixedpoint.Value `json:"grossLoss" yaml:"grossLoss"`
|
||||||
|
Profits []fixedpoint.Value `json:"profits" yaml:"profits"`
|
||||||
|
Losses []fixedpoint.Value `json:"losses" yaml:"losses"`
|
||||||
|
MostProfitableTrade fixedpoint.Value `json:"mostProfitableTrade" yaml:"mostProfitableTrade"`
|
||||||
|
MostLossTrade fixedpoint.Value `json:"mostLossTrade" yaml:"mostLossTrade"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TradeStats) Add(pnl fixedpoint.Value) {
|
||||||
|
if pnl.Sign() > 0 {
|
||||||
|
s.NumOfProfitTrade++
|
||||||
|
s.Profits = append(s.Profits, pnl)
|
||||||
|
s.GrossProfit = s.GrossProfit.Add(pnl)
|
||||||
|
s.MostProfitableTrade = fixedpoint.Max(s.MostProfitableTrade, pnl)
|
||||||
|
} else {
|
||||||
|
s.NumOfLossTrade++
|
||||||
|
s.Losses = append(s.Losses, pnl)
|
||||||
|
s.GrossLoss = s.GrossLoss.Add(pnl)
|
||||||
|
s.MostLossTrade = fixedpoint.Min(s.MostLossTrade, pnl)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WinningRatio = fixedpoint.NewFromFloat(float64(s.NumOfProfitTrade) / float64(s.NumOfLossTrade))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TradeStats) String() string {
|
||||||
|
out, _ := yaml.Marshal(s)
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
const ID = "pivotshort"
|
const ID = "pivotshort"
|
||||||
|
|
||||||
var log = logrus.WithField("strategy", ID)
|
var log = logrus.WithField("strategy", ID)
|
||||||
|
@ -24,8 +59,15 @@ type IntervalWindowSetting struct {
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
|
||||||
|
type BreakLow struct {
|
||||||
|
Ratio fixedpoint.Value `json:"ratio"`
|
||||||
|
Quantity fixedpoint.Value `json:"quantity"`
|
||||||
|
StopEMARange fixedpoint.Value `json:"stopEMARange"`
|
||||||
|
StopEMA *types.IntervalWindow `json:"stopEMA"`
|
||||||
|
}
|
||||||
|
|
||||||
type Entry struct {
|
type Entry struct {
|
||||||
Immediate bool `json:"immediate"`
|
|
||||||
CatBounceRatio fixedpoint.Value `json:"catBounceRatio"`
|
CatBounceRatio fixedpoint.Value `json:"catBounceRatio"`
|
||||||
NumLayers int `json:"numLayers"`
|
NumLayers int `json:"numLayers"`
|
||||||
TotalQuantity fixedpoint.Value `json:"totalQuantity"`
|
TotalQuantity fixedpoint.Value `json:"totalQuantity"`
|
||||||
|
@ -35,9 +77,11 @@ type Entry struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Exit struct {
|
type Exit struct {
|
||||||
TakeProfitPercentage fixedpoint.Value `json:"takeProfitPercentage"`
|
RoiStopLossPercentage fixedpoint.Value `json:"roiStopLossPercentage"`
|
||||||
StopLossPercentage fixedpoint.Value `json:"stopLossPercentage"`
|
RoiTakeProfitPercentage fixedpoint.Value `json:"roiTakeProfitPercentage"`
|
||||||
|
|
||||||
LowerShadowRatio fixedpoint.Value `json:"lowerShadowRatio"`
|
LowerShadowRatio fixedpoint.Value `json:"lowerShadowRatio"`
|
||||||
|
|
||||||
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
|
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,12 +98,13 @@ type Strategy struct {
|
||||||
// persistence fields
|
// persistence fields
|
||||||
Position *types.Position `json:"position,omitempty" persistence:"position"`
|
Position *types.Position `json:"position,omitempty" persistence:"position"`
|
||||||
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
|
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
|
||||||
|
TradeStats *TradeStats `persistence:"trade_stats"`
|
||||||
|
|
||||||
PivotLength int `json:"pivotLength"`
|
PivotLength int `json:"pivotLength"`
|
||||||
LastLow fixedpoint.Value
|
|
||||||
|
|
||||||
Entry Entry
|
BreakLow BreakLow `json:"breakLow"`
|
||||||
Exit Exit
|
Entry Entry `json:"entry"`
|
||||||
|
Exit Exit `json:"exit"`
|
||||||
|
|
||||||
activeMakerOrders *bbgo.ActiveOrderBook
|
activeMakerOrders *bbgo.ActiveOrderBook
|
||||||
orderStore *bbgo.OrderStore
|
orderStore *bbgo.OrderStore
|
||||||
|
@ -67,7 +112,9 @@ type Strategy struct {
|
||||||
|
|
||||||
session *bbgo.ExchangeSession
|
session *bbgo.ExchangeSession
|
||||||
|
|
||||||
|
lastLow fixedpoint.Value
|
||||||
pivot *indicator.Pivot
|
pivot *indicator.Pivot
|
||||||
|
ewma *indicator.EWMA
|
||||||
pivotLowPrices []fixedpoint.Value
|
pivotLowPrices []fixedpoint.Value
|
||||||
|
|
||||||
// StrategyController
|
// StrategyController
|
||||||
|
@ -95,8 +142,7 @@ func (s *Strategy) submitOrders(ctx context.Context, orderExecutor bbgo.OrderExe
|
||||||
s.tradeCollector.Process()
|
s.tradeCollector.Process()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) placeMarketSell(ctx context.Context, orderExecutor bbgo.OrderExecutor) {
|
func (s *Strategy) placeMarketSell(ctx context.Context, orderExecutor bbgo.OrderExecutor, quantity fixedpoint.Value) {
|
||||||
quantity := s.Entry.Quantity
|
|
||||||
if quantity.IsZero() {
|
if quantity.IsZero() {
|
||||||
if balance, ok := s.session.Account.Balance(s.Market.BaseCurrency); ok {
|
if balance, ok := s.session.Account.Balance(s.Market.BaseCurrency); ok {
|
||||||
s.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String())
|
s.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String())
|
||||||
|
@ -109,29 +155,19 @@ func (s *Strategy) placeMarketSell(ctx context.Context, orderExecutor bbgo.Order
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sideEffect := s.Entry.MarginSideEffect
|
|
||||||
if len(sideEffect) == 0 {
|
|
||||||
sideEffect = types.SideEffectTypeMarginBuy
|
|
||||||
}
|
|
||||||
|
|
||||||
submitOrder := types.SubmitOrder{
|
submitOrder := types.SubmitOrder{
|
||||||
Symbol: s.Symbol,
|
Symbol: s.Symbol,
|
||||||
Side: types.SideTypeSell,
|
Side: types.SideTypeSell,
|
||||||
Type: types.OrderTypeMarket,
|
Type: types.OrderTypeMarket,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
MarginSideEffect: sideEffect,
|
MarginSideEffect: types.SideEffectTypeMarginBuy,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.submitOrders(ctx, orderExecutor, submitOrder)
|
s.submitOrders(ctx, orderExecutor, submitOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if position can be close or not
|
|
||||||
func canClosePosition(position *types.Position, price fixedpoint.Value) bool {
|
|
||||||
return position.IsShort() && !(position.IsClosed() || position.IsDust(price))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
|
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
|
||||||
submitOrder := s.Position.NewClosePositionOrder(percentage) // types.SubmitOrder{
|
submitOrder := s.Position.NewMarketCloseOrder(percentage) // types.SubmitOrder{
|
||||||
if submitOrder == nil {
|
if submitOrder == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -140,7 +176,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
|
||||||
submitOrder.MarginSideEffect = s.Exit.MarginSideEffect
|
submitOrder.MarginSideEffect = s.Exit.MarginSideEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Notify("Submitting %s buy order to close position by %v", s.Symbol, percentage)
|
s.Notify("Closing %s position by %f", s.Symbol, percentage.Float64())
|
||||||
|
|
||||||
createdOrders, err := s.session.Exchange.SubmitOrders(ctx, *submitOrder)
|
createdOrders, err := s.session.Exchange.SubmitOrders(ctx, *submitOrder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -152,6 +188,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
|
||||||
s.tradeCollector.Process()
|
s.tradeCollector.Process()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) InstanceID() string {
|
func (s *Strategy) InstanceID() string {
|
||||||
return fmt.Sprintf("%s:%s", ID, s.Symbol)
|
return fmt.Sprintf("%s:%s", ID, s.Symbol)
|
||||||
}
|
}
|
||||||
|
@ -174,6 +211,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
s.ProfitStats = types.NewProfitStats(s.Market)
|
s.ProfitStats = types.NewProfitStats(s.Market)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.TradeStats == nil {
|
||||||
|
s.TradeStats = &TradeStats{}
|
||||||
|
}
|
||||||
|
|
||||||
instanceID := s.InstanceID()
|
instanceID := s.InstanceID()
|
||||||
|
|
||||||
// Always update the position fields
|
// Always update the position fields
|
||||||
|
@ -189,6 +230,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
s.Environment.RecordPosition(s.Position, trade, nil)
|
s.Environment.RecordPosition(s.Position, trade, nil)
|
||||||
} else {
|
} else {
|
||||||
log.Infof("%s generated profit: %v", s.Symbol, profit)
|
log.Infof("%s generated profit: %v", s.Symbol, profit)
|
||||||
|
|
||||||
p := s.Position.NewProfit(trade, profit, netProfit)
|
p := s.Position.NewProfit(trade, profit, netProfit)
|
||||||
p.Strategy = ID
|
p.Strategy = ID
|
||||||
p.StrategyInstanceID = instanceID
|
p.StrategyInstanceID = instanceID
|
||||||
|
@ -197,6 +239,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
s.ProfitStats.AddProfit(p)
|
s.ProfitStats.AddProfit(p)
|
||||||
s.Notify(&s.ProfitStats)
|
s.Notify(&s.ProfitStats)
|
||||||
|
|
||||||
|
s.TradeStats.Add(profit)
|
||||||
|
|
||||||
s.Environment.RecordPosition(s.Position, trade, &p)
|
s.Environment.RecordPosition(s.Position, trade, &p)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -212,7 +256,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
s.pivot = &indicator.Pivot{IntervalWindow: iw}
|
s.pivot = &indicator.Pivot{IntervalWindow: iw}
|
||||||
s.pivot.Bind(store)
|
s.pivot.Bind(store)
|
||||||
|
|
||||||
s.LastLow = fixedpoint.Zero
|
standardIndicator, _ := session.StandardIndicatorSet(s.Symbol)
|
||||||
|
if s.BreakLow.StopEMA != nil {
|
||||||
|
s.ewma = standardIndicator.EWMA(*s.BreakLow.StopEMA)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastLow = fixedpoint.Zero
|
||||||
|
|
||||||
session.UserDataStream.OnStart(func() {
|
session.UserDataStream.OnStart(func() {
|
||||||
/*
|
/*
|
||||||
|
@ -231,44 +280,87 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: handle stop loss here, faster than closed kline
|
isPositionOpened := !s.Position.IsClosed() && !s.Position.IsDust(kline.Close)
|
||||||
if canClosePosition(s.Position, kline.Close) {
|
|
||||||
|
if isPositionOpened && s.Position.IsShort() {
|
||||||
|
// calculate return rate
|
||||||
|
// TODO: apply quantity to this formula
|
||||||
|
roi := s.Position.AverageCost.Sub(kline.Close).Div(s.Position.AverageCost)
|
||||||
|
if roi.Compare(s.Exit.RoiStopLossPercentage.Neg()) < 0 {
|
||||||
|
// SL
|
||||||
|
s.Notify("%s ROI StopLoss triggered at price %f, ROI = %s", s.Symbol, kline.Close.Float64(), roi.Percentage())
|
||||||
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
|
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
|
||||||
log.WithError(err).Errorf("graceful cancel order error")
|
log.WithError(err).Errorf("graceful cancel order error")
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate return rate
|
if err := s.ClosePosition(ctx, fixedpoint.One); err != nil {
|
||||||
R := kline.Close.Sub(s.Position.AverageCost).Div(s.Position.AverageCost)
|
log.WithError(err).Errorf("close position error")
|
||||||
if R.Compare(s.Exit.StopLossPercentage) > 0 {
|
}
|
||||||
// SL
|
|
||||||
s.Notify("%s SL triggered at price %f", s.Symbol, kline.Close.Float64())
|
|
||||||
s.ClosePosition(ctx, fixedpoint.One)
|
|
||||||
return
|
return
|
||||||
} else if R.Compare(s.Exit.TakeProfitPercentage.Neg()) < 0 && kline.GetLowerShadowRatio().Compare(s.Exit.LowerShadowRatio) > 0 {
|
} else if roi.Compare(s.Exit.RoiTakeProfitPercentage) > 0 { // disable this condition temporarily
|
||||||
// TP
|
s.Notify("%s TakeProfit triggered at price %f, ROI take profit percentage by %s", s.Symbol, kline.Close.Float64(), roi.Percentage(), kline)
|
||||||
s.Notify("%s TP triggered at price %f", s.Symbol, kline.Close.Float64())
|
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
|
||||||
s.ClosePosition(ctx, fixedpoint.One)
|
log.WithError(err).Errorf("graceful cancel order error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ClosePosition(ctx, fixedpoint.One); err != nil {
|
||||||
|
log.WithError(err).Errorf("close position error")
|
||||||
|
}
|
||||||
|
} else if !s.Exit.LowerShadowRatio.IsZero() && kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Exit.LowerShadowRatio) > 0 {
|
||||||
|
s.Notify("%s TakeProfit triggered at price %f: shadow ratio %f", s.Symbol, kline.Close.Float64(), kline.GetLowerShadowRatio().Float64(), kline)
|
||||||
|
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
|
||||||
|
log.WithError(err).Errorf("graceful cancel order error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ClosePosition(ctx, fixedpoint.One); err != nil {
|
||||||
|
log.WithError(err).Errorf("close position error")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s.pivotLowPrices) > 0 {
|
if len(s.pivotLowPrices) == 0 {
|
||||||
lastLow := s.pivotLowPrices[len(s.pivotLowPrices)-1]
|
return
|
||||||
if kline.Close.Compare(lastLow) < 0 {
|
}
|
||||||
s.Notify("%s price %f breaks the previous low %f, submitting market sell to open a short position", s.Symbol, kline.Close.Float64(), lastLow.Float64())
|
|
||||||
|
previousLow := s.pivotLowPrices[len(s.pivotLowPrices)-1]
|
||||||
|
|
||||||
|
// truncate the pivot low prices
|
||||||
|
if len(s.pivotLowPrices) > 10 {
|
||||||
|
s.pivotLowPrices = s.pivotLowPrices[len(s.pivotLowPrices)-10:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ewma != nil && !s.BreakLow.StopEMARange.IsZero() {
|
||||||
|
ema := fixedpoint.NewFromFloat(s.ewma.Last())
|
||||||
|
if ema.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.BreakLow.StopEMARange))
|
||||||
|
if kline.Close.Compare(emaStopShortPrice) < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ratio := fixedpoint.One.Sub(s.BreakLow.Ratio)
|
||||||
|
breakPrice := previousLow.Mul(ratio)
|
||||||
|
if kline.Close.Compare(breakPrice) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !s.Position.IsClosed() && !s.Position.IsDust(kline.Close) {
|
if !s.Position.IsClosed() && !s.Position.IsDust(kline.Close) {
|
||||||
s.Notify("skip opening %s position, which is not closed", s.Symbol, s.Position)
|
// s.Notify("skip opening %s position, which is not closed", s.Symbol, s.Position)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.Notify("%s price %f breaks the previous low %f with ratio %f, submitting market sell to open a short position", s.Symbol, kline.Close.Float64(), previousLow.Float64(), s.BreakLow.Ratio.Float64())
|
||||||
|
|
||||||
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
|
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
|
||||||
log.WithError(err).Errorf("graceful cancel order error")
|
log.WithError(err).Errorf("graceful cancel order error")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.placeMarketSell(ctx, orderExecutor)
|
s.placeMarketSell(ctx, orderExecutor, s.BreakLow.Quantity)
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
||||||
|
@ -277,10 +369,18 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.pivot.LastLow() > 0.0 {
|
if s.pivot.LastLow() > 0.0 {
|
||||||
log.Infof("pivot low signal detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time())
|
log.Infof("pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time())
|
||||||
s.LastLow = fixedpoint.NewFromFloat(s.pivot.LastLow())
|
lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow())
|
||||||
s.pivotLowPrices = append(s.pivotLowPrices, s.LastLow)
|
if lastLow.Compare(s.lastLow) != 0 {
|
||||||
|
s.lastLow = lastLow
|
||||||
|
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
|
log.Info(s.TradeStats.String())
|
||||||
|
wg.Done()
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -337,13 +437,9 @@ func (s *Strategy) placeOrder(ctx context.Context, lastLow fixedpoint.Value, lim
|
||||||
Quantity: qty,
|
Quantity: qty,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !lastLow.IsZero() && s.Entry.Immediate && lastLow.Compare(currentPrice) <= 0 {
|
if !lastLow.IsZero() && lastLow.Compare(currentPrice) <= 0 {
|
||||||
submitOrder.Type = types.OrderTypeMarket
|
submitOrder.Type = types.OrderTypeMarket
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.session.Margin {
|
|
||||||
submitOrder.MarginSideEffect = s.Entry.MarginSideEffect
|
|
||||||
}
|
|
||||||
|
|
||||||
s.submitOrders(ctx, orderExecutor, submitOrder)
|
s.submitOrders(ctx, orderExecutor, submitOrder)
|
||||||
}
|
}
|
||||||
|
|
|
@ -807,8 +807,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
|
||||||
defer tradeScanTicker.Stop()
|
defer tradeScanTicker.Stop()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := s.activeMakerOrders.GracefulCancel(context.Background(),
|
if err := s.activeMakerOrders.GracefulCancel(context.Background(), s.makerSession.Exchange); err != nil {
|
||||||
s.makerSession.Exchange); err != nil {
|
|
||||||
log.WithError(err).Errorf("can not cancel %s orders", s.Symbol)
|
log.WithError(err).Errorf("can not cancel %s orders", s.Symbol)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -120,7 +120,7 @@ func (p *Position) NewProfit(trade Trade, profit, netProfit fixedpoint.Value) Pr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Position) NewClosePositionOrder(percentage fixedpoint.Value) *SubmitOrder {
|
func (p *Position) NewMarketCloseOrder(percentage fixedpoint.Value) *SubmitOrder {
|
||||||
base := p.GetBase()
|
base := p.GetBase()
|
||||||
|
|
||||||
quantity := base.Abs()
|
quantity := base.Abs()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user