mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 16:25:16 +00:00
Merge branch 'main' into improve/supertrend-strategy
This commit is contained in:
commit
f8777752a0
98
README.md
98
README.md
|
@ -30,7 +30,8 @@ You can use BBGO's trading unit and back-test unit to implement your own strateg
|
||||||
|
|
||||||
### Trading Unit Developers 🧑💻
|
### Trading Unit Developers 🧑💻
|
||||||
|
|
||||||
You can use BBGO's underlying common exchange API, currently it supports 4+ major exchanges, so you don't have to repeat the implementation.
|
You can use BBGO's underlying common exchange API, currently it supports 4+ major exchanges, so you don't have to repeat
|
||||||
|
the implementation.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -44,7 +45,8 @@ You can use BBGO's underlying common exchange API, currently it supports 4+ majo
|
||||||
- Built-in parameter optimization tool.
|
- Built-in parameter optimization tool.
|
||||||
- Built-in Grid strategy and many other built-in strategies.
|
- Built-in Grid strategy and many other built-in strategies.
|
||||||
- Multi-exchange session support: you can connect to more than 2 exchanges with different accounts or subaccounts.
|
- Multi-exchange session support: you can connect to more than 2 exchanges with different accounts or subaccounts.
|
||||||
- Indicators with interface similar to `pandas.Series`([series](https://github.com/c9s/bbgo/blob/main/doc/development/series.md))([usage](https://github.com/c9s/bbgo/blob/main/doc/development/indicator.md)):
|
- Indicators with interface similar
|
||||||
|
to `pandas.Series`([series](https://github.com/c9s/bbgo/blob/main/doc/development/series.md))([usage](https://github.com/c9s/bbgo/blob/main/doc/development/indicator.md)):
|
||||||
- [Accumulation/Distribution Indicator](./pkg/indicator/ad.go)
|
- [Accumulation/Distribution Indicator](./pkg/indicator/ad.go)
|
||||||
- [Arnaud Legoux Moving Average](./pkg/indicator/alma.go)
|
- [Arnaud Legoux Moving Average](./pkg/indicator/alma.go)
|
||||||
- [Average True Range](./pkg/indicator/atr.go)
|
- [Average True Range](./pkg/indicator/atr.go)
|
||||||
|
@ -115,7 +117,8 @@ Get your exchange API key and secret after you register the accounts (you can ch
|
||||||
- OKEx: <https://www.okex.com/join/2412712?src=from:ios-share>
|
- OKEx: <https://www.okex.com/join/2412712?src=from:ios-share>
|
||||||
- Kucoin: <https://www.kucoin.com/ucenter/signup?rcode=r3KX2D4>
|
- Kucoin: <https://www.kucoin.com/ucenter/signup?rcode=r3KX2D4>
|
||||||
|
|
||||||
This project is maintained and supported by a small group of team. If you would like to support this project, please register on the exchanges using the provided links with referral codes above.
|
This project is maintained and supported by a small group of team. If you would like to support this project, please
|
||||||
|
register on the exchanges using the provided links with referral codes above.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -145,8 +148,8 @@ bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/download.
|
||||||
|
|
||||||
Or refer to the [Release Page](https://github.com/c9s/bbgo/releases) and download manually.
|
Or refer to the [Release Page](https://github.com/c9s/bbgo/releases) and download manually.
|
||||||
|
|
||||||
Since v2, we've added new float point implementation from dnum to support decimals with higher precision.
|
Since v2, we've added new float point implementation from dnum to support decimals with higher precision. To download &
|
||||||
To download & setup, please refer to [Dnum Installation](doc/topics/dnum-binary.md)
|
setup, please refer to [Dnum Installation](doc/topics/dnum-binary.md)
|
||||||
|
|
||||||
### One-click Linode StackScript
|
### One-click Linode StackScript
|
||||||
|
|
||||||
|
@ -241,8 +244,8 @@ bbgo pnl --exchange binance --asset BTC --since "2019-01-01"
|
||||||
|
|
||||||
### Testnet (Paper Trading)
|
### Testnet (Paper Trading)
|
||||||
|
|
||||||
Currently only supports binance testnet.
|
Currently only supports binance testnet. To run bbgo in testnet, apply new API keys
|
||||||
To run bbgo in testnet, apply new API keys from [Binance Test Network](https://testnet.binance.vision), and set the following env before you start bbgo:
|
from [Binance Test Network](https://testnet.binance.vision), and set the following env before you start bbgo:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export PAPER_TRADE=1
|
export PAPER_TRADE=1
|
||||||
|
@ -344,7 +347,8 @@ Check out the strategy directory [strategy](pkg/strategy) for all built-in strat
|
||||||
indicator [bollgrid](pkg/strategy/bollgrid)
|
indicator [bollgrid](pkg/strategy/bollgrid)
|
||||||
- `grid` strategy implements the fixed price band grid strategy [grid](pkg/strategy/grid). See
|
- `grid` strategy implements the fixed price band grid strategy [grid](pkg/strategy/grid). See
|
||||||
[document](./doc/strategy/grid.md).
|
[document](./doc/strategy/grid.md).
|
||||||
- `supertrend` strategy uses Supertrend indicator as trend, and DEMA indicator as noise filter [supertrend](pkg/strategy/supertrend). See
|
- `supertrend` strategy uses Supertrend indicator as trend, and DEMA indicator as noise
|
||||||
|
filter [supertrend](pkg/strategy/supertrend). See
|
||||||
[document](./doc/strategy/supertrend.md).
|
[document](./doc/strategy/supertrend.md).
|
||||||
- `support` strategy uses K-lines with high volume as support [support](pkg/strategy/support). See
|
- `support` strategy uses K-lines with high volume as support [support](pkg/strategy/support). See
|
||||||
[document](./doc/strategy/support.md).
|
[document](./doc/strategy/support.md).
|
||||||
|
@ -365,78 +369,9 @@ bbgo run --config config/buyandhold.yaml
|
||||||
|
|
||||||
See [Back-testing](./doc/topics/back-testing.md)
|
See [Back-testing](./doc/topics/back-testing.md)
|
||||||
|
|
||||||
## Adding New Built-in Strategy
|
## Adding Strategy
|
||||||
|
|
||||||
Fork and clone this repository, Create a directory under `pkg/strategy/newstrategy`, write your strategy
|
See [Developing Strategy](./doc/topics/developing-strategy.md)
|
||||||
at `pkg/strategy/newstrategy/strategy.go`.
|
|
||||||
|
|
||||||
Define a strategy struct:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package newstrategy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Strategy struct {
|
|
||||||
Symbol string `json:"symbol"`
|
|
||||||
Param1 int `json:"param1"`
|
|
||||||
Param2 int `json:"param2"`
|
|
||||||
Param3 fixedpoint.Value `json:"param3"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Register your strategy:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package newstrategy
|
|
||||||
|
|
||||||
const ID = "newstrategy"
|
|
||||||
|
|
||||||
const stateKey = "state-v1"
|
|
||||||
|
|
||||||
var log = logrus.WithField("strategy", ID)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
bbgo.RegisterStrategy(ID, &Strategy{})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Implement the strategy methods:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package newstrategy
|
|
||||||
|
|
||||||
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
|
||||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "2m"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
|
||||||
// ....
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Edit `pkg/cmd/builtin.go`, and import the package, like this:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
// import built-in strategies
|
|
||||||
import (
|
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/bollgrid"
|
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/buyandhold"
|
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/flashcrash"
|
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/grid"
|
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
|
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/support"
|
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/swing"
|
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/trailingstop"
|
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/xmaker"
|
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/xpuremaker"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Write your own private strategy
|
## Write your own private strategy
|
||||||
|
|
||||||
|
@ -635,8 +570,9 @@ What's Position?
|
||||||
|
|
||||||
## Looking For A New Strategy?
|
## Looking For A New Strategy?
|
||||||
|
|
||||||
You can write an article about BBGO in any topic, in 750-1500 words for exchange, and I can implement the strategy for you (depends on the complexity and efforts).
|
You can write an article about BBGO in any topic, in 750-1500 words for exchange, and I can implement the strategy for
|
||||||
If you're interested in, DM me in telegram <https://t.me/c123456789s> or twitter <https://twitter.com/c9s>, we can discuss.
|
you (depends on the complexity and efforts). If you're interested in, DM me in telegram <https://t.me/c123456789s> or
|
||||||
|
twitter <https://twitter.com/c9s>, we can discuss.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
@ -39,26 +39,30 @@ exchangeStrategies:
|
||||||
|
|
||||||
# stopEMARange is the price range we allow short.
|
# stopEMARange is the price range we allow short.
|
||||||
# Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange])
|
# Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange])
|
||||||
stopEMARange: 0%
|
# Higher the stopEMARange than higher the chance to open a short
|
||||||
|
stopEMARange: 2%
|
||||||
stopEMA:
|
stopEMA:
|
||||||
interval: 1h
|
interval: 1h
|
||||||
window: 99
|
window: 99
|
||||||
|
|
||||||
bounceShort:
|
resistanceShort:
|
||||||
enabled: false
|
enabled: true
|
||||||
interval: 1h
|
interval: 5m
|
||||||
window: 10
|
window: 80
|
||||||
|
|
||||||
quantity: 10.0
|
quantity: 10.0
|
||||||
minDistance: 3%
|
|
||||||
# stopLossPercentage: 1%
|
# minDistance is used to ignore the place that is too near to the current price
|
||||||
|
minDistance: 5%
|
||||||
|
groupDistance: 1%
|
||||||
|
|
||||||
# ratio is the ratio of the resistance price,
|
# ratio is the ratio of the resistance price,
|
||||||
# higher the ratio, lower the price
|
# higher the ratio, higher the sell price
|
||||||
# first_layer_price = resistance_price * (1 - ratio)
|
# first_layer_price = resistance_price * (1 + ratio)
|
||||||
# second_layer_price = (resistance_price * (1 - ratio)) * (2 * layerSpread)
|
# second_layer_price = (resistance_price * (1 + ratio)) * (2 * layerSpread)
|
||||||
ratio: 0%
|
ratio: 1.5%
|
||||||
numOfLayers: 1
|
numOfLayers: 3
|
||||||
layerSpread: 0.1%
|
layerSpread: 0.4%
|
||||||
|
|
||||||
exits:
|
exits:
|
||||||
# (0) roiStopLoss is the stop loss percentage of the position ROI (currently the price change)
|
# (0) roiStopLoss is the stop loss percentage of the position ROI (currently the price change)
|
||||||
|
@ -86,17 +90,20 @@ exchangeStrategies:
|
||||||
# you can grab a simple stats by the following SQL:
|
# 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;
|
# 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;
|
||||||
- lowerShadowTakeProfit:
|
- lowerShadowTakeProfit:
|
||||||
|
interval: 30m
|
||||||
|
window: 99
|
||||||
ratio: 3%
|
ratio: 3%
|
||||||
|
|
||||||
# (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold
|
# (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold
|
||||||
- cumulatedVolumeTakeProfit:
|
- cumulatedVolumeTakeProfit:
|
||||||
minQuoteVolume: 100_000_000
|
interval: 5m
|
||||||
window: 2
|
window: 2
|
||||||
|
minQuoteVolume: 200_000_000
|
||||||
|
|
||||||
backtest:
|
backtest:
|
||||||
sessions:
|
sessions:
|
||||||
- binance
|
- binance
|
||||||
startTime: "2022-04-01"
|
startTime: "2022-01-01"
|
||||||
endTime: "2022-06-18"
|
endTime: "2022-06-18"
|
||||||
symbols:
|
symbols:
|
||||||
- ETHUSDT
|
- ETHUSDT
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
* [Support](strategy/support.md) - Support strategy that buys on high volume support
|
* [Support](strategy/support.md) - Support strategy that buys on high volume support
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
* [Developing Strategy](topics/developing-strategy.md) - developing strategy
|
||||||
* [Adding New Exchange](development/adding-new-exchange.md) - Check lists for adding new exchanges
|
* [Adding New Exchange](development/adding-new-exchange.md) - Check lists for adding new exchanges
|
||||||
* [KuCoin Command-line Test Tool](development/kucoin-cli.md) - Kucoin command-line tools
|
* [KuCoin Command-line Test Tool](development/kucoin-cli.md) - Kucoin command-line tools
|
||||||
* [SQL Migration](development/migration.md) - Adding new SQL migration scripts
|
* [SQL Migration](development/migration.md) - Adding new SQL migration scripts
|
||||||
|
|
|
@ -10,7 +10,91 @@ For built-in strategies, they are placed in `pkg/strategy` of the BBGO source re
|
||||||
For external strategies, you can create a private repository as an isolated go package and place your strategy inside
|
For external strategies, you can create a private repository as an isolated go package and place your strategy inside
|
||||||
it.
|
it.
|
||||||
|
|
||||||
In general, strategies are Go struct, placed in Go package.
|
In general, strategies are Go struct, defined in the Go package.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
To add your first strategy, the fastest way is to add it as a built-in strategy.
|
||||||
|
|
||||||
|
Simply edit `pkg/cmd/builtin.go` and import your strategy package there.
|
||||||
|
|
||||||
|
When BBGO starts, the strategy will be imported as a package, and register its struct to the engine.
|
||||||
|
|
||||||
|
You can also create a new file called `pkg/cmd/builtin_short.go` and import your strategy package.
|
||||||
|
|
||||||
|
```
|
||||||
|
import (
|
||||||
|
_ "github.com/c9s/bbgo/pkg/strategy/short"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a directory for your new strategy in the BBGO source code repository:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mkdir -p pkg/strategy/short
|
||||||
|
```
|
||||||
|
|
||||||
|
Open a new file at `pkg/strategy/short/strategy.go` and paste the simplest strategy code:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package short
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ID = "short"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Register our struct type to BBGO
|
||||||
|
// Note that you don't need to field the fields.
|
||||||
|
// BBGO uses reflect to parse your type information.
|
||||||
|
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Strategy struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Interval types.Interval `json:"interval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||||
|
session.MarketDataStream.OnKLineClosed(func(k types.KLine) {
|
||||||
|
fmt.Println(k)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the most simple strategy with only ~30 lines code, it subscribes to the kline channel with the given symbol from
|
||||||
|
the config, And when the kline is closed, it prints the kline to the console.
|
||||||
|
|
||||||
|
Note that, when Run() is executed, the user data stream is not connected to the exchange yet, but the history market
|
||||||
|
data is already loaded, so if you need to submit an order on start, be sure to write your order submit code inside the
|
||||||
|
event closures like `OnKLineClosed` or `OnStart`.
|
||||||
|
|
||||||
|
Now you can prepare your config file, create a file called `bbgo.yaml` with the following content:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
short:
|
||||||
|
symbol: ETHUSDT
|
||||||
|
interval: 1m
|
||||||
|
```
|
||||||
|
|
||||||
|
And then, you should be able to run this strategy by running the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go run ./cmd/bbgo run
|
||||||
|
```
|
||||||
|
|
||||||
## The Strategy Struct
|
## The Strategy Struct
|
||||||
|
|
||||||
|
@ -28,7 +112,7 @@ externalStrategies:
|
||||||
|
|
||||||
You can write the following struct to load the symbol setting:
|
You can write the following struct to load the symbol setting:
|
||||||
|
|
||||||
```
|
```go
|
||||||
package short
|
package short
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
|
@ -39,7 +123,7 @@ type Strategy struct {
|
||||||
|
|
||||||
To use the Symbol setting, you can get the value from the Run method of the strategy:
|
To use the Symbol setting, you can get the value from the Run method of the strategy:
|
||||||
|
|
||||||
```
|
```go
|
||||||
func (s *Strategy) Run(ctx context.Context, session *bbgo.ExchangeSession) error {
|
func (s *Strategy) Run(ctx context.Context, session *bbgo.ExchangeSession) error {
|
||||||
// you need to import the "log" package
|
// you need to import the "log" package
|
||||||
log.Println("%s", s.Symbol)
|
log.Println("%s", s.Symbol)
|
||||||
|
@ -47,18 +131,18 @@ func (s *Strategy) Run(ctx context.Context, session *bbgo.ExchangeSession) error
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Now you have the Go struct and the Go package, but BBGO does not know your strategy,
|
Now you have the Go struct and the Go package, but BBGO does not know your strategy, so you need to register your
|
||||||
so you need to register your strategy.
|
strategy.
|
||||||
|
|
||||||
Define an ID const in your package:
|
Define an ID const in your package:
|
||||||
|
|
||||||
```
|
```go
|
||||||
const ID = "short"
|
const ID = "short"
|
||||||
```
|
```
|
||||||
|
|
||||||
Then call bbgo.RegisterStrategy with the ID you just defined and a struct reference:
|
Then call bbgo.RegisterStrategy with the ID you just defined and a struct reference:
|
||||||
|
|
||||||
```
|
```go
|
||||||
func init() {
|
func init() {
|
||||||
bbgo.RegisterStrategy(ID, &Strategy{})
|
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||||
}
|
}
|
||||||
|
@ -66,17 +150,354 @@ func init() {
|
||||||
|
|
||||||
Note that you don't need to fill the fields in the struct, BBGO just need to know the type of struct.
|
Note that you don't need to fill the fields in the struct, BBGO just need to know the type of struct.
|
||||||
|
|
||||||
(BBGO use reflect to parse the fields from the given struct and allocate a new struct object from the given struct type internally)
|
(BBGO use reflect to parse the fields from the given struct and allocate a new struct object from the given struct type
|
||||||
|
internally)
|
||||||
|
|
||||||
|
## Exchange Session
|
||||||
|
|
||||||
## Built-in Strategy
|
The `*bbgo.ExchangeSession` represents a connectivity to a crypto exchange, it's also a hub that connects to everything
|
||||||
|
you need, for example, standard indicators, account information, balance information, market data stream, user data
|
||||||
|
stream, exchange APIs, and so on.
|
||||||
|
|
||||||
|
By default, BBGO checks the environment variables that you defined to detect which exchange session to be created.
|
||||||
|
|
||||||
|
For example, environment variables like `BINANCE_API_KEY`, `BINANCE_API_SECRET` will be transformed into an exchange
|
||||||
|
session that connects to Binance.
|
||||||
|
|
||||||
|
You can not only connect to multiple different crypt exchanges, but also create multiple sessions to the same crypto
|
||||||
|
exchange with few different options.
|
||||||
|
|
||||||
|
To do that, add the following section to your `bbgo.yaml` config file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
binance_cross_margin:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
margin: true
|
||||||
|
binance_margin_ethusdt:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
margin: true
|
||||||
|
isolatedMargin: true
|
||||||
|
isolatedMarginSymbol: ETHUSDT
|
||||||
|
okex1:
|
||||||
|
exchange: okex
|
||||||
|
envVarPrefix: okex
|
||||||
|
okex2:
|
||||||
|
exchange: okex
|
||||||
|
envVarPrefix: okex
|
||||||
|
```
|
||||||
|
|
||||||
|
You can specify which exchange session you want to mount for each strategy in the config file, it's quiet simple:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance_margin_ethusdt
|
||||||
|
short:
|
||||||
|
symbol: ETHUSDT
|
||||||
|
|
||||||
|
- on: binance_margin
|
||||||
|
foo:
|
||||||
|
symbol: ETHUSDT
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
bar:
|
||||||
|
symbol: ETHUSDT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Market Data Stream and User Data Stream
|
||||||
|
|
||||||
|
When BBGO connects to the exchange, it allocates two stream objects for different purposes.
|
||||||
|
|
||||||
|
They are:
|
||||||
|
|
||||||
|
- MarketDataStream receives market data from the exchange, for example, KLine data (candlestick, or bars), market public
|
||||||
|
trades.
|
||||||
|
- UserDataStream receives your personal trading data, for example, orders, executed trades, balance updates and other
|
||||||
|
private information.
|
||||||
|
|
||||||
|
To add your market data subscription to the `MarketDataStream`, you can register your subscription in the `Subscribe` of
|
||||||
|
the strategy code, for example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Since the back-test engine is a kline-based engine, to subscribe market trades, you need to check if you're in the
|
||||||
|
back-test environment:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||||
|
if !bbgo.IsBackTesting {
|
||||||
|
session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To receive the market data from the market data stream, you need to register the event callback:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||||
|
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
||||||
|
// handle closed kline event here
|
||||||
|
})
|
||||||
|
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
|
||||||
|
// handle market trade event here
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, we register our event callback to the market data stream of the current exchange session, The
|
||||||
|
market data stream object here is a session-wide market data stream, so it's shared with other strategies that are also
|
||||||
|
using the same exchange session, so you might receive kline with different symbol or interval.
|
||||||
|
|
||||||
|
It's better to add a condition to filter the kline events:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||||
|
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
||||||
|
if kline.Symbol != s.Symbol || kline.Interval != s.Interval {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// handle your kline here
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use the KLineWith method to wrap your kline closure with the filter condition:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||||
|
session.MarketDataStream.OnKLineClosed(types.KLineWith("BTCUSDT", types.Interval1m, func(kline types.KLine) {
|
||||||
|
// handle your kline here
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that, when the Run() method is executed, the user data stream and market data stream are not connected yet.
|
||||||
|
|
||||||
|
## Submitting Orders
|
||||||
|
|
||||||
|
To place an order, you can call `SubmitOrders` exchange API:
|
||||||
|
|
||||||
|
```go
|
||||||
|
createdOrders, err := session.Exchange.SubmitOrders(ctx, types.SubmitOrder{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Price: fixedpoint.NewFromFloat(18000.0),
|
||||||
|
Quantity: fixedpoint.NewFromFloat(1.0),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Errorf("can not submit orders")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("createdOrders: %+v", createdOrders)
|
||||||
|
```
|
||||||
|
|
||||||
|
There are some pre-defined order types you can use:
|
||||||
|
|
||||||
|
- `types.OrderTypeLimit`
|
||||||
|
- `types.OrderTypeMarket`
|
||||||
|
- `types.OrderTypeStopMarket`
|
||||||
|
- `types.OrderTypeStopLimit`
|
||||||
|
- `types.OrderTypeLimitMaker` - forces the order to be a maker.
|
||||||
|
|
||||||
|
Although it's crypto market, the above order types are actually derived from the stock market:
|
||||||
|
|
||||||
|
A limit order is an order to buy or sell a stock with a restriction on the maximum price to be paid or the minimum price
|
||||||
|
to be received (the "limit price"). If the order is filled, it will only be at the specified limit price or better.
|
||||||
|
However, there is no assurance of execution. A limit order may be appropriate when you think you can buy at a price
|
||||||
|
lower than--or sell at a price higher than -- the current quote.
|
||||||
|
|
||||||
|
A market order is an order to buy or sell a stock at the market's current best available price. A market order typically
|
||||||
|
ensures an execution, but it does not guarantee a specified price. Market orders are optimal when the primary goal is to
|
||||||
|
execute the trade immediately. A market order is generally appropriate when you think a stock is priced right, when you
|
||||||
|
are sure you want a fill on your order, or when you want an immediate execution.
|
||||||
|
|
||||||
|
A stop order is an order to buy or sell a stock at the market price once the stock has traded at or through a specified
|
||||||
|
price (the "stop price"). If the stock reaches the stop price, the order becomes a market order and is filled at the
|
||||||
|
next available market price.
|
||||||
|
|
||||||
|
## UserDataStream
|
||||||
|
|
||||||
|
UserDataStream is an authenticated connection to the crypto exchange. You can receive the following data type from the
|
||||||
|
user data stream:
|
||||||
|
|
||||||
|
- OrderUpdate
|
||||||
|
- TradeUpdate
|
||||||
|
- BalanceUpdate
|
||||||
|
|
||||||
|
When you submit an order to the exchange, you might want to know when the order is filled or not, user data stream is
|
||||||
|
the real time notification let you receive the order update event.
|
||||||
|
|
||||||
|
To get the order update from the user data stream:
|
||||||
|
|
||||||
|
```go
|
||||||
|
session.UserDataStream.OnOrderUpdate(func(order types.Order) {
|
||||||
|
if order.Status == types.OrderStatusFilled {
|
||||||
|
log.Infof("your order is filled: %+v", order)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
However, order update only contains status, price, quantity of the order, if you're submitting market order, you won't know
|
||||||
|
the actual price of the order execution.
|
||||||
|
|
||||||
|
One order can be filled by different size trades from the market, by collecting the trades, you can calculate the
|
||||||
|
average price of the order execution and the total trading fee that you used for the order.
|
||||||
|
|
||||||
|
If you need to get the details of the trade execution. you need the trade update event:
|
||||||
|
|
||||||
|
```go
|
||||||
|
session.UserDataStream.OnTrade(func(trade types.Trade) {
|
||||||
|
log.Infof("trade price %f, fee %f %s", trade.Price.Float64(), trade.Fee.Float64(), trade.FeeCurrency)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
To monitor your balance change, you can use the balance update event callback:
|
||||||
|
|
||||||
|
```go
|
||||||
|
session.UserDataStream.OnBalanceUpdate(func(balances types.BalanceMap) {
|
||||||
|
log.Infof("balance update: %+v", balances)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that, as we mentioned above, the user data stream is a session-wide stream, that means you might receive the order update event for other strategies.
|
||||||
|
|
||||||
|
To prevent that, you need to manage your active order for your strategy:
|
||||||
|
|
||||||
|
```go
|
||||||
|
activeBook := bbgo.NewActiveOrderBook("BTCUSDT")
|
||||||
|
activeBook.Bind(session.UserDataStream)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, when you create some orders, you can register your order to the active order book, so that it can manage the order
|
||||||
|
update:
|
||||||
|
|
||||||
|
```go
|
||||||
|
createdOrders, err := session.Exchange.SubmitOrders(ctx, types.SubmitOrder{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Price: fixedpoint.NewFromFloat(18000.0),
|
||||||
|
Quantity: fixedpoint.NewFromFloat(1.0),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Errorf("can not submit orders")
|
||||||
|
}
|
||||||
|
|
||||||
|
activeBook.Add(createdOrders...)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notification
|
||||||
|
|
||||||
|
You can use the notification API to send notification to Telegram or Slack:
|
||||||
|
|
||||||
|
```go
|
||||||
|
bbgo.Notify(message)
|
||||||
|
bbgo.Notify(message, objs...)
|
||||||
|
bbgo.Notify(format, arg1, arg2, arg3, objs...)
|
||||||
|
bbgo.Notify(object, object2, object3)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that, if you're using the third format, simple arguments (float, bool, string... etc) will be used for calling the
|
||||||
|
fmt.Sprintf, and the extra arguments will be rendered as attachments.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
bbgo.Notify("%s found support price: %f", "BTCUSDT", 19000.0, kline)
|
||||||
|
```
|
||||||
|
|
||||||
|
The above call will render the first format string with the given float number 19000, and then attach the kline object as the attachment.
|
||||||
|
|
||||||
|
## Handling Trades and Profit
|
||||||
|
|
||||||
|
In order to manage the trades and orders for each strategy, BBGO designed an order executor API that helps you collect
|
||||||
|
the related trades and orders from the strategy, so trades from other strategies won't bother your logics.
|
||||||
|
|
||||||
|
To do that, you can use the *bbgo.GeneralOrderExecutor:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var profitStats = types.NewProfitStats(s.Market)
|
||||||
|
var position = types.NewPositionFromMarket(s.Market)
|
||||||
|
var tradeStats = &types.TradeStats{}
|
||||||
|
orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, position)
|
||||||
|
|
||||||
|
// bind the trade events to update the profit stats
|
||||||
|
orderExecutor.BindProfitStats(profitStats)
|
||||||
|
|
||||||
|
// bind the trade events to update the trade stats
|
||||||
|
orderExecutor.BindTradeStats(tradeStats)
|
||||||
|
orderExecutor.Bind()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Graceful Shutdown
|
||||||
|
|
||||||
|
When BBGO shuts down, you might want to clean up your open orders for your strategy, to do that, you can use the
|
||||||
|
OnShutdown API to register your handler.
|
||||||
|
|
||||||
|
```go
|
||||||
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
|
||||||
|
if err := s.orderExecutor.GracefulCancel(ctx) ; err != nil {
|
||||||
|
log.WithError(err).Error("graceful cancel order error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
When you need to adjust the parameters and restart BBGO process, everything in the memory will be reset after the
|
||||||
|
restart, how can we keep these data?
|
||||||
|
|
||||||
|
Although BBGO is written in Golang, BBGO provides a useful dynamic system to help you persist your data.
|
||||||
|
|
||||||
|
If you have some state needs to preserve before shutting down, you can simply add the `persistence` struct tag to the field,
|
||||||
|
and BBGO will automatically save and restore your state. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Strategy struct {
|
||||||
|
Position *types.Position `persistence:"position"`
|
||||||
|
ProfitStats *types.ProfitStats `persistence:"profit_stats"`
|
||||||
|
TradeStats *types.TradeStats `persistence:"trade_stats"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And remember to add the `persistence` section in your bbgo.yaml config:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the Run method of your strategy, you need to check if these fields are nil, and you need to initialize them:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if s.Position == nil {
|
||||||
|
s.Position = types.NewPositionFromMarket(s.Market)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ProfitStats == nil {
|
||||||
|
s.ProfitStats = types.NewProfitStats(s.Market)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.TradeStats == nil {
|
||||||
|
s.TradeStats = types.NewTradeStats(s.Symbol)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it. Hit Ctrl-C and you should see BBGO saving your strategy states.
|
||||||
|
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -75,6 +75,7 @@ require (
|
||||||
github.com/go-test/deep v1.0.6 // indirect
|
github.com/go-test/deep v1.0.6 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
|
@ -116,11 +117,13 @@ require (
|
||||||
go.opentelemetry.io/otel/trace v0.19.0 // indirect
|
go.opentelemetry.io/otel/trace v0.19.0 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
|
||||||
|
golang.org/x/mod v0.5.1 // indirect
|
||||||
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect
|
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect
|
||||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
golang.org/x/tools v0.1.9 // indirect
|
golang.org/x/tools v0.1.9 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect
|
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect
|
||||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
|
9
go.sum
9
go.sum
|
@ -194,7 +194,10 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
|
||||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
@ -330,8 +333,6 @@ github.com/lestrrat-go/strftime v1.0.0/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR7
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
|
|
||||||
github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
|
||||||
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
|
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
|
||||||
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||||
|
@ -525,6 +526,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||||
|
@ -601,6 +603,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
||||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
@ -782,6 +786,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
|
||||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
|
golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
|
||||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
|
|
|
@ -53,12 +53,12 @@ type Exchange struct {
|
||||||
sourceName types.ExchangeName
|
sourceName types.ExchangeName
|
||||||
publicExchange types.Exchange
|
publicExchange types.Exchange
|
||||||
srv *service.BacktestService
|
srv *service.BacktestService
|
||||||
startTime, endTime time.Time
|
currentTime time.Time
|
||||||
|
|
||||||
account *types.Account
|
account *types.Account
|
||||||
config *bbgo.Backtest
|
config *bbgo.Backtest
|
||||||
|
|
||||||
UserDataStream, MarketDataStream types.StandardStreamEmitter
|
MarketDataStream types.StandardStreamEmitter
|
||||||
|
|
||||||
trades map[string][]types.Trade
|
trades map[string][]types.Trade
|
||||||
tradesMutex sync.Mutex
|
tradesMutex sync.Mutex
|
||||||
|
@ -72,20 +72,6 @@ type Exchange struct {
|
||||||
markets types.MarketMap
|
markets types.MarketMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) {
|
|
||||||
book := e.matchingBooks[q.Symbol]
|
|
||||||
oid, err := strconv.ParseUint(q.OrderID, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
order, ok := book.getOrder(oid)
|
|
||||||
if ok {
|
|
||||||
return &order, nil
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, srv *service.BacktestService, config *bbgo.Backtest) (*Exchange, error) {
|
func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, srv *service.BacktestService, config *bbgo.Backtest) (*Exchange, error) {
|
||||||
ex := sourceExchange
|
ex := sourceExchange
|
||||||
|
|
||||||
|
@ -94,14 +80,7 @@ func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, s
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var startTime, endTime time.Time
|
startTime := config.StartTime.Time()
|
||||||
startTime = config.StartTime.Time()
|
|
||||||
if config.EndTime != nil {
|
|
||||||
endTime = config.EndTime.Time()
|
|
||||||
} else {
|
|
||||||
endTime = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
configAccount := config.GetAccount(sourceName.String())
|
configAccount := config.GetAccount(sourceName.String())
|
||||||
|
|
||||||
account := &types.Account{
|
account := &types.Account{
|
||||||
|
@ -120,8 +99,7 @@ func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, s
|
||||||
srv: srv,
|
srv: srv,
|
||||||
config: config,
|
config: config,
|
||||||
account: account,
|
account: account,
|
||||||
startTime: startTime,
|
currentTime: startTime,
|
||||||
endTime: endTime,
|
|
||||||
closedOrders: make(map[string][]types.Order),
|
closedOrders: make(map[string][]types.Order),
|
||||||
trades: make(map[string][]types.Trade),
|
trades: make(map[string][]types.Trade),
|
||||||
}
|
}
|
||||||
|
@ -159,7 +137,7 @@ func (e *Exchange) addMatchingBook(symbol string, market types.Market) {
|
||||||
|
|
||||||
func (e *Exchange) _addMatchingBook(symbol string, market types.Market) {
|
func (e *Exchange) _addMatchingBook(symbol string, market types.Market) {
|
||||||
e.matchingBooks[symbol] = &SimplePriceMatching{
|
e.matchingBooks[symbol] = &SimplePriceMatching{
|
||||||
CurrentTime: e.startTime,
|
CurrentTime: e.currentTime,
|
||||||
Account: e.account,
|
Account: e.account,
|
||||||
Market: market,
|
Market: market,
|
||||||
closedOrders: make(map[uint64]types.Order),
|
closedOrders: make(map[uint64]types.Order),
|
||||||
|
@ -172,10 +150,21 @@ func (e *Exchange) NewStream() types.Stream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) {
|
func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) {
|
||||||
if e.UserDataStream == nil {
|
book := e.matchingBooks[q.Symbol]
|
||||||
return createdOrders, fmt.Errorf("SubmitOrders should be called after UserDataStream been initialized")
|
oid, err := strconv.ParseUint(q.OrderID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
order, ok := book.getOrder(oid)
|
||||||
|
if ok {
|
||||||
|
return &order, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) {
|
||||||
for _, order := range orders {
|
for _, order := range orders {
|
||||||
symbol := order.Symbol
|
symbol := order.Symbol
|
||||||
matching, ok := e.matchingBook(symbol)
|
matching, ok := e.matchingBook(symbol)
|
||||||
|
@ -196,8 +185,6 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
|
||||||
case types.OrderStatusFilled, types.OrderStatusCanceled, types.OrderStatusRejected:
|
case types.OrderStatusFilled, types.OrderStatusCanceled, types.OrderStatusRejected:
|
||||||
e.addClosedOrder(*createdOrder)
|
e.addClosedOrder(*createdOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.UserDataStream.EmitOrderUpdate(*createdOrder)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,20 +210,15 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {
|
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {
|
||||||
if e.UserDataStream == nil {
|
|
||||||
return fmt.Errorf("CancelOrders should be called after UserDataStream been initialized")
|
|
||||||
}
|
|
||||||
for _, order := range orders {
|
for _, order := range orders {
|
||||||
matching, ok := e.matchingBook(order.Symbol)
|
matching, ok := e.matchingBook(order.Symbol)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("matching engine is not initialized for symbol %s", order.Symbol)
|
return fmt.Errorf("matching engine is not initialized for symbol %s", order.Symbol)
|
||||||
}
|
}
|
||||||
canceledOrder, err := matching.CancelOrder(order)
|
_, err := matching.CancelOrder(order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.UserDataStream.EmitOrderUpdate(canceledOrder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -318,21 +300,21 @@ func (e *Exchange) matchingBook(symbol string) (*SimplePriceMatching, bool) {
|
||||||
return m, ok
|
return m, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) InitMarketData() {
|
func (e *Exchange) BindUserData(userDataStream types.StandardStreamEmitter) {
|
||||||
e.UserDataStream.OnTradeUpdate(func(trade types.Trade) {
|
userDataStream.OnTradeUpdate(func(trade types.Trade) {
|
||||||
e.addTrade(trade)
|
e.addTrade(trade)
|
||||||
})
|
})
|
||||||
|
|
||||||
e.matchingBooksMutex.Lock()
|
e.matchingBooksMutex.Lock()
|
||||||
for _, matching := range e.matchingBooks {
|
for _, matching := range e.matchingBooks {
|
||||||
matching.OnTradeUpdate(e.UserDataStream.EmitTradeUpdate)
|
matching.OnTradeUpdate(userDataStream.EmitTradeUpdate)
|
||||||
matching.OnOrderUpdate(e.UserDataStream.EmitOrderUpdate)
|
matching.OnOrderUpdate(userDataStream.EmitOrderUpdate)
|
||||||
matching.OnBalanceUpdate(e.UserDataStream.EmitBalanceUpdate)
|
matching.OnBalanceUpdate(userDataStream.EmitBalanceUpdate)
|
||||||
}
|
}
|
||||||
e.matchingBooksMutex.Unlock()
|
e.matchingBooksMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) SubscribeMarketData(extraIntervals ...types.Interval) (chan types.KLine, error) {
|
func (e *Exchange) SubscribeMarketData(startTime, endTime time.Time, extraIntervals ...types.Interval) (chan types.KLine, error) {
|
||||||
log.Infof("collecting backtest configurations...")
|
log.Infof("collecting backtest configurations...")
|
||||||
|
|
||||||
loadedSymbols := map[string]struct{}{}
|
loadedSymbols := map[string]struct{}{}
|
||||||
|
@ -371,7 +353,7 @@ func (e *Exchange) SubscribeMarketData(extraIntervals ...types.Interval) (chan t
|
||||||
|
|
||||||
log.Infof("using symbols: %v and intervals: %v for back-testing", symbols, intervals)
|
log.Infof("using symbols: %v and intervals: %v for back-testing", symbols, intervals)
|
||||||
log.Infof("querying klines from database...")
|
log.Infof("querying klines from database...")
|
||||||
klineC, errC := e.srv.QueryKLinesCh(e.startTime, e.endTime, e, symbols, intervals)
|
klineC, errC := e.srv.QueryKLinesCh(startTime, endTime, e, symbols, intervals)
|
||||||
go func() {
|
go func() {
|
||||||
if err := <-errC; err != nil {
|
if err := <-errC; err != nil {
|
||||||
log.WithError(err).Error("backtest data feed error")
|
log.WithError(err).Error("backtest data feed error")
|
||||||
|
@ -382,6 +364,8 @@ func (e *Exchange) SubscribeMarketData(extraIntervals ...types.Interval) (chan t
|
||||||
|
|
||||||
func (e *Exchange) ConsumeKLine(k types.KLine) {
|
func (e *Exchange) ConsumeKLine(k types.KLine) {
|
||||||
if k.Interval == types.Interval1m {
|
if k.Interval == types.Interval1m {
|
||||||
|
e.currentTime = k.EndTime.Time()
|
||||||
|
|
||||||
matching, ok := e.matchingBook(k.Symbol)
|
matching, ok := e.matchingBook(k.Symbol)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Errorf("matching book of %s is not initialized", k.Symbol)
|
log.Errorf("matching book of %s is not initialized", k.Symbol)
|
||||||
|
|
|
@ -99,7 +99,6 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
|
||||||
}
|
}
|
||||||
m.askOrders = orders
|
m.askOrders = orders
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
|
@ -191,6 +190,8 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
|
||||||
order2.ExecutedQuantity = order2.Quantity
|
order2.ExecutedQuantity = order2.Quantity
|
||||||
order2.IsWorking = false
|
order2.IsWorking = false
|
||||||
|
|
||||||
|
m.EmitOrderUpdate(order2)
|
||||||
|
|
||||||
// let the exchange emit the "FILLED" order update (we need the closed order)
|
// let the exchange emit the "FILLED" order update (we need the closed order)
|
||||||
// m.EmitOrderUpdate(order2)
|
// m.EmitOrderUpdate(order2)
|
||||||
return &order2, &trade, nil
|
return &order2, &trade, nil
|
||||||
|
@ -570,6 +571,7 @@ func (m *SimplePriceMatching) getOrder(orderID uint64) (types.Order, bool) {
|
||||||
|
|
||||||
func (m *SimplePriceMatching) processKLine(kline types.KLine) {
|
func (m *SimplePriceMatching) processKLine(kline types.KLine) {
|
||||||
m.CurrentTime = kline.EndTime.Time()
|
m.CurrentTime = kline.EndTime.Time()
|
||||||
|
|
||||||
if m.LastPrice.IsZero() {
|
if m.LastPrice.IsZero() {
|
||||||
m.LastPrice = kline.Open
|
m.LastPrice = kline.Open
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -131,8 +131,6 @@ func (b *ActiveOrderBook) orderUpdateHandler(order types.Order) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("[ActiveOrderBook] received order update: %+v", order)
|
|
||||||
|
|
||||||
switch order.Status {
|
switch order.Status {
|
||||||
case types.OrderStatusFilled:
|
case types.OrderStatusFilled:
|
||||||
// make sure we have the order and we remove it
|
// make sure we have the order and we remove it
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/datatype"
|
"github.com/c9s/bbgo/pkg/datatype"
|
||||||
|
"github.com/c9s/bbgo/pkg/dynamic"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/service"
|
"github.com/c9s/bbgo/pkg/service"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
@ -387,7 +388,7 @@ func (c *Config) GetSignature() string {
|
||||||
id := strategy.ID()
|
id := strategy.ID()
|
||||||
ps = append(ps, id)
|
ps = append(ps, id)
|
||||||
|
|
||||||
if symbol, ok := isSymbolBasedStrategy(reflect.ValueOf(strategy)); ok {
|
if symbol, ok := dynamic.LookupSymbolField(reflect.ValueOf(strategy)); ok {
|
||||||
ps = append(ps, symbol)
|
ps = append(ps, symbol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,44 +3,80 @@ package bbgo
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/dynamic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ExitMethodSet []ExitMethod
|
||||||
|
|
||||||
|
func (s *ExitMethodSet) SetAndSubscribe(session *ExchangeSession, parent interface{}) {
|
||||||
|
for i := range *s {
|
||||||
|
m := (*s)[i]
|
||||||
|
|
||||||
|
// manually inherit configuration from strategy
|
||||||
|
m.Inherit(parent)
|
||||||
|
m.Subscribe(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ExitMethod struct {
|
type ExitMethod struct {
|
||||||
RoiStopLoss *RoiStopLoss `json:"roiStopLoss"`
|
RoiStopLoss *RoiStopLoss `json:"roiStopLoss"`
|
||||||
ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"`
|
ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"`
|
||||||
RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"`
|
RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"`
|
||||||
LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"`
|
LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"`
|
||||||
CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"`
|
CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"`
|
||||||
|
TrailingStop *TrailingStop2 `json:"trailingStop"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ExitMethod) Subscribe() {
|
// Inherit is used for inheriting properties from the given strategy struct
|
||||||
// TODO: pull out this implementation as a simple function to reflect.go
|
// for example, some exit method requires the default interval and symbol name from the strategy param object
|
||||||
rv := reflect.ValueOf(m)
|
func (m *ExitMethod) Inherit(parent interface{}) {
|
||||||
rt := reflect.TypeOf(m)
|
// we need to pass some information from the strategy configuration to the exit methods, like symbol, interval and window
|
||||||
|
rt := reflect.TypeOf(m).Elem()
|
||||||
rv = rv.Elem()
|
rv := reflect.ValueOf(m).Elem()
|
||||||
rt = rt.Elem()
|
for j := 0; j < rv.NumField(); j++ {
|
||||||
infType := reflect.TypeOf((*types.Subscriber)(nil)).Elem()
|
if !rt.Field(j).IsExported() {
|
||||||
for i := 0; i < rt.NumField(); i++ {
|
continue
|
||||||
fieldType := rt.Field(i)
|
|
||||||
if fieldType.Type.Implements(infType) {
|
|
||||||
method := rv.Field(i).MethodByName("Subscribe")
|
|
||||||
method.Call(nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldValue := rv.Field(j)
|
||||||
|
if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic.InheritStructValues(fieldValue.Interface(), parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ExitMethod) Subscribe(session *ExchangeSession) {
|
||||||
|
if err := dynamic.CallStructFieldsMethod(m, "Subscribe", session); err != nil {
|
||||||
|
panic(errors.Wrap(err, "dynamic Subscribe call failed"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ExitMethod) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
|
func (m *ExitMethod) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
|
||||||
if m.ProtectiveStopLoss != nil {
|
if m.ProtectiveStopLoss != nil {
|
||||||
m.ProtectiveStopLoss.Bind(session, orderExecutor)
|
m.ProtectiveStopLoss.Bind(session, orderExecutor)
|
||||||
} else if m.RoiStopLoss != nil {
|
}
|
||||||
|
|
||||||
|
if m.RoiStopLoss != nil {
|
||||||
m.RoiStopLoss.Bind(session, orderExecutor)
|
m.RoiStopLoss.Bind(session, orderExecutor)
|
||||||
} else if m.RoiTakeProfit != nil {
|
}
|
||||||
|
|
||||||
|
if m.RoiTakeProfit != nil {
|
||||||
m.RoiTakeProfit.Bind(session, orderExecutor)
|
m.RoiTakeProfit.Bind(session, orderExecutor)
|
||||||
} else if m.LowerShadowTakeProfit != nil {
|
}
|
||||||
|
|
||||||
|
if m.LowerShadowTakeProfit != nil {
|
||||||
m.LowerShadowTakeProfit.Bind(session, orderExecutor)
|
m.LowerShadowTakeProfit.Bind(session, orderExecutor)
|
||||||
} else if m.CumulatedVolumeTakeProfit != nil {
|
}
|
||||||
|
|
||||||
|
if m.CumulatedVolumeTakeProfit != nil {
|
||||||
m.CumulatedVolumeTakeProfit.Bind(session, orderExecutor)
|
m.CumulatedVolumeTakeProfit.Bind(session, orderExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.TrailingStop != nil {
|
||||||
|
m.TrailingStop.Bind(session, orderExecutor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package bbgo
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
@ -15,7 +17,10 @@ import (
|
||||||
// > SELECT start_time, `interval`, quote_volume, open, close FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' ORDER BY quote_volume DESC LIMIT 20;
|
// > SELECT start_time, `interval`, quote_volume, open, close FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' ORDER BY quote_volume DESC LIMIT 20;
|
||||||
//
|
//
|
||||||
type CumulatedVolumeTakeProfit struct {
|
type CumulatedVolumeTakeProfit struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
|
|
||||||
Ratio fixedpoint.Value `json:"ratio"`
|
Ratio fixedpoint.Value `json:"ratio"`
|
||||||
MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"`
|
MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"`
|
||||||
|
|
||||||
|
@ -31,11 +36,7 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor
|
||||||
|
|
||||||
store, _ := session.MarketDataStore(position.Symbol)
|
store, _ := session.MarketDataStore(position.Symbol)
|
||||||
|
|
||||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||||
if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
closePrice := kline.Close
|
closePrice := kline.Close
|
||||||
if position.IsClosed() || position.IsDust(closePrice) {
|
if position.IsClosed() || position.IsDust(closePrice) {
|
||||||
return
|
return
|
||||||
|
@ -46,7 +47,16 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if klines, ok := store.KLinesOfInterval(s.Interval); ok {
|
klines, ok := store.KLinesOfInterval(s.Interval)
|
||||||
|
if !ok {
|
||||||
|
log.Warnf("history kline not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*klines) < s.Window {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var cbv = fixedpoint.Zero
|
var cbv = fixedpoint.Zero
|
||||||
var cqv = fixedpoint.Zero
|
var cqv = fixedpoint.Zero
|
||||||
for i := 0; i < s.Window; i++ {
|
for i := 0; i < s.Window; i++ {
|
||||||
|
@ -65,6 +75,5 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor
|
||||||
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit")
|
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,22 +8,31 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type LowerShadowTakeProfit struct {
|
type LowerShadowTakeProfit struct {
|
||||||
Ratio fixedpoint.Value `json:"ratio"`
|
// inherit from the strategy
|
||||||
|
types.IntervalWindow
|
||||||
|
|
||||||
|
// inherit from the strategy
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
|
||||||
|
Ratio fixedpoint.Value `json:"ratio"`
|
||||||
session *ExchangeSession
|
session *ExchangeSession
|
||||||
orderExecutor *GeneralOrderExecutor
|
orderExecutor *GeneralOrderExecutor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *LowerShadowTakeProfit) Subscribe(session *ExchangeSession) {
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
|
func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
|
||||||
s.session = session
|
s.session = session
|
||||||
s.orderExecutor = orderExecutor
|
s.orderExecutor = orderExecutor
|
||||||
|
|
||||||
position := orderExecutor.Position()
|
stdIndicatorSet, _ := session.StandardIndicatorSet(s.Symbol)
|
||||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
ewma := stdIndicatorSet.EWMA(s.IntervalWindow)
|
||||||
if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
position := orderExecutor.Position()
|
||||||
|
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||||
closePrice := kline.Close
|
closePrice := kline.Close
|
||||||
if position.IsClosed() || position.IsDust(closePrice) {
|
if position.IsClosed() || position.IsDust(closePrice) {
|
||||||
return
|
return
|
||||||
|
@ -38,6 +47,11 @@ func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *Ge
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skip close price higher than the ewma
|
||||||
|
if closePrice.Float64() > ewma.Last() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Ratio) > 0 {
|
if kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Ratio) > 0 {
|
||||||
Notify("%s TakeProfit triggered by shadow ratio %f, price = %f",
|
Notify("%s TakeProfit triggered by shadow ratio %f, price = %f",
|
||||||
position.Symbol,
|
position.Symbol,
|
||||||
|
@ -48,5 +62,5 @@ func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *Ge
|
||||||
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One)
|
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,24 +8,26 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type RoiStopLoss struct {
|
type RoiStopLoss struct {
|
||||||
|
Symbol string
|
||||||
Percentage fixedpoint.Value `json:"percentage"`
|
Percentage fixedpoint.Value `json:"percentage"`
|
||||||
|
|
||||||
session *ExchangeSession
|
session *ExchangeSession
|
||||||
orderExecutor *GeneralOrderExecutor
|
orderExecutor *GeneralOrderExecutor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RoiStopLoss) Subscribe(session *ExchangeSession) {
|
||||||
|
// use 1m kline to handle roi stop
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RoiStopLoss) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
|
func (s *RoiStopLoss) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
|
||||||
s.session = session
|
s.session = session
|
||||||
s.orderExecutor = orderExecutor
|
s.orderExecutor = orderExecutor
|
||||||
|
|
||||||
position := orderExecutor.Position()
|
position := orderExecutor.Position()
|
||||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) {
|
||||||
if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.checkStopPrice(kline.Close, position)
|
s.checkStopPrice(kline.Close, position)
|
||||||
})
|
}))
|
||||||
|
|
||||||
if !IsBackTesting {
|
if !IsBackTesting {
|
||||||
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
|
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
|
||||||
|
|
|
@ -4,5 +4,5 @@ import "testing"
|
||||||
|
|
||||||
func TestExitMethod(t *testing.T) {
|
func TestExitMethod(t *testing.T) {
|
||||||
em := &ExitMethod{}
|
em := &ExitMethod{}
|
||||||
em.Subscribe()
|
em.Subscribe(&ExchangeSession{})
|
||||||
}
|
}
|
||||||
|
|
157
pkg/bbgo/exit_trailing_stop.go
Normal file
157
pkg/bbgo/exit_trailing_stop.go
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
package bbgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TrailingStop2 struct {
|
||||||
|
Symbol string
|
||||||
|
|
||||||
|
// CallbackRate is the callback rate from the previous high price
|
||||||
|
CallbackRate fixedpoint.Value `json:"callbackRate,omitempty"`
|
||||||
|
|
||||||
|
ActivationRatio fixedpoint.Value `json:"activationRatio,omitempty"`
|
||||||
|
|
||||||
|
// ClosePosition is a percentage of the position to be closed
|
||||||
|
ClosePosition fixedpoint.Value `json:"closePosition,omitempty"`
|
||||||
|
|
||||||
|
// MinProfit is the percentage of the minimum profit ratio.
|
||||||
|
// Stop order will be activated only when the price reaches above this threshold.
|
||||||
|
MinProfit fixedpoint.Value `json:"minProfit,omitempty"`
|
||||||
|
|
||||||
|
// Interval is the time resolution to update the stop order
|
||||||
|
// KLine per Interval will be used for updating the stop order
|
||||||
|
Interval types.Interval `json:"interval,omitempty"`
|
||||||
|
|
||||||
|
Side types.SideType `json:"side,omitempty"`
|
||||||
|
|
||||||
|
latestHigh fixedpoint.Value
|
||||||
|
|
||||||
|
// activated: when the price reaches the min profit price, we set the activated to true to enable trailing stop
|
||||||
|
activated bool
|
||||||
|
|
||||||
|
// private fields
|
||||||
|
session *ExchangeSession
|
||||||
|
orderExecutor *GeneralOrderExecutor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrailingStop2) Subscribe(session *ExchangeSession) {
|
||||||
|
// use 1m kline to handle roi stop
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
|
||||||
|
s.session = session
|
||||||
|
s.orderExecutor = orderExecutor
|
||||||
|
s.latestHigh = fixedpoint.Zero
|
||||||
|
|
||||||
|
position := orderExecutor.Position()
|
||||||
|
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||||
|
if err := s.checkStopPrice(kline.Close, position); err != nil {
|
||||||
|
log.WithError(err).Errorf("error")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
if !IsBackTesting {
|
||||||
|
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
|
||||||
|
if trade.Symbol != position.Symbol {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.checkStopPrice(trade.Price, position); err != nil {
|
||||||
|
log.WithError(err).Errorf("error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrailingStop2) getRatio(price fixedpoint.Value, position *types.Position) (fixedpoint.Value, error) {
|
||||||
|
switch s.Side {
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
// for short position
|
||||||
|
return position.AverageCost.Sub(price).Div(price), nil
|
||||||
|
case types.SideTypeSell:
|
||||||
|
return price.Sub(position.AverageCost).Div(position.AverageCost), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixedpoint.Zero, fmt.Errorf("unexpected side type: %v", s.Side)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrailingStop2) checkStopPrice(price fixedpoint.Value, position *types.Position) error {
|
||||||
|
if position.IsClosed() || position.IsDust(price) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.MinProfit.IsZero() {
|
||||||
|
// check if we have the minimal profit
|
||||||
|
roi := position.ROI(price)
|
||||||
|
if roi.Compare(s.MinProfit) >= 0 {
|
||||||
|
Notify("[trailingStop] activated: ROI %f > minimal profit ratio %f", roi.Float64(), s.MinProfit.Float64())
|
||||||
|
s.activated = true
|
||||||
|
}
|
||||||
|
} else if !s.ActivationRatio.IsZero() {
|
||||||
|
ratio, err := s.getRatio(price, position)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ratio.Compare(s.ActivationRatio) >= 0 {
|
||||||
|
s.activated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the latest high for the sell order, or the latest low for the buy order
|
||||||
|
if s.latestHigh.IsZero() {
|
||||||
|
s.latestHigh = price
|
||||||
|
} else {
|
||||||
|
switch s.Side {
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
s.latestHigh = fixedpoint.Min(price, s.latestHigh)
|
||||||
|
case types.SideTypeSell:
|
||||||
|
s.latestHigh = fixedpoint.Max(price, s.latestHigh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.activated {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s.Side {
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
s.latestHigh = fixedpoint.Min(price, s.latestHigh)
|
||||||
|
|
||||||
|
change := price.Sub(s.latestHigh).Div(s.latestHigh)
|
||||||
|
if change.Compare(s.CallbackRate) >= 0 {
|
||||||
|
// submit order
|
||||||
|
return s.triggerStop(price)
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.SideTypeSell:
|
||||||
|
s.latestHigh = fixedpoint.Max(price, s.latestHigh)
|
||||||
|
|
||||||
|
change := s.latestHigh.Sub(price).Div(price)
|
||||||
|
if change.Compare(s.CallbackRate) >= 0 {
|
||||||
|
// submit order
|
||||||
|
return s.triggerStop(price)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrailingStop2) triggerStop(price fixedpoint.Value) error {
|
||||||
|
// reset activated flag
|
||||||
|
defer func() {
|
||||||
|
s.activated = false
|
||||||
|
s.latestHigh = fixedpoint.Zero
|
||||||
|
}()
|
||||||
|
Notify("[TrailingStop] %s stop loss triggered. price: %f callback rate: %f", s.Symbol, price.Float64(), s.CallbackRate.Float64())
|
||||||
|
ctx := context.Background()
|
||||||
|
return s.orderExecutor.ClosePosition(ctx, fixedpoint.One, "trailingStop")
|
||||||
|
}
|
182
pkg/bbgo/exit_trailing_stop_test.go
Normal file
182
pkg/bbgo/exit_trailing_stop_test.go
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
package bbgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
"github.com/c9s/bbgo/pkg/types/mocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getTestMarket returns the BTCUSDT market information
|
||||||
|
// for tests, we always use BTCUSDT
|
||||||
|
func getTestMarket() types.Market {
|
||||||
|
market := types.Market{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
PricePrecision: 8,
|
||||||
|
VolumePrecision: 8,
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
BaseCurrency: "BTC",
|
||||||
|
MinNotional: fixedpoint.MustNewFromString("0.001"),
|
||||||
|
MinAmount: fixedpoint.MustNewFromString("10.0"),
|
||||||
|
MinQuantity: fixedpoint.MustNewFromString("0.001"),
|
||||||
|
}
|
||||||
|
return market
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrailingStop_ShortPosition(t *testing.T) {
|
||||||
|
market := getTestMarket()
|
||||||
|
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
|
||||||
|
mockEx := mocks.NewMockExchange(mockCtrl)
|
||||||
|
mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2)
|
||||||
|
mockEx.EXPECT().SubmitOrders(gomock.Any(), types.SubmitOrder{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Type: types.OrderTypeMarket,
|
||||||
|
Market: market,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(1.0),
|
||||||
|
Tag: "trailingStop",
|
||||||
|
})
|
||||||
|
|
||||||
|
session := NewExchangeSession("test", mockEx)
|
||||||
|
assert.NotNil(t, session)
|
||||||
|
|
||||||
|
session.markets[market.Symbol] = market
|
||||||
|
|
||||||
|
position := types.NewPositionFromMarket(market)
|
||||||
|
position.AverageCost = fixedpoint.NewFromFloat(20000.0)
|
||||||
|
position.Base = fixedpoint.NewFromFloat(-1.0)
|
||||||
|
|
||||||
|
orderExecutor := NewGeneralOrderExecutor(session, "BTCUSDT", "test", "test-01", position)
|
||||||
|
|
||||||
|
activationRatio := fixedpoint.NewFromFloat(0.01)
|
||||||
|
callbackRatio := fixedpoint.NewFromFloat(0.01)
|
||||||
|
stop := &TrailingStop2{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Interval: types.Interval1m,
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
CallbackRate: callbackRatio,
|
||||||
|
ActivationRatio: activationRatio,
|
||||||
|
}
|
||||||
|
stop.Bind(session, orderExecutor)
|
||||||
|
|
||||||
|
// the same price
|
||||||
|
currentPrice := fixedpoint.NewFromFloat(20000.0)
|
||||||
|
err := stop.checkStopPrice(currentPrice, position)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.False(t, stop.activated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20000 - 1% = 19800
|
||||||
|
currentPrice = currentPrice.Mul(one.Sub(activationRatio))
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(19800.0), currentPrice)
|
||||||
|
|
||||||
|
err = stop.checkStopPrice(currentPrice, position)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.True(t, stop.activated)
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(19800.0), stop.latestHigh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 19800 - 1% = 19602
|
||||||
|
currentPrice = currentPrice.Mul(one.Sub(callbackRatio))
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(19602.0), currentPrice)
|
||||||
|
|
||||||
|
err = stop.checkStopPrice(currentPrice, position)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(19602.0), stop.latestHigh)
|
||||||
|
assert.True(t, stop.activated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 19602 + 1% = 19798.02
|
||||||
|
currentPrice = currentPrice.Mul(one.Add(callbackRatio))
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(19798.02), currentPrice)
|
||||||
|
|
||||||
|
err = stop.checkStopPrice(currentPrice, position)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.Equal(t, fixedpoint.Zero, stop.latestHigh)
|
||||||
|
assert.False(t, stop.activated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrailingStop_LongPosition(t *testing.T) {
|
||||||
|
market := getTestMarket()
|
||||||
|
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
|
||||||
|
mockEx := mocks.NewMockExchange(mockCtrl)
|
||||||
|
mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2)
|
||||||
|
mockEx.EXPECT().SubmitOrders(gomock.Any(), types.SubmitOrder{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Type: types.OrderTypeMarket,
|
||||||
|
Market: market,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(1.0),
|
||||||
|
Tag: "trailingStop",
|
||||||
|
})
|
||||||
|
|
||||||
|
session := NewExchangeSession("test", mockEx)
|
||||||
|
assert.NotNil(t, session)
|
||||||
|
|
||||||
|
session.markets[market.Symbol] = market
|
||||||
|
|
||||||
|
position := types.NewPositionFromMarket(market)
|
||||||
|
position.AverageCost = fixedpoint.NewFromFloat(20000.0)
|
||||||
|
position.Base = fixedpoint.NewFromFloat(1.0)
|
||||||
|
|
||||||
|
orderExecutor := NewGeneralOrderExecutor(session, "BTCUSDT", "test", "test-01", position)
|
||||||
|
|
||||||
|
activationRatio := fixedpoint.NewFromFloat(0.01)
|
||||||
|
callbackRatio := fixedpoint.NewFromFloat(0.01)
|
||||||
|
stop := &TrailingStop2{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Interval: types.Interval1m,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
CallbackRate: callbackRatio,
|
||||||
|
ActivationRatio: activationRatio,
|
||||||
|
}
|
||||||
|
stop.Bind(session, orderExecutor)
|
||||||
|
|
||||||
|
// the same price
|
||||||
|
currentPrice := fixedpoint.NewFromFloat(20000.0)
|
||||||
|
err := stop.checkStopPrice(currentPrice, position)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.False(t, stop.activated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20000 + 1% = 20200
|
||||||
|
currentPrice = currentPrice.Mul(one.Add(activationRatio))
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(20200.0), currentPrice)
|
||||||
|
|
||||||
|
err = stop.checkStopPrice(currentPrice, position)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.True(t, stop.activated)
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(20200.0), stop.latestHigh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20200 + 1% = 20402
|
||||||
|
currentPrice = currentPrice.Mul(one.Add(callbackRatio))
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(20402.0), currentPrice)
|
||||||
|
|
||||||
|
err = stop.checkStopPrice(currentPrice, position)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(20402.0), stop.latestHigh)
|
||||||
|
assert.True(t, stop.activated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20402 - 1%
|
||||||
|
currentPrice = currentPrice.Mul(one.Sub(callbackRatio))
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(20197.98), currentPrice)
|
||||||
|
|
||||||
|
err = stop.checkStopPrice(currentPrice, position)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.Equal(t, fixedpoint.Zero, stop.latestHigh)
|
||||||
|
assert.False(t, stop.activated)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,18 +3,40 @@ package bbgo
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var graceful = &Graceful{}
|
||||||
|
|
||||||
//go:generate callbackgen -type Graceful
|
//go:generate callbackgen -type Graceful
|
||||||
type Graceful struct {
|
type Graceful struct {
|
||||||
shutdownCallbacks []func(ctx context.Context, wg *sync.WaitGroup)
|
shutdownCallbacks []func(ctx context.Context, wg *sync.WaitGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown is a blocking call to emit all shutdown callbacks at the same time.
|
||||||
func (g *Graceful) Shutdown(ctx context.Context) {
|
func (g *Graceful) Shutdown(ctx context.Context) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(len(g.shutdownCallbacks))
|
wg.Add(len(g.shutdownCallbacks))
|
||||||
|
|
||||||
go g.EmitShutdown(ctx, &wg)
|
// for each shutdown callback, we give them 10 second
|
||||||
|
shtCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
|
||||||
|
go g.EmitShutdown(shtCtx, &wg)
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnShutdown(f func(ctx context.Context, wg *sync.WaitGroup)) {
|
||||||
|
graceful.OnShutdown(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Shutdown() {
|
||||||
|
logrus.Infof("shutting down...")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
|
||||||
|
graceful.Shutdown(ctx)
|
||||||
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
package bbgo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/service"
|
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_injectField(t *testing.T) {
|
|
||||||
type TT struct {
|
|
||||||
TradeService *service.TradeService
|
|
||||||
}
|
|
||||||
|
|
||||||
// only pointer object can be set.
|
|
||||||
var tt = &TT{}
|
|
||||||
|
|
||||||
// get the value of the pointer, or it can not be set.
|
|
||||||
var rv = reflect.ValueOf(tt).Elem()
|
|
||||||
|
|
||||||
_, ret := hasField(rv, "TradeService")
|
|
||||||
assert.True(t, ret)
|
|
||||||
|
|
||||||
ts := &service.TradeService{}
|
|
||||||
|
|
||||||
err := injectField(rv, "TradeService", ts, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_parseStructAndInject(t *testing.T) {
|
|
||||||
t.Run("skip nil", func(t *testing.T) {
|
|
||||||
ss := struct {
|
|
||||||
a int
|
|
||||||
Env *Environment
|
|
||||||
}{
|
|
||||||
a: 1,
|
|
||||||
Env: nil,
|
|
||||||
}
|
|
||||||
err := parseStructAndInject(&ss, nil)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Nil(t, ss.Env)
|
|
||||||
})
|
|
||||||
t.Run("pointer", func(t *testing.T) {
|
|
||||||
ss := struct {
|
|
||||||
a int
|
|
||||||
Env *Environment
|
|
||||||
}{
|
|
||||||
a: 1,
|
|
||||||
Env: nil,
|
|
||||||
}
|
|
||||||
err := parseStructAndInject(&ss, &Environment{})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, ss.Env)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("composition", func(t *testing.T) {
|
|
||||||
type TT struct {
|
|
||||||
*service.TradeService
|
|
||||||
}
|
|
||||||
ss := TT{}
|
|
||||||
err := parseStructAndInject(&ss, &service.TradeService{})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, ss.TradeService)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("struct", func(t *testing.T) {
|
|
||||||
ss := struct {
|
|
||||||
a int
|
|
||||||
Env Environment
|
|
||||||
}{
|
|
||||||
a: 1,
|
|
||||||
}
|
|
||||||
err := parseStructAndInject(&ss, Environment{
|
|
||||||
startTime: time.Now(),
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEqual(t, time.Time{}, ss.Env.startTime)
|
|
||||||
})
|
|
||||||
t.Run("interface/any", func(t *testing.T) {
|
|
||||||
ss := struct {
|
|
||||||
Any interface{} // anything
|
|
||||||
}{
|
|
||||||
Any: nil,
|
|
||||||
}
|
|
||||||
err := parseStructAndInject(&ss, &Environment{
|
|
||||||
startTime: time.Now(),
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, ss.Any)
|
|
||||||
})
|
|
||||||
t.Run("interface/stringer", func(t *testing.T) {
|
|
||||||
ss := struct {
|
|
||||||
Stringer types.Stringer // stringer interface
|
|
||||||
}{
|
|
||||||
Stringer: nil,
|
|
||||||
}
|
|
||||||
err := parseStructAndInject(&ss, &types.Trade{})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, ss.Stringer)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -53,7 +53,8 @@ func (e *GeneralOrderExecutor) BindTradeStats(tradeStats *types.TradeStats) {
|
||||||
if profit == nil {
|
if profit == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tradeStats.Add(profit.Profit)
|
|
||||||
|
tradeStats.Add(profit)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) {
|
||||||
}
|
}
|
||||||
|
|
||||||
profitStats.AddProfit(*profit)
|
profitStats.AddProfit(*profit)
|
||||||
Notify(&profitStats)
|
Notify(profitStats)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,9 +87,9 @@ func (e *GeneralOrderExecutor) Bind() {
|
||||||
e.tradeCollector.BindStream(e.session.UserDataStream)
|
e.tradeCollector.BindStream(e.session.UserDataStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CancelOrders cancels the given order objects directly
|
||||||
func (e *GeneralOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error {
|
func (e *GeneralOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error {
|
||||||
err := e.session.Exchange.CancelOrders(ctx, orders...)
|
return e.session.Exchange.CancelOrders(ctx, orders...)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) {
|
func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) {
|
||||||
|
@ -108,8 +109,9 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ..
|
||||||
return createdOrders, err
|
return createdOrders, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error {
|
// GracefulCancelActiveOrderBook cancels the orders from the active orderbook.
|
||||||
if err := e.activeMakerOrders.GracefulCancel(ctx, e.session.Exchange); err != nil {
|
func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook) error {
|
||||||
|
if err := activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil {
|
||||||
log.WithError(err).Errorf("graceful cancel order error")
|
log.WithError(err).Errorf("graceful cancel order error")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -118,6 +120,11 @@ func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GracefulCancel cancels all active maker orders
|
||||||
|
func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error {
|
||||||
|
return e.GracefulCancelActiveOrderBook(ctx, e.activeMakerOrders)
|
||||||
|
}
|
||||||
|
|
||||||
func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error {
|
func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error {
|
||||||
submitOrder := e.position.NewMarketCloseOrder(percentage)
|
submitOrder := e.position.NewMarketCloseOrder(percentage)
|
||||||
if submitOrder == nil {
|
if submitOrder == nil {
|
||||||
|
@ -125,7 +132,6 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix
|
||||||
}
|
}
|
||||||
|
|
||||||
submitOrder.Tag = strings.Join(tags, ",")
|
submitOrder.Tag = strings.Join(tags, ",")
|
||||||
|
|
||||||
_, err := e.SubmitOrders(ctx, *submitOrder)
|
_, err := e.SubmitOrders(ctx, *submitOrder)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/dynamic"
|
||||||
"github.com/c9s/bbgo/pkg/service"
|
"github.com/c9s/bbgo/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -106,10 +107,10 @@ func Sync(obj interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadPersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error {
|
func loadPersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error {
|
||||||
return iterateFieldsByTag(obj, "persistence", func(tag string, field reflect.StructField, value reflect.Value) error {
|
return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, field reflect.StructField, value reflect.Value) error {
|
||||||
log.Debugf("[loadPersistenceFields] loading value into field %v, tag = %s, original value = %v", field, tag, value)
|
log.Debugf("[loadPersistenceFields] loading value into field %v, tag = %s, original value = %v", field, tag, value)
|
||||||
|
|
||||||
newValueInf := newTypeValueInterface(value.Type())
|
newValueInf := dynamic.NewTypeValueInterface(value.Type())
|
||||||
// inf := value.Interface()
|
// inf := value.Interface()
|
||||||
store := persistence.NewStore("state", id, tag)
|
store := persistence.NewStore("state", id, tag)
|
||||||
if err := store.Load(&newValueInf); err != nil {
|
if err := store.Load(&newValueInf); err != nil {
|
||||||
|
@ -134,7 +135,7 @@ func loadPersistenceFields(obj interface{}, id string, persistence service.Persi
|
||||||
}
|
}
|
||||||
|
|
||||||
func storePersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error {
|
func storePersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error {
|
||||||
return iterateFieldsByTag(obj, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error {
|
return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error {
|
||||||
log.Debugf("[storePersistenceFields] storing value from field %v, tag = %s, original value = %v", ft, tag, fv)
|
log.Debugf("[storePersistenceFields] storing value from field %v, tag = %s, original value = %v", ft, tag, fv)
|
||||||
|
|
||||||
inf := fv.Interface()
|
inf := fv.Interface()
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/dynamic"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/service"
|
"github.com/c9s/bbgo/pkg/service"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
@ -23,7 +24,6 @@ func (s *TestStructWithoutInstanceID) ID() string {
|
||||||
|
|
||||||
type TestStruct struct {
|
type TestStruct struct {
|
||||||
*Environment
|
*Environment
|
||||||
*Graceful
|
|
||||||
|
|
||||||
Position *types.Position `persistence:"position"`
|
Position *types.Position `persistence:"position"`
|
||||||
Integer int64 `persistence:"integer"`
|
Integer int64 `persistence:"integer"`
|
||||||
|
@ -83,7 +83,7 @@ func Test_loadPersistenceFields(t *testing.T) {
|
||||||
t.Run(psName+"/nil", func(t *testing.T) {
|
t.Run(psName+"/nil", func(t *testing.T) {
|
||||||
var b *TestStruct = nil
|
var b *TestStruct = nil
|
||||||
err := loadPersistenceFields(b, "test-nil", ps)
|
err := loadPersistenceFields(b, "test-nil", ps)
|
||||||
assert.Equal(t, errCanNotIterateNilPointer, err)
|
assert.Equal(t, dynamic.ErrCanNotIterateNilPointer, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run(psName+"/pointer-field", func(t *testing.T) {
|
t.Run(psName+"/pointer-field", func(t *testing.T) {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package bbgo
|
package bbgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/dynamic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InstanceIDProvider interface {
|
type InstanceIDProvider interface {
|
||||||
|
@ -19,7 +19,7 @@ func callID(obj interface{}) string {
|
||||||
return ret[0].String()
|
return ret[0].String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if symbol, ok := isSymbolBasedStrategy(sv); ok {
|
if symbol, ok := dynamic.LookupSymbolField(sv); ok {
|
||||||
m := sv.MethodByName("ID")
|
m := sv.MethodByName("ID")
|
||||||
ret := m.Call(nil)
|
ret := m.Call(nil)
|
||||||
return ret[0].String() + ":" + symbol
|
return ret[0].String() + ":" + symbol
|
||||||
|
@ -31,82 +31,3 @@ func callID(obj interface{}) string {
|
||||||
return ret[0].String() + ":"
|
return ret[0].String() + ":"
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSymbolBasedStrategy(rs reflect.Value) (string, bool) {
|
|
||||||
if rs.Kind() == reflect.Ptr {
|
|
||||||
rs = rs.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
field := rs.FieldByName("Symbol")
|
|
||||||
if !field.IsValid() {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
if field.Kind() != reflect.String {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
return field.String(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasField(rs reflect.Value, fieldName string) (field reflect.Value, ok bool) {
|
|
||||||
field = rs.FieldByName(fieldName)
|
|
||||||
return field, field.IsValid()
|
|
||||||
}
|
|
||||||
|
|
||||||
type StructFieldIterator func(tag string, ft reflect.StructField, fv reflect.Value) error
|
|
||||||
|
|
||||||
var errCanNotIterateNilPointer = errors.New("can not iterate struct on a nil pointer")
|
|
||||||
|
|
||||||
func iterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error {
|
|
||||||
sv := reflect.ValueOf(obj)
|
|
||||||
st := reflect.TypeOf(obj)
|
|
||||||
|
|
||||||
if st.Kind() != reflect.Ptr {
|
|
||||||
return fmt.Errorf("f should be a pointer of a struct, %s given", st)
|
|
||||||
}
|
|
||||||
|
|
||||||
// for pointer, check if it's nil
|
|
||||||
if sv.IsNil() {
|
|
||||||
return errCanNotIterateNilPointer
|
|
||||||
}
|
|
||||||
|
|
||||||
// solve the reference
|
|
||||||
st = st.Elem()
|
|
||||||
sv = sv.Elem()
|
|
||||||
|
|
||||||
if st.Kind() != reflect.Struct {
|
|
||||||
return fmt.Errorf("f should be a struct, %s given", st)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < sv.NumField(); i++ {
|
|
||||||
fv := sv.Field(i)
|
|
||||||
ft := st.Field(i)
|
|
||||||
|
|
||||||
// skip unexported fields
|
|
||||||
if !st.Field(i).IsExported() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tag, ok := ft.Tag.Lookup(tagName)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cb(tag, ft, fv); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/xiaojun207/go-base-utils/blob/master/utils/Clone.go
|
|
||||||
func newTypeValueInterface(typ reflect.Type) interface{} {
|
|
||||||
if typ.Kind() == reflect.Ptr {
|
|
||||||
typ = typ.Elem()
|
|
||||||
dst := reflect.New(typ).Elem()
|
|
||||||
return dst.Addr().Interface()
|
|
||||||
}
|
|
||||||
dst := reflect.New(typ)
|
|
||||||
return dst.Interface()
|
|
||||||
}
|
|
||||||
|
|
2
pkg/bbgo/reflect_test.go
Normal file
2
pkg/bbgo/reflect_test.go
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
package bbgo
|
||||||
|
|
|
@ -160,10 +160,6 @@ func (set *StandardIndicatorSet) VOLATILITY(iw types.IntervalWindow) *indicator.
|
||||||
// ExchangeSession presents the exchange connection Session
|
// ExchangeSession presents the exchange connection Session
|
||||||
// It also maintains and collects the data returned from the stream.
|
// It also maintains and collects the data returned from the stream.
|
||||||
type ExchangeSession struct {
|
type ExchangeSession struct {
|
||||||
// exchange Session based notification system
|
|
||||||
// we make it as a value field so that we can configure it separately
|
|
||||||
Notifiability `json:"-" yaml:"-"`
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Session config fields
|
// Session config fields
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
@ -253,12 +249,6 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession {
|
||||||
marketDataStream.SetPublicOnly()
|
marketDataStream.SetPublicOnly()
|
||||||
|
|
||||||
session := &ExchangeSession{
|
session := &ExchangeSession{
|
||||||
Notifiability: Notifiability{
|
|
||||||
SymbolChannelRouter: NewPatternChannelRouter(nil),
|
|
||||||
SessionChannelRouter: NewPatternChannelRouter(nil),
|
|
||||||
ObjectChannelRouter: NewObjectChannelRouter(),
|
|
||||||
},
|
|
||||||
|
|
||||||
Name: name,
|
Name: name,
|
||||||
Exchange: exchange,
|
Exchange: exchange,
|
||||||
UserDataStream: userDataStream,
|
UserDataStream: userDataStream,
|
||||||
|
@ -282,7 +272,6 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession {
|
||||||
|
|
||||||
session.OrderExecutor = &ExchangeOrderExecutor{
|
session.OrderExecutor = &ExchangeOrderExecutor{
|
||||||
// copy the notification system so that we can route
|
// copy the notification system so that we can route
|
||||||
Notifiability: session.Notifiability,
|
|
||||||
Session: session,
|
Session: session,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -805,11 +794,6 @@ func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) err
|
||||||
}
|
}
|
||||||
|
|
||||||
session.Name = name
|
session.Name = name
|
||||||
session.Notifiability = Notifiability{
|
|
||||||
SymbolChannelRouter: NewPatternChannelRouter(nil),
|
|
||||||
SessionChannelRouter: NewPatternChannelRouter(nil),
|
|
||||||
ObjectChannelRouter: NewObjectChannelRouter(),
|
|
||||||
}
|
|
||||||
session.Exchange = ex
|
session.Exchange = ex
|
||||||
session.UserDataStream = ex.NewStream()
|
session.UserDataStream = ex.NewStream()
|
||||||
session.MarketDataStream = ex.NewStream()
|
session.MarketDataStream = ex.NewStream()
|
||||||
|
@ -830,7 +814,6 @@ func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) err
|
||||||
session.orderStores = make(map[string]*OrderStore)
|
session.orderStores = make(map[string]*OrderStore)
|
||||||
session.OrderExecutor = &ExchangeOrderExecutor{
|
session.OrderExecutor = &ExchangeOrderExecutor{
|
||||||
// copy the notification system so that we can route
|
// copy the notification system so that we can route
|
||||||
Notifiability: session.Notifiability,
|
|
||||||
Session: session,
|
Session: session,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/dynamic"
|
||||||
"github.com/c9s/bbgo/pkg/interact"
|
"github.com/c9s/bbgo/pkg/interact"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -72,8 +73,6 @@ type Trader struct {
|
||||||
exchangeStrategies map[string][]SingleExchangeStrategy
|
exchangeStrategies map[string][]SingleExchangeStrategy
|
||||||
|
|
||||||
logger Logger
|
logger Logger
|
||||||
|
|
||||||
Graceful Graceful
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTrader(environ *Environment) *Trader {
|
func NewTrader(environ *Environment) *Trader {
|
||||||
|
@ -197,11 +196,11 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := injectField(rs, "OrderExecutor", orderExecutor, false); err != nil {
|
if err := dynamic.InjectField(rs, "OrderExecutor", orderExecutor, false); err != nil {
|
||||||
return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy)
|
return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy)
|
||||||
}
|
}
|
||||||
|
|
||||||
if symbol, ok := isSymbolBasedStrategy(rs); ok {
|
if symbol, ok := dynamic.LookupSymbolField(rs); ok {
|
||||||
log.Infof("found symbol based strategy from %s", rs.Type())
|
log.Infof("found symbol based strategy from %s", rs.Type())
|
||||||
|
|
||||||
market, ok := session.Market(symbol)
|
market, ok := session.Market(symbol)
|
||||||
|
@ -219,7 +218,7 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si
|
||||||
return fmt.Errorf("marketDataStore of symbol %s not found", symbol)
|
return fmt.Errorf("marketDataStore of symbol %s not found", symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := parseStructAndInject(strategy,
|
if err := dynamic.ParseStructAndInject(strategy,
|
||||||
market,
|
market,
|
||||||
indicatorSet,
|
indicatorSet,
|
||||||
store,
|
store,
|
||||||
|
@ -394,7 +393,7 @@ func (trader *Trader) injectCommonServices(s interface{}) error {
|
||||||
// a special injection for persistence selector:
|
// a special injection for persistence selector:
|
||||||
// if user defined the selector, the facade pointer will be nil, hence we need to update the persistence facade pointer
|
// if user defined the selector, the facade pointer will be nil, hence we need to update the persistence facade pointer
|
||||||
sv := reflect.ValueOf(s).Elem()
|
sv := reflect.ValueOf(s).Elem()
|
||||||
if field, ok := hasField(sv, "Persistence"); ok {
|
if field, ok := dynamic.HasField(sv, "Persistence"); ok {
|
||||||
// the selector is set, but we need to update the facade pointer
|
// the selector is set, but we need to update the facade pointer
|
||||||
if !field.IsNil() {
|
if !field.IsNil() {
|
||||||
elem := field.Elem()
|
elem := field.Elem()
|
||||||
|
@ -402,20 +401,19 @@ func (trader *Trader) injectCommonServices(s interface{}) error {
|
||||||
return fmt.Errorf("field Persistence is not a struct element, %s given", field)
|
return fmt.Errorf("field Persistence is not a struct element, %s given", field)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := injectField(elem, "Facade", PersistenceServiceFacade, true); err != nil {
|
if err := dynamic.InjectField(elem, "Facade", PersistenceServiceFacade, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
if err := parseStructAndInject(field.Interface(), persistenceFacade); err != nil {
|
if err := ParseStructAndInject(field.Interface(), persistenceFacade); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseStructAndInject(s,
|
return dynamic.ParseStructAndInject(s,
|
||||||
&trader.Graceful,
|
|
||||||
&trader.logger,
|
&trader.logger,
|
||||||
Notification,
|
Notification,
|
||||||
trader.environment.TradeService,
|
trader.environment.TradeService,
|
||||||
|
|
|
@ -267,6 +267,13 @@ var BacktestCmd = &cobra.Command{
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, session := range environ.Sessions() {
|
||||||
|
userDataStream := session.UserDataStream.(types.StandardStreamEmitter)
|
||||||
|
backtestEx := session.Exchange.(*backtest.Exchange)
|
||||||
|
backtestEx.MarketDataStream = session.MarketDataStream.(types.StandardStreamEmitter)
|
||||||
|
backtestEx.BindUserData(userDataStream)
|
||||||
|
}
|
||||||
|
|
||||||
trader := bbgo.NewTrader(environ)
|
trader := bbgo.NewTrader(environ)
|
||||||
if verboseCnt == 0 {
|
if verboseCnt == 0 {
|
||||||
trader.DisableLogging()
|
trader.DisableLogging()
|
||||||
|
@ -281,7 +288,7 @@ var BacktestCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
backTestIntervals := []types.Interval{types.Interval1h, types.Interval1d}
|
backTestIntervals := []types.Interval{types.Interval1h, types.Interval1d}
|
||||||
exchangeSources, err := toExchangeSources(environ.Sessions(), backTestIntervals...)
|
exchangeSources, err := toExchangeSources(environ.Sessions(), startTime, endTime, backTestIntervals...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -443,9 +450,7 @@ var BacktestCmd = &cobra.Command{
|
||||||
cmdutil.WaitForSignal(runCtx, syscall.SIGINT, syscall.SIGTERM)
|
cmdutil.WaitForSignal(runCtx, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
log.Infof("shutting down trader...")
|
log.Infof("shutting down trader...")
|
||||||
shutdownCtx, cancelShutdown := context.WithDeadline(runCtx, time.Now().Add(10*time.Second))
|
bbgo.Shutdown()
|
||||||
trader.Graceful.Shutdown(shutdownCtx)
|
|
||||||
cancelShutdown()
|
|
||||||
|
|
||||||
// put the logger back to print the pnl
|
// put the logger back to print the pnl
|
||||||
log.SetLevel(log.InfoLevel)
|
log.SetLevel(log.InfoLevel)
|
||||||
|
@ -642,14 +647,11 @@ func confirmation(s string) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, extraIntervals ...types.Interval) (exchangeSources []backtest.ExchangeDataSource, err error) {
|
func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, startTime, endTime time.Time, extraIntervals ...types.Interval) (exchangeSources []backtest.ExchangeDataSource, err error) {
|
||||||
for _, session := range sessions {
|
for _, session := range sessions {
|
||||||
exchange := session.Exchange.(*backtest.Exchange)
|
backtestEx := session.Exchange.(*backtest.Exchange)
|
||||||
exchange.UserDataStream = session.UserDataStream.(types.StandardStreamEmitter)
|
|
||||||
exchange.MarketDataStream = session.MarketDataStream.(types.StandardStreamEmitter)
|
|
||||||
exchange.InitMarketData()
|
|
||||||
|
|
||||||
c, err := exchange.SubscribeMarketData(extraIntervals...)
|
c, err := backtestEx.SubscribeMarketData(startTime, endTime, extraIntervals...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return exchangeSources, err
|
return exchangeSources, err
|
||||||
}
|
}
|
||||||
|
@ -657,7 +659,7 @@ func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, extraIntervals
|
||||||
sessionCopy := session
|
sessionCopy := session
|
||||||
exchangeSources = append(exchangeSources, backtest.ExchangeDataSource{
|
exchangeSources = append(exchangeSources, backtest.ExchangeDataSource{
|
||||||
C: c,
|
C: c,
|
||||||
Exchange: exchange,
|
Exchange: backtestEx,
|
||||||
Session: sessionCopy,
|
Session: sessionCopy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,16 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/data/tsv"
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/optimizer"
|
"github.com/c9s/bbgo/pkg/optimizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,6 +21,7 @@ func init() {
|
||||||
optimizeCmd.Flags().String("optimizer-config", "optimizer.yaml", "config file")
|
optimizeCmd.Flags().String("optimizer-config", "optimizer.yaml", "config file")
|
||||||
optimizeCmd.Flags().String("output", "output", "backtest report output directory")
|
optimizeCmd.Flags().String("output", "output", "backtest report output directory")
|
||||||
optimizeCmd.Flags().Bool("json", false, "print optimizer metrics in json format")
|
optimizeCmd.Flags().Bool("json", false, "print optimizer metrics in json format")
|
||||||
|
optimizeCmd.Flags().Bool("tsv", false, "print optimizer metrics in csv format")
|
||||||
RootCmd.AddCommand(optimizeCmd)
|
RootCmd.AddCommand(optimizeCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +48,11 @@ var optimizeCmd = &cobra.Command{
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printTsvFormat, err := cmd.Flags().GetBool("tsv")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
outputDirectory, err := cmd.Flags().GetString("output")
|
outputDirectory, err := cmd.Flags().GetString("output")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -104,6 +114,10 @@ var optimizeCmd = &cobra.Command{
|
||||||
|
|
||||||
// print metrics JSON to stdout
|
// print metrics JSON to stdout
|
||||||
fmt.Println(string(out))
|
fmt.Println(string(out))
|
||||||
|
} else if printTsvFormat {
|
||||||
|
if err := formatMetricsTsv(metrics, os.Stdout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for n, values := range metrics {
|
for n, values := range metrics {
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
|
@ -120,3 +134,95 @@ var optimizeCmd = &cobra.Command{
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func transformMetricsToRows(metrics map[string][]optimizer.Metric) (headers []string, rows [][]interface{}) {
|
||||||
|
var metricsKeys []string
|
||||||
|
for k := range metrics {
|
||||||
|
metricsKeys = append(metricsKeys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
var numEntries int
|
||||||
|
var paramLabels []string
|
||||||
|
for _, ms := range metrics {
|
||||||
|
for _, m := range ms {
|
||||||
|
paramLabels = m.Labels
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
numEntries = len(ms)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = append(paramLabels, metricsKeys...)
|
||||||
|
rows = make([][]interface{}, numEntries)
|
||||||
|
|
||||||
|
var metricsRows = make([][]interface{}, numEntries)
|
||||||
|
|
||||||
|
// build params into the rows
|
||||||
|
for i, m := range metrics[metricsKeys[0]] {
|
||||||
|
rows[i] = m.Params
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, metricKey := range metricsKeys {
|
||||||
|
for i, ms := range metrics[metricKey] {
|
||||||
|
if len(metricsRows[i]) == 0 {
|
||||||
|
metricsRows[i] = make([]interface{}, 0, len(metricsKeys))
|
||||||
|
}
|
||||||
|
metricsRows[i] = append(metricsRows[i], ms.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge rows
|
||||||
|
for i := range rows {
|
||||||
|
rows[i] = append(rows[i], metricsRows[i]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers, rows
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMetricsTsv(metrics map[string][]optimizer.Metric, writer io.WriteCloser) error {
|
||||||
|
headers, rows := transformMetricsToRows(metrics)
|
||||||
|
w := tsv.NewWriter(writer)
|
||||||
|
if err := w.Write(headers); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
var cells []string
|
||||||
|
for _, o := range row {
|
||||||
|
cell, err := castCellValue(o)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cells = append(cells, cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.Write(cells); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func castCellValue(a interface{}) (string, error) {
|
||||||
|
switch tv := a.(type) {
|
||||||
|
case fixedpoint.Value:
|
||||||
|
return tv.String(), nil
|
||||||
|
case float64:
|
||||||
|
return strconv.FormatFloat(tv, 'f', -1, 64), nil
|
||||||
|
case int64:
|
||||||
|
return strconv.FormatInt(tv, 10), nil
|
||||||
|
case int32:
|
||||||
|
return strconv.FormatInt(int64(tv), 10), nil
|
||||||
|
case int:
|
||||||
|
return strconv.Itoa(tv), nil
|
||||||
|
case bool:
|
||||||
|
return strconv.FormatBool(tv), nil
|
||||||
|
case string:
|
||||||
|
return tv, nil
|
||||||
|
case []byte:
|
||||||
|
return string(tv), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported object type: %T value: %v", tv, tv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -184,9 +184,15 @@ var PnLCmd = &cobra.Command{
|
||||||
return errors.New("no ticker data for current price")
|
return errors.New("no ticker data for current price")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
market, ok := session.Market(symbol)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name())
|
||||||
|
}
|
||||||
|
|
||||||
currentPrice := currentTick.Last
|
currentPrice := currentTick.Last
|
||||||
calculator := &pnl.AverageCostCalculator{
|
calculator := &pnl.AverageCostCalculator{
|
||||||
TradingFeeCurrency: tradingFeeCurrency,
|
TradingFeeCurrency: tradingFeeCurrency,
|
||||||
|
Market: market,
|
||||||
}
|
}
|
||||||
|
|
||||||
report := calculator.Calculate(symbol, trades, currentPrice)
|
report := calculator.Calculate(symbol, trades, currentPrice)
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -78,12 +77,7 @@ func runSetup(baseCtx context.Context, userConfig *bbgo.Config, enableApiServer
|
||||||
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
|
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||||
cancelTrading()
|
cancelTrading()
|
||||||
|
|
||||||
// graceful period = 15 second
|
bbgo.Shutdown()
|
||||||
shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(15*time.Second))
|
|
||||||
|
|
||||||
log.Infof("shutting down...")
|
|
||||||
trader.Graceful.Shutdown(shutdownCtx)
|
|
||||||
cancelShutdown()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,10 +210,7 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con
|
||||||
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
|
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||||
cancelTrading()
|
cancelTrading()
|
||||||
|
|
||||||
log.Infof("shutting down...")
|
bbgo.Shutdown()
|
||||||
shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(30*time.Second))
|
|
||||||
trader.Graceful.Shutdown(shutdownCtx)
|
|
||||||
cancelShutdown()
|
|
||||||
|
|
||||||
if err := trader.SaveState(); err != nil {
|
if err := trader.SaveState(); err != nil {
|
||||||
log.WithError(err).Errorf("can not save strategy states")
|
log.WithError(err).Errorf("can not save strategy states")
|
||||||
|
|
155
pkg/dynamic/call.go
Normal file
155
pkg/dynamic/call.go
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CallStructFieldsMethod iterates field from the given struct object
|
||||||
|
// check if the field object implements the interface, if it's implemented, then we call a specific method
|
||||||
|
func CallStructFieldsMethod(m interface{}, method string, args ...interface{}) error {
|
||||||
|
rv := reflect.ValueOf(m)
|
||||||
|
rt := reflect.TypeOf(m)
|
||||||
|
|
||||||
|
if rt.Kind() != reflect.Ptr {
|
||||||
|
return errors.New("the given object needs to be a pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
rv = rv.Elem()
|
||||||
|
rt = rt.Elem()
|
||||||
|
|
||||||
|
if rt.Kind() != reflect.Struct {
|
||||||
|
return errors.New("the given object needs to be struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
argValues := ToReflectValues(args...)
|
||||||
|
for i := 0; i < rt.NumField(); i++ {
|
||||||
|
fieldType := rt.Field(i)
|
||||||
|
fieldValue := rv.Field(i)
|
||||||
|
|
||||||
|
// skip non-exported fields
|
||||||
|
if !fieldType.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldType.Type.Kind() == reflect.Ptr && fieldValue.IsNil() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
methodType, ok := fieldType.Type.MethodByName(method)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(argValues) < methodType.Type.NumIn() {
|
||||||
|
// return fmt.Errorf("method %v require %d args, %d given", methodType, methodType.Type.NumIn(), len(argValues))
|
||||||
|
}
|
||||||
|
|
||||||
|
refMethod := fieldValue.MethodByName(method)
|
||||||
|
refMethod.Call(argValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallMatch calls the function with the matched argument automatically
|
||||||
|
// you can define multiple parameter factory function to inject the return value as the function argument.
|
||||||
|
// e.g.,
|
||||||
|
// CallMatch(targetFunction, 1, 10, true, func() *ParamType { .... })
|
||||||
|
//
|
||||||
|
func CallMatch(f interface{}, objects ...interface{}) ([]reflect.Value, error) {
|
||||||
|
fv := reflect.ValueOf(f)
|
||||||
|
ft := reflect.TypeOf(f)
|
||||||
|
|
||||||
|
var startIndex = 0
|
||||||
|
var fArgs []reflect.Value
|
||||||
|
|
||||||
|
var factoryParams = findFactoryParams(objects...)
|
||||||
|
|
||||||
|
nextDynamicInputArg:
|
||||||
|
for i := 0; i < ft.NumIn(); i++ {
|
||||||
|
at := ft.In(i)
|
||||||
|
|
||||||
|
// uat == underlying argument type
|
||||||
|
uat := at
|
||||||
|
if at.Kind() == reflect.Ptr {
|
||||||
|
uat = at.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
for oi := startIndex; oi < len(objects); oi++ {
|
||||||
|
var obj = objects[oi]
|
||||||
|
var objT = reflect.TypeOf(obj)
|
||||||
|
if objT == at {
|
||||||
|
fArgs = append(fArgs, reflect.ValueOf(obj))
|
||||||
|
startIndex = oi + 1
|
||||||
|
continue nextDynamicInputArg
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the kind of argument
|
||||||
|
switch k := uat.Kind(); k {
|
||||||
|
|
||||||
|
case reflect.Interface:
|
||||||
|
if objT.Implements(at) {
|
||||||
|
fArgs = append(fArgs, reflect.ValueOf(obj))
|
||||||
|
startIndex = oi + 1
|
||||||
|
continue nextDynamicInputArg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// factory param can be reused
|
||||||
|
for _, fp := range factoryParams {
|
||||||
|
fpt := fp.Type()
|
||||||
|
outType := fpt.Out(0)
|
||||||
|
if outType == at {
|
||||||
|
fOut := fp.Call(nil)
|
||||||
|
fArgs = append(fArgs, fOut[0])
|
||||||
|
continue nextDynamicInputArg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fArgs = append(fArgs, reflect.Zero(at))
|
||||||
|
}
|
||||||
|
|
||||||
|
out := fv.Call(fArgs)
|
||||||
|
if ft.NumOut() == 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to get the error object from the return value (if any)
|
||||||
|
var err error
|
||||||
|
for i := 0; i < ft.NumOut(); i++ {
|
||||||
|
outType := ft.Out(i)
|
||||||
|
switch outType.Kind() {
|
||||||
|
case reflect.Interface:
|
||||||
|
o := out[i].Interface()
|
||||||
|
switch ov := o.(type) {
|
||||||
|
case error:
|
||||||
|
err = ov
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func findFactoryParams(objs ...interface{}) (fs []reflect.Value) {
|
||||||
|
for i := range objs {
|
||||||
|
obj := objs[i]
|
||||||
|
|
||||||
|
objT := reflect.TypeOf(obj)
|
||||||
|
|
||||||
|
if objT.Kind() != reflect.Func {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if objT.NumOut() == 0 || objT.NumIn() > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fs = append(fs, reflect.ValueOf(obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}
|
115
pkg/dynamic/call_test.go
Normal file
115
pkg/dynamic/call_test.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type callTest struct {
|
||||||
|
ChildCall1 *childCall1
|
||||||
|
ChildCall2 *childCall2
|
||||||
|
}
|
||||||
|
|
||||||
|
type childCall1 struct{}
|
||||||
|
|
||||||
|
func (c *childCall1) Subscribe(a int) {}
|
||||||
|
|
||||||
|
type childCall2 struct{}
|
||||||
|
|
||||||
|
func (c *childCall2) Subscribe(a int) {}
|
||||||
|
|
||||||
|
func TestCallStructFieldsMethod(t *testing.T) {
|
||||||
|
c := &callTest{
|
||||||
|
ChildCall1: &childCall1{},
|
||||||
|
ChildCall2: &childCall2{},
|
||||||
|
}
|
||||||
|
err := CallStructFieldsMethod(c, "Subscribe", 10)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type S struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S) String() string { return s.ID }
|
||||||
|
|
||||||
|
func TestCallMatch(t *testing.T) {
|
||||||
|
t.Run("simple", func(t *testing.T) {
|
||||||
|
f := func(a int, b int) {
|
||||||
|
assert.Equal(t, 1, a)
|
||||||
|
assert.Equal(t, 2, b)
|
||||||
|
}
|
||||||
|
_, err := CallMatch(f, 1, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("interface", func(t *testing.T) {
|
||||||
|
type A interface {
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
f := func(foo int, a A) {
|
||||||
|
assert.Equal(t, "foo", a.String())
|
||||||
|
}
|
||||||
|
_, err := CallMatch(f, 10, &S{ID: "foo"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil interface", func(t *testing.T) {
|
||||||
|
type A interface {
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
f := func(foo int, a A) {
|
||||||
|
assert.Equal(t, 10, foo)
|
||||||
|
assert.Nil(t, a)
|
||||||
|
}
|
||||||
|
_, err := CallMatch(f, 10)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("struct pointer", func(t *testing.T) {
|
||||||
|
f := func(foo int, s *S) {
|
||||||
|
assert.Equal(t, 10, foo)
|
||||||
|
assert.NotNil(t, s)
|
||||||
|
}
|
||||||
|
_, err := CallMatch(f, 10, &S{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("struct pointer x 2", func(t *testing.T) {
|
||||||
|
f := func(foo int, s1, s2 *S) {
|
||||||
|
assert.Equal(t, 10, foo)
|
||||||
|
assert.Equal(t, "s1", s1.String())
|
||||||
|
assert.Equal(t, "s2", s2.String())
|
||||||
|
}
|
||||||
|
_, err := CallMatch(f, 10, &S{ID: "s1"}, &S{ID: "s2"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("func factory", func(t *testing.T) {
|
||||||
|
f := func(s *S) {
|
||||||
|
assert.Equal(t, "factory", s.String())
|
||||||
|
}
|
||||||
|
_, err := CallMatch(f, func() *S {
|
||||||
|
return &S{ID: "factory"}
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
f := func(s *S) {
|
||||||
|
assert.Nil(t, s)
|
||||||
|
}
|
||||||
|
_, err := CallMatch(f)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero struct", func(t *testing.T) {
|
||||||
|
f := func(s S) {
|
||||||
|
assert.Equal(t, S{}, s)
|
||||||
|
}
|
||||||
|
_, err := CallMatch(f)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
26
pkg/dynamic/field.go
Normal file
26
pkg/dynamic/field.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
func HasField(rs reflect.Value, fieldName string) (field reflect.Value, ok bool) {
|
||||||
|
field = rs.FieldByName(fieldName)
|
||||||
|
return field, field.IsValid()
|
||||||
|
}
|
||||||
|
|
||||||
|
func LookupSymbolField(rs reflect.Value) (string, bool) {
|
||||||
|
if rs.Kind() == reflect.Ptr {
|
||||||
|
rs = rs.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
field := rs.FieldByName("Symbol")
|
||||||
|
if !field.IsValid() {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Kind() != reflect.String {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.String(), true
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
package bbgo
|
package dynamic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/service"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func injectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error {
|
type testEnvironment struct {
|
||||||
|
startTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func InjectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error {
|
||||||
field := rs.FieldByName(fieldName)
|
field := rs.FieldByName(fieldName)
|
||||||
if !field.IsValid() {
|
if !field.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
|
@ -38,10 +48,10 @@ func injectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnl
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseStructAndInject parses the struct fields and injects the objects into the corresponding fields by its type.
|
// ParseStructAndInject parses the struct fields and injects the objects into the corresponding fields by its type.
|
||||||
// if the given object is a reference of an object, the type of the target field MUST BE a pointer field.
|
// if the given object is a reference of an object, the type of the target field MUST BE a pointer field.
|
||||||
// if the given object is a struct value, the type of the target field CAN BE a pointer field or a struct value field.
|
// if the given object is a struct value, the type of the target field CAN BE a pointer field or a struct value field.
|
||||||
func parseStructAndInject(f interface{}, objects ...interface{}) error {
|
func ParseStructAndInject(f interface{}, objects ...interface{}) error {
|
||||||
sv := reflect.ValueOf(f)
|
sv := reflect.ValueOf(f)
|
||||||
st := reflect.TypeOf(f)
|
st := reflect.TypeOf(f)
|
||||||
|
|
||||||
|
@ -121,3 +131,96 @@ func parseStructAndInject(f interface{}, objects ...interface{}) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_injectField(t *testing.T) {
|
||||||
|
type TT struct {
|
||||||
|
TradeService *service.TradeService
|
||||||
|
}
|
||||||
|
|
||||||
|
// only pointer object can be set.
|
||||||
|
var tt = &TT{}
|
||||||
|
|
||||||
|
// get the value of the pointer, or it can not be set.
|
||||||
|
var rv = reflect.ValueOf(tt).Elem()
|
||||||
|
|
||||||
|
_, ret := HasField(rv, "TradeService")
|
||||||
|
assert.True(t, ret)
|
||||||
|
|
||||||
|
ts := &service.TradeService{}
|
||||||
|
|
||||||
|
err := InjectField(rv, "TradeService", ts, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_parseStructAndInject(t *testing.T) {
|
||||||
|
t.Run("skip nil", func(t *testing.T) {
|
||||||
|
ss := struct {
|
||||||
|
a int
|
||||||
|
Env *testEnvironment
|
||||||
|
}{
|
||||||
|
a: 1,
|
||||||
|
Env: nil,
|
||||||
|
}
|
||||||
|
err := ParseStructAndInject(&ss, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, ss.Env)
|
||||||
|
})
|
||||||
|
t.Run("pointer", func(t *testing.T) {
|
||||||
|
ss := struct {
|
||||||
|
a int
|
||||||
|
Env *testEnvironment
|
||||||
|
}{
|
||||||
|
a: 1,
|
||||||
|
Env: nil,
|
||||||
|
}
|
||||||
|
err := ParseStructAndInject(&ss, &testEnvironment{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, ss.Env)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("composition", func(t *testing.T) {
|
||||||
|
type TT struct {
|
||||||
|
*service.TradeService
|
||||||
|
}
|
||||||
|
ss := TT{}
|
||||||
|
err := ParseStructAndInject(&ss, &service.TradeService{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, ss.TradeService)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("struct", func(t *testing.T) {
|
||||||
|
ss := struct {
|
||||||
|
a int
|
||||||
|
Env testEnvironment
|
||||||
|
}{
|
||||||
|
a: 1,
|
||||||
|
}
|
||||||
|
err := ParseStructAndInject(&ss, testEnvironment{
|
||||||
|
startTime: time.Now(),
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEqual(t, time.Time{}, ss.Env.startTime)
|
||||||
|
})
|
||||||
|
t.Run("interface/any", func(t *testing.T) {
|
||||||
|
ss := struct {
|
||||||
|
Any interface{} // anything
|
||||||
|
}{
|
||||||
|
Any: nil,
|
||||||
|
}
|
||||||
|
err := ParseStructAndInject(&ss, &testEnvironment{
|
||||||
|
startTime: time.Now(),
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, ss.Any)
|
||||||
|
})
|
||||||
|
t.Run("interface/stringer", func(t *testing.T) {
|
||||||
|
ss := struct {
|
||||||
|
Stringer types.Stringer // stringer interface
|
||||||
|
}{
|
||||||
|
Stringer: nil,
|
||||||
|
}
|
||||||
|
err := ParseStructAndInject(&ss, &types.Trade{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, ss.Stringer)
|
||||||
|
})
|
||||||
|
}
|
96
pkg/dynamic/iterate.go
Normal file
96
pkg/dynamic/iterate.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StructFieldIterator func(tag string, ft reflect.StructField, fv reflect.Value) error
|
||||||
|
|
||||||
|
var ErrCanNotIterateNilPointer = errors.New("can not iterate struct on a nil pointer")
|
||||||
|
|
||||||
|
func IterateFields(obj interface{}, cb func(ft reflect.StructField, fv reflect.Value) error) error {
|
||||||
|
if obj == nil {
|
||||||
|
return errors.New("can not iterate field, given object is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
sv := reflect.ValueOf(obj)
|
||||||
|
st := reflect.TypeOf(obj)
|
||||||
|
|
||||||
|
if st.Kind() != reflect.Ptr {
|
||||||
|
return fmt.Errorf("f should be a pointer of a struct, %s given", st)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for pointer, check if it's nil
|
||||||
|
if sv.IsNil() {
|
||||||
|
return ErrCanNotIterateNilPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
// solve the reference
|
||||||
|
st = st.Elem()
|
||||||
|
sv = sv.Elem()
|
||||||
|
|
||||||
|
if st.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("f should be a struct, %s given", st)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < sv.NumField(); i++ {
|
||||||
|
fv := sv.Field(i)
|
||||||
|
ft := st.Field(i)
|
||||||
|
|
||||||
|
// skip unexported fields
|
||||||
|
if !st.Field(i).IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cb(ft, fv) ; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error {
|
||||||
|
sv := reflect.ValueOf(obj)
|
||||||
|
st := reflect.TypeOf(obj)
|
||||||
|
|
||||||
|
if st.Kind() != reflect.Ptr {
|
||||||
|
return fmt.Errorf("f should be a pointer of a struct, %s given", st)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for pointer, check if it's nil
|
||||||
|
if sv.IsNil() {
|
||||||
|
return ErrCanNotIterateNilPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
// solve the reference
|
||||||
|
st = st.Elem()
|
||||||
|
sv = sv.Elem()
|
||||||
|
|
||||||
|
if st.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("f should be a struct, %s given", st)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < sv.NumField(); i++ {
|
||||||
|
fv := sv.Field(i)
|
||||||
|
ft := st.Field(i)
|
||||||
|
|
||||||
|
// skip unexported fields
|
||||||
|
if !st.Field(i).IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tag, ok := ft.Tag.Lookup(tagName)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cb(tag, ft, fv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
44
pkg/dynamic/iterate_test.go
Normal file
44
pkg/dynamic/iterate_test.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIterateFields(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("basic", func(t *testing.T) {
|
||||||
|
var a = struct {
|
||||||
|
A int
|
||||||
|
B float64
|
||||||
|
C *os.File
|
||||||
|
}{}
|
||||||
|
|
||||||
|
cnt := 0
|
||||||
|
err := IterateFields(&a, func(ft reflect.StructField, fv reflect.Value) error {
|
||||||
|
cnt++
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 3, cnt)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-ptr", func(t *testing.T) {
|
||||||
|
err := IterateFields(struct{}{}, func(ft reflect.StructField, fv reflect.Value) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
err := IterateFields(nil, func(ft reflect.StructField, fv reflect.Value) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
41
pkg/dynamic/merge.go
Normal file
41
pkg/dynamic/merge.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
// InheritStructValues merges the field value from the source struct to the dest struct.
|
||||||
|
// Only fields with the same type and the same name will be updated.
|
||||||
|
func InheritStructValues(dst, src interface{}) {
|
||||||
|
if dst == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rtA := reflect.TypeOf(dst)
|
||||||
|
srcStructType := reflect.TypeOf(src)
|
||||||
|
|
||||||
|
rtA = rtA.Elem()
|
||||||
|
srcStructType = srcStructType.Elem()
|
||||||
|
|
||||||
|
for i := 0; i < rtA.NumField(); i++ {
|
||||||
|
fieldType := rtA.Field(i)
|
||||||
|
fieldName := fieldType.Name
|
||||||
|
|
||||||
|
if !fieldType.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a field with the same name
|
||||||
|
fieldSrcType, found := srcStructType.FieldByName(fieldName)
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that the type is the same
|
||||||
|
if fieldSrcType.Type == fieldType.Type {
|
||||||
|
srcValue := reflect.ValueOf(src).Elem().FieldByName(fieldName)
|
||||||
|
dstValue := reflect.ValueOf(dst).Elem().FieldByName(fieldName)
|
||||||
|
if (fieldType.Type.Kind() == reflect.Ptr && dstValue.IsNil()) || dstValue.IsZero() {
|
||||||
|
dstValue.Set(srcValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
pkg/dynamic/merge_test.go
Normal file
82
pkg/dynamic/merge_test.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestStrategy struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Interval string `json:"interval"`
|
||||||
|
BaseQuantity fixedpoint.Value `json:"baseQuantity"`
|
||||||
|
MaxAssetQuantity fixedpoint.Value `json:"maxAssetQuantity"`
|
||||||
|
MinDropPercentage fixedpoint.Value `json:"minDropPercentage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_reflectMergeStructFields(t *testing.T) {
|
||||||
|
t.Run("zero value", func(t *testing.T) {
|
||||||
|
a := &TestStrategy{Symbol: "BTCUSDT"}
|
||||||
|
b := &struct{ Symbol string }{Symbol: ""}
|
||||||
|
InheritStructValues(b, a)
|
||||||
|
assert.Equal(t, "BTCUSDT", b.Symbol)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-zero value", func(t *testing.T) {
|
||||||
|
a := &TestStrategy{Symbol: "BTCUSDT"}
|
||||||
|
b := &struct{ Symbol string }{Symbol: "ETHUSDT"}
|
||||||
|
InheritStructValues(b, a)
|
||||||
|
assert.Equal(t, "ETHUSDT", b.Symbol, "should be the original value")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero embedded struct", func(t *testing.T) {
|
||||||
|
iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30}
|
||||||
|
a := &struct {
|
||||||
|
types.IntervalWindow
|
||||||
|
Symbol string
|
||||||
|
}{
|
||||||
|
IntervalWindow: iw,
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
}
|
||||||
|
b := &struct {
|
||||||
|
Symbol string
|
||||||
|
types.IntervalWindow
|
||||||
|
}{}
|
||||||
|
InheritStructValues(b, a)
|
||||||
|
assert.Equal(t, iw, b.IntervalWindow)
|
||||||
|
assert.Equal(t, "BTCUSDT", b.Symbol)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-zero embedded struct", func(t *testing.T) {
|
||||||
|
iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30}
|
||||||
|
a := &struct {
|
||||||
|
types.IntervalWindow
|
||||||
|
}{
|
||||||
|
IntervalWindow: iw,
|
||||||
|
}
|
||||||
|
b := &struct {
|
||||||
|
types.IntervalWindow
|
||||||
|
}{
|
||||||
|
IntervalWindow: types.IntervalWindow{Interval: types.Interval5m, Window: 9},
|
||||||
|
}
|
||||||
|
InheritStructValues(b, a)
|
||||||
|
assert.Equal(t, types.IntervalWindow{Interval: types.Interval5m, Window: 9}, b.IntervalWindow)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("skip different type but the same name", func(t *testing.T) {
|
||||||
|
a := &struct {
|
||||||
|
A float64
|
||||||
|
}{
|
||||||
|
A: 1.99,
|
||||||
|
}
|
||||||
|
b := &struct {
|
||||||
|
A string
|
||||||
|
}{}
|
||||||
|
InheritStructValues(b, a)
|
||||||
|
assert.Equal(t, "", b.A)
|
||||||
|
assert.Equal(t, 1.99, a.A)
|
||||||
|
})
|
||||||
|
}
|
24
pkg/dynamic/typevalue.go
Normal file
24
pkg/dynamic/typevalue.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
// https://github.com/xiaojun207/go-base-utils/blob/master/utils/Clone.go
|
||||||
|
func NewTypeValueInterface(typ reflect.Type) interface{} {
|
||||||
|
if typ.Kind() == reflect.Ptr {
|
||||||
|
typ = typ.Elem()
|
||||||
|
dst := reflect.New(typ).Elem()
|
||||||
|
return dst.Addr().Interface()
|
||||||
|
}
|
||||||
|
dst := reflect.New(typ)
|
||||||
|
return dst.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToReflectValues convert the go objects into reflect.Value slice
|
||||||
|
func ToReflectValues(args ...interface{}) (values []reflect.Value) {
|
||||||
|
for i := range args {
|
||||||
|
arg := args[i]
|
||||||
|
values = append(values, reflect.ValueOf(arg))
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ Accumulation/Distribution Indicator (A/D)
|
||||||
*/
|
*/
|
||||||
//go:generate callbackgen -type AD
|
//go:generate callbackgen -type AD
|
||||||
type AD struct {
|
type AD struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
PrePrice float64
|
PrePrice float64
|
||||||
|
@ -23,6 +24,9 @@ type AD struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *AD) Update(high, low, cloze, volume float64) {
|
func (inc *AD) Update(high, low, cloze, volume float64) {
|
||||||
|
if len(inc.Values) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
|
}
|
||||||
var moneyFlowVolume float64
|
var moneyFlowVolume float64
|
||||||
if high == low {
|
if high == low {
|
||||||
moneyFlowVolume = 0
|
moneyFlowVolume = 0
|
||||||
|
@ -53,7 +57,7 @@ func (inc *AD) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &AD{}
|
var _ types.SeriesExtend = &AD{}
|
||||||
|
|
||||||
func (inc *AD) calculateAndUpdate(kLines []types.KLine) {
|
func (inc *AD) calculateAndUpdate(kLines []types.KLine) {
|
||||||
for _, k := range kLines {
|
for _, k := range kLines {
|
||||||
|
|
|
@ -13,11 +13,12 @@ import (
|
||||||
// @param sigma: the standard deviation applied to the combo line. This makes the combo line sharper
|
// @param sigma: the standard deviation applied to the combo line. This makes the combo line sharper
|
||||||
//go:generate callbackgen -type ALMA
|
//go:generate callbackgen -type ALMA
|
||||||
type ALMA struct {
|
type ALMA struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow // required
|
types.IntervalWindow // required
|
||||||
Offset float64 // required: recommend to be 5
|
Offset float64 // required: recommend to be 5
|
||||||
Sigma int // required: recommend to be 0.5
|
Sigma int // required: recommend to be 0.5
|
||||||
Weight []float64
|
weight []float64
|
||||||
Sum float64
|
sum float64
|
||||||
input []float64
|
input []float64
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
UpdateCallbacks []func(value float64)
|
UpdateCallbacks []func(value float64)
|
||||||
|
@ -27,16 +28,17 @@ const MaxNumOfALMA = 5_000
|
||||||
const MaxNumOfALMATruncateSize = 100
|
const MaxNumOfALMATruncateSize = 100
|
||||||
|
|
||||||
func (inc *ALMA) Update(value float64) {
|
func (inc *ALMA) Update(value float64) {
|
||||||
if inc.Weight == nil {
|
if inc.weight == nil {
|
||||||
inc.Weight = make([]float64, inc.Window)
|
inc.SeriesBase.Series = inc
|
||||||
|
inc.weight = make([]float64, inc.Window)
|
||||||
m := inc.Offset * (float64(inc.Window) - 1.)
|
m := inc.Offset * (float64(inc.Window) - 1.)
|
||||||
s := float64(inc.Window) / float64(inc.Sigma)
|
s := float64(inc.Window) / float64(inc.Sigma)
|
||||||
inc.Sum = 0.
|
inc.sum = 0.
|
||||||
for i := 0; i < inc.Window; i++ {
|
for i := 0; i < inc.Window; i++ {
|
||||||
diff := float64(i) - m
|
diff := float64(i) - m
|
||||||
wt := math.Exp(-diff * diff / 2. / s / s)
|
wt := math.Exp(-diff * diff / 2. / s / s)
|
||||||
inc.Sum += wt
|
inc.sum += wt
|
||||||
inc.Weight[i] = wt
|
inc.weight[i] = wt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inc.input = append(inc.input, value)
|
inc.input = append(inc.input, value)
|
||||||
|
@ -44,9 +46,9 @@ func (inc *ALMA) Update(value float64) {
|
||||||
weightedSum := 0.0
|
weightedSum := 0.0
|
||||||
inc.input = inc.input[len(inc.input)-inc.Window:]
|
inc.input = inc.input[len(inc.input)-inc.Window:]
|
||||||
for i := 0; i < inc.Window; i++ {
|
for i := 0; i < inc.Window; i++ {
|
||||||
weightedSum += inc.Weight[inc.Window-i-1] * inc.input[i]
|
weightedSum += inc.weight[inc.Window-i-1] * inc.input[i]
|
||||||
}
|
}
|
||||||
inc.Values.Push(weightedSum / inc.Sum)
|
inc.Values.Push(weightedSum / inc.sum)
|
||||||
if len(inc.Values) > MaxNumOfALMA {
|
if len(inc.Values) > MaxNumOfALMA {
|
||||||
inc.Values = inc.Values[MaxNumOfALMATruncateSize-1:]
|
inc.Values = inc.Values[MaxNumOfALMATruncateSize-1:]
|
||||||
}
|
}
|
||||||
|
@ -71,6 +73,8 @@ func (inc *ALMA) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ types.SeriesExtend = &ALMA{}
|
||||||
|
|
||||||
func (inc *ALMA) calculateAndUpdate(allKLines []types.KLine) {
|
func (inc *ALMA) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
if inc.input == nil {
|
if inc.input == nil {
|
||||||
for _, k := range allKLines {
|
for _, k := range allKLines {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
//go:generate callbackgen -type ATR
|
//go:generate callbackgen -type ATR
|
||||||
type ATR struct {
|
type ATR struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
PercentageVolatility types.Float64Slice
|
PercentageVolatility types.Float64Slice
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ func (inc *ATR) Update(high, low, cloze float64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if inc.RMA == nil {
|
if inc.RMA == nil {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.RMA = &RMA{
|
inc.RMA = &RMA{
|
||||||
IntervalWindow: types.IntervalWindow{Window: inc.Window},
|
IntervalWindow: types.IntervalWindow{Window: inc.Window},
|
||||||
Adjust: true,
|
Adjust: true,
|
||||||
|
@ -73,7 +75,7 @@ func (inc *ATR) Length() int {
|
||||||
return inc.RMA.Length()
|
return inc.RMA.Length()
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &ATR{}
|
var _ types.SeriesExtend = &ATR{}
|
||||||
|
|
||||||
func (inc *ATR) CalculateAndUpdate(kLines []types.KLine) {
|
func (inc *ATR) CalculateAndUpdate(kLines []types.KLine) {
|
||||||
for _, k := range kLines {
|
for _, k := range kLines {
|
||||||
|
|
|
@ -41,20 +41,20 @@ type BOLL struct {
|
||||||
|
|
||||||
type BandType int
|
type BandType int
|
||||||
|
|
||||||
func (inc *BOLL) GetUpBand() types.Series {
|
func (inc *BOLL) GetUpBand() types.SeriesExtend {
|
||||||
return &inc.UpBand
|
return types.NewSeries(&inc.UpBand)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *BOLL) GetDownBand() types.Series {
|
func (inc *BOLL) GetDownBand() types.SeriesExtend {
|
||||||
return &inc.DownBand
|
return types.NewSeries(&inc.DownBand)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *BOLL) GetSMA() types.Series {
|
func (inc *BOLL) GetSMA() types.SeriesExtend {
|
||||||
return &inc.SMA
|
return types.NewSeries(&inc.SMA)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *BOLL) GetStdDev() types.Series {
|
func (inc *BOLL) GetStdDev() types.SeriesExtend {
|
||||||
return &inc.StdDev
|
return types.NewSeries(&inc.StdDev)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *BOLL) LastUpBand() float64 {
|
func (inc *BOLL) LastUpBand() float64 {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
// with modification of ddof=0 to let standard deviation to be divided by N instead of N-1
|
// with modification of ddof=0 to let standard deviation to be divided by N instead of N-1
|
||||||
//go:generate callbackgen -type CCI
|
//go:generate callbackgen -type CCI
|
||||||
type CCI struct {
|
type CCI struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Input types.Float64Slice
|
Input types.Float64Slice
|
||||||
TypicalPrice types.Float64Slice
|
TypicalPrice types.Float64Slice
|
||||||
|
@ -23,6 +24,7 @@ type CCI struct {
|
||||||
|
|
||||||
func (inc *CCI) Update(value float64) {
|
func (inc *CCI) Update(value float64) {
|
||||||
if len(inc.TypicalPrice) == 0 {
|
if len(inc.TypicalPrice) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.TypicalPrice.Push(value)
|
inc.TypicalPrice.Push(value)
|
||||||
inc.Input.Push(value)
|
inc.Input.Push(value)
|
||||||
return
|
return
|
||||||
|
@ -75,7 +77,7 @@ func (inc *CCI) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &CCI{}
|
var _ types.SeriesExtend = &CCI{}
|
||||||
|
|
||||||
var three = fixedpoint.NewFromInt(3)
|
var three = fixedpoint.NewFromInt(3)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
// Refer: https://en.wikipedia.org/wiki/Moving_average
|
// Refer: https://en.wikipedia.org/wiki/Moving_average
|
||||||
//go:generate callbackgen -type CA
|
//go:generate callbackgen -type CA
|
||||||
type CA struct {
|
type CA struct {
|
||||||
|
types.SeriesBase
|
||||||
Interval types.Interval
|
Interval types.Interval
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
length float64
|
length float64
|
||||||
|
@ -15,11 +16,15 @@ type CA struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *CA) Update(x float64) {
|
func (inc *CA) Update(x float64) {
|
||||||
|
if len(inc.Values) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
|
}
|
||||||
newVal := (inc.Values.Last()*inc.length + x) / (inc.length + 1.)
|
newVal := (inc.Values.Last()*inc.length + x) / (inc.length + 1.)
|
||||||
inc.length += 1
|
inc.length += 1
|
||||||
inc.Values.Push(newVal)
|
inc.Values.Push(newVal)
|
||||||
if len(inc.Values) > MaxNumOfEWMA {
|
if len(inc.Values) > MaxNumOfEWMA {
|
||||||
inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:]
|
inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:]
|
||||||
|
inc.length = float64(len(inc.Values))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +46,7 @@ func (inc *CA) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &CA{}
|
var _ types.SeriesExtend = &CA{}
|
||||||
|
|
||||||
func (inc *CA) calculateAndUpdate(allKLines []types.KLine) {
|
func (inc *CA) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
for _, k := range allKLines {
|
for _, k := range allKLines {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
//go:generate callbackgen -type DEMA
|
//go:generate callbackgen -type DEMA
|
||||||
type DEMA struct {
|
type DEMA struct {
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
|
types.SeriesBase
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
a1 *EWMA
|
a1 *EWMA
|
||||||
a2 *EWMA
|
a2 *EWMA
|
||||||
|
@ -19,6 +20,7 @@ type DEMA struct {
|
||||||
|
|
||||||
func (inc *DEMA) Update(value float64) {
|
func (inc *DEMA) Update(value float64) {
|
||||||
if len(inc.Values) == 0 {
|
if len(inc.Values) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.a1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
inc.a1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
||||||
inc.a2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
inc.a2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +48,7 @@ func (inc *DEMA) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &DEMA{}
|
var _ types.SeriesExtend = &DEMA{}
|
||||||
|
|
||||||
func (inc *DEMA) calculateAndUpdate(allKLines []types.KLine) {
|
func (inc *DEMA) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
if inc.a1 == nil {
|
if inc.a1 == nil {
|
||||||
|
|
|
@ -17,11 +17,11 @@ type DMI struct {
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
ADXSmoothing int
|
ADXSmoothing int
|
||||||
atr *ATR
|
atr *ATR
|
||||||
DMP types.UpdatableSeries
|
DMP types.UpdatableSeriesExtend
|
||||||
DMN types.UpdatableSeries
|
DMN types.UpdatableSeriesExtend
|
||||||
DIPlus *types.Queue
|
DIPlus *types.Queue
|
||||||
DIMinus *types.Queue
|
DIMinus *types.Queue
|
||||||
ADX types.UpdatableSeries
|
ADX types.UpdatableSeriesExtend
|
||||||
PrevHigh, PrevLow float64
|
PrevHigh, PrevLow float64
|
||||||
UpdateCallbacks []func(diplus, diminus, adx float64)
|
UpdateCallbacks []func(diplus, diminus, adx float64)
|
||||||
}
|
}
|
||||||
|
@ -71,15 +71,15 @@ func (inc *DMI) Update(high, low, cloze float64) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *DMI) GetDIPlus() types.Series {
|
func (inc *DMI) GetDIPlus() types.SeriesExtend {
|
||||||
return inc.DIPlus
|
return inc.DIPlus
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *DMI) GetDIMinus() types.Series {
|
func (inc *DMI) GetDIMinus() types.SeriesExtend {
|
||||||
return inc.DIMinus
|
return inc.DIMinus
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *DMI) GetADX() types.Series {
|
func (inc *DMI) GetADX() types.SeriesExtend {
|
||||||
return inc.ADX
|
return inc.ADX
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
// could be used in Monte Carlo Simulations
|
// could be used in Monte Carlo Simulations
|
||||||
//go:generate callbackgen -type Drift
|
//go:generate callbackgen -type Drift
|
||||||
type Drift struct {
|
type Drift struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
chng *types.Queue
|
chng *types.Queue
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
|
@ -22,6 +23,7 @@ type Drift struct {
|
||||||
|
|
||||||
func (inc *Drift) Update(value float64) {
|
func (inc *Drift) Update(value float64) {
|
||||||
if inc.chng == nil {
|
if inc.chng == nil {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.SMA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}}
|
inc.SMA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}}
|
||||||
inc.chng = types.NewQueue(inc.Window)
|
inc.chng = types.NewQueue(inc.Window)
|
||||||
inc.LastValue = value
|
inc.LastValue = value
|
||||||
|
@ -64,7 +66,7 @@ func (inc *Drift) Length() int {
|
||||||
return inc.Values.Length()
|
return inc.Values.Length()
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &Drift{}
|
var _ types.SeriesExtend = &Drift{}
|
||||||
|
|
||||||
func (inc *Drift) calculateAndUpdate(allKLines []types.KLine) {
|
func (inc *Drift) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
if inc.chng == nil {
|
if inc.chng == nil {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
//go:generate callbackgen -type EMV
|
//go:generate callbackgen -type EMV
|
||||||
type EMV struct {
|
type EMV struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
prevH float64
|
prevH float64
|
||||||
prevL float64
|
prevL float64
|
||||||
|
@ -25,6 +26,7 @@ func (inc *EMV) Update(high, low, vol float64) {
|
||||||
inc.EMVScale = DefaultEMVScale
|
inc.EMVScale = DefaultEMVScale
|
||||||
}
|
}
|
||||||
if inc.prevH == 0 || inc.Values == nil {
|
if inc.prevH == 0 || inc.Values == nil {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.prevH = high
|
inc.prevH = high
|
||||||
inc.prevL = low
|
inc.prevL = low
|
||||||
inc.Values = &SMA{IntervalWindow: inc.IntervalWindow}
|
inc.Values = &SMA{IntervalWindow: inc.IntervalWindow}
|
||||||
|
@ -59,7 +61,7 @@ func (inc *EMV) Length() int {
|
||||||
return inc.Values.Length()
|
return inc.Values.Length()
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &EMV{}
|
var _ types.SeriesExtend = &EMV{}
|
||||||
|
|
||||||
func (inc *EMV) calculateAndUpdate(allKLines []types.KLine) {
|
func (inc *EMV) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
if inc.Values == nil {
|
if inc.Values == nil {
|
||||||
|
|
|
@ -16,6 +16,7 @@ const MaxNumOfEWMATruncateSize = 100
|
||||||
//go:generate callbackgen -type EWMA
|
//go:generate callbackgen -type EWMA
|
||||||
type EWMA struct {
|
type EWMA struct {
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
|
types.SeriesBase
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
LastOpenTime time.Time
|
LastOpenTime time.Time
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ func (inc *EWMA) Update(value float64) {
|
||||||
var multiplier = 2.0 / float64(1+inc.Window)
|
var multiplier = 2.0 / float64(1+inc.Window)
|
||||||
|
|
||||||
if len(inc.Values) == 0 {
|
if len(inc.Values) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.Values.Push(value)
|
inc.Values.Push(value)
|
||||||
return
|
return
|
||||||
} else if len(inc.Values) > MaxNumOfEWMA {
|
} else if len(inc.Values) > MaxNumOfEWMA {
|
||||||
|
@ -136,4 +138,4 @@ func (inc *EWMA) Bind(updater KLineWindowUpdater) {
|
||||||
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
|
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &EWMA{}
|
var _ types.SeriesExtend = &EWMA{}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
// Refer URL: https://fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/hull-moving-average
|
// Refer URL: https://fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/hull-moving-average
|
||||||
//go:generate callbackgen -type HULL
|
//go:generate callbackgen -type HULL
|
||||||
type HULL struct {
|
type HULL struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
ma1 *EWMA
|
ma1 *EWMA
|
||||||
ma2 *EWMA
|
ma2 *EWMA
|
||||||
|
@ -20,6 +21,7 @@ type HULL struct {
|
||||||
|
|
||||||
func (inc *HULL) Update(value float64) {
|
func (inc *HULL) Update(value float64) {
|
||||||
if inc.result == nil {
|
if inc.result == nil {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.ma1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window / 2}}
|
inc.ma1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window / 2}}
|
||||||
inc.ma2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
inc.ma2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
||||||
inc.result = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, int(math.Sqrt(float64(inc.Window)))}}
|
inc.result = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, int(math.Sqrt(float64(inc.Window)))}}
|
||||||
|
@ -50,7 +52,7 @@ func (inc *HULL) Length() int {
|
||||||
return inc.result.Length()
|
return inc.result.Length()
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &HULL{}
|
var _ types.SeriesExtend = &HULL{}
|
||||||
|
|
||||||
// TODO: should we just ignore the possible overlapping?
|
// TODO: should we just ignore the possible overlapping?
|
||||||
func (inc *HULL) calculateAndUpdate(allKLines []types.KLine) {
|
func (inc *HULL) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
// 3. resistance
|
// 3. resistance
|
||||||
// of the market data, defined with series interface
|
// of the market data, defined with series interface
|
||||||
type Line struct {
|
type Line struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
start float64
|
start float64
|
||||||
end float64
|
end float64
|
||||||
|
@ -63,7 +64,7 @@ func (l *Line) SetXY2(index int, value float64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLine(startIndex int, startValue float64, endIndex int, endValue float64, interval types.Interval) *Line {
|
func NewLine(startIndex int, startValue float64, endIndex int, endValue float64, interval types.Interval) *Line {
|
||||||
return &Line{
|
line := &Line{
|
||||||
start: startValue,
|
start: startValue,
|
||||||
end: endValue,
|
end: endValue,
|
||||||
startIndex: startIndex,
|
startIndex: startIndex,
|
||||||
|
@ -71,6 +72,8 @@ func NewLine(startIndex int, startValue float64, endIndex int, endValue float64,
|
||||||
currentTime: time.Time{},
|
currentTime: time.Time{},
|
||||||
Interval: interval,
|
Interval: interval,
|
||||||
}
|
}
|
||||||
|
line.SeriesBase.Series = line
|
||||||
|
return line
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &Line{}
|
var _ types.SeriesExtend = &Line{}
|
||||||
|
|
|
@ -87,6 +87,7 @@ func (inc *MACD) Bind(updater KLineWindowUpdater) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type MACDValues struct {
|
type MACDValues struct {
|
||||||
|
types.SeriesBase
|
||||||
*MACD
|
*MACD
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,10 +110,12 @@ func (inc *MACDValues) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *MACD) MACD() types.Series {
|
func (inc *MACD) MACD() types.SeriesExtend {
|
||||||
return &MACDValues{inc}
|
out := &MACDValues{MACD: inc}
|
||||||
|
out.SeriesBase.Series = out
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *MACD) Singals() types.Series {
|
func (inc *MACD) Singals() types.SeriesExtend {
|
||||||
return &inc.SignalLine
|
return &inc.SignalLine
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ On-Balance Volume (OBV) Definition
|
||||||
*/
|
*/
|
||||||
//go:generate callbackgen -type OBV
|
//go:generate callbackgen -type OBV
|
||||||
type OBV struct {
|
type OBV struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
PrePrice float64
|
PrePrice float64
|
||||||
|
@ -24,6 +25,7 @@ type OBV struct {
|
||||||
|
|
||||||
func (inc *OBV) Update(price, volume float64) {
|
func (inc *OBV) Update(price, volume float64) {
|
||||||
if len(inc.Values) == 0 {
|
if len(inc.Values) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.PrePrice = price
|
inc.PrePrice = price
|
||||||
inc.Values.Push(volume)
|
inc.Values.Push(volume)
|
||||||
return
|
return
|
||||||
|
@ -43,6 +45,15 @@ func (inc *OBV) Last() float64 {
|
||||||
return inc.Values[len(inc.Values)-1]
|
return inc.Values[len(inc.Values)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (inc *OBV) Index(i int) float64 {
|
||||||
|
if len(inc.Values)-i <= 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
return inc.Values[len(inc.Values)-i-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ types.SeriesExtend = &OBV{}
|
||||||
|
|
||||||
func (inc *OBV) calculateAndUpdate(kLines []types.KLine) {
|
func (inc *OBV) calculateAndUpdate(kLines []types.KLine) {
|
||||||
for _, k := range kLines {
|
for _, k := range kLines {
|
||||||
if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) {
|
if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
// Refer: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html#pandas-dataframe-ewm
|
// Refer: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html#pandas-dataframe-ewm
|
||||||
//go:generate callbackgen -type RMA
|
//go:generate callbackgen -type RMA
|
||||||
type RMA struct {
|
type RMA struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
counter int
|
counter int
|
||||||
|
@ -24,6 +25,7 @@ type RMA struct {
|
||||||
func (inc *RMA) Update(x float64) {
|
func (inc *RMA) Update(x float64) {
|
||||||
lambda := 1 / float64(inc.Window)
|
lambda := 1 / float64(inc.Window)
|
||||||
if inc.counter == 0 {
|
if inc.counter == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.sum = 1
|
inc.sum = 1
|
||||||
inc.tmp = x
|
inc.tmp = x
|
||||||
} else {
|
} else {
|
||||||
|
@ -60,7 +62,7 @@ func (inc *RMA) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &RMA{}
|
var _ types.SeriesExtend = &RMA{}
|
||||||
|
|
||||||
func (inc *RMA) calculateAndUpdate(kLines []types.KLine) {
|
func (inc *RMA) calculateAndUpdate(kLines []types.KLine) {
|
||||||
for _, k := range kLines {
|
for _, k := range kLines {
|
||||||
|
|
|
@ -14,6 +14,7 @@ https://www.investopedia.com/terms/r/rsi.asp
|
||||||
*/
|
*/
|
||||||
//go:generate callbackgen -type RSI
|
//go:generate callbackgen -type RSI
|
||||||
type RSI struct {
|
type RSI struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
Prices types.Float64Slice
|
Prices types.Float64Slice
|
||||||
|
@ -25,6 +26,9 @@ type RSI struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *RSI) Update(price float64) {
|
func (inc *RSI) Update(price float64) {
|
||||||
|
if len(inc.Prices) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
|
}
|
||||||
inc.Prices.Push(price)
|
inc.Prices.Push(price)
|
||||||
|
|
||||||
if len(inc.Prices) < inc.Window+1 {
|
if len(inc.Prices) < inc.Window+1 {
|
||||||
|
@ -74,7 +78,7 @@ func (inc *RSI) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &RSI{}
|
var _ types.SeriesExtend = &RSI{}
|
||||||
|
|
||||||
func (inc *RSI) calculateAndUpdate(kLines []types.KLine) {
|
func (inc *RSI) calculateAndUpdate(kLines []types.KLine) {
|
||||||
for _, k := range kLines {
|
for _, k := range kLines {
|
||||||
|
|
|
@ -16,6 +16,7 @@ var zeroTime time.Time
|
||||||
|
|
||||||
//go:generate callbackgen -type SMA
|
//go:generate callbackgen -type SMA
|
||||||
type SMA struct {
|
type SMA struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
Cache types.Float64Slice
|
Cache types.Float64Slice
|
||||||
|
@ -44,10 +45,13 @@ func (inc *SMA) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &SMA{}
|
var _ types.SeriesExtend = &SMA{}
|
||||||
|
|
||||||
func (inc *SMA) Update(value float64) {
|
func (inc *SMA) Update(value float64) {
|
||||||
if len(inc.Cache) < inc.Window {
|
if len(inc.Cache) < inc.Window {
|
||||||
|
if len(inc.Cache) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
|
}
|
||||||
inc.Cache = append(inc.Cache, value)
|
inc.Cache = append(inc.Cache, value)
|
||||||
if len(inc.Cache) == inc.Window {
|
if len(inc.Cache) == inc.Window {
|
||||||
inc.Values = append(inc.Values, types.Mean(&inc.Cache))
|
inc.Values = append(inc.Values, types.Mean(&inc.Cache))
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
//
|
//
|
||||||
//go:generate callbackgen -type SSF
|
//go:generate callbackgen -type SSF
|
||||||
type SSF struct {
|
type SSF struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Poles int
|
Poles int
|
||||||
c1 float64
|
c1 float64
|
||||||
|
@ -34,6 +35,7 @@ type SSF struct {
|
||||||
func (inc *SSF) Update(value float64) {
|
func (inc *SSF) Update(value float64) {
|
||||||
if inc.Poles == 3 {
|
if inc.Poles == 3 {
|
||||||
if inc.Values == nil {
|
if inc.Values == nil {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
x := math.Pi / float64(inc.Window)
|
x := math.Pi / float64(inc.Window)
|
||||||
a0 := math.Exp(-x)
|
a0 := math.Exp(-x)
|
||||||
b0 := 2. * a0 * math.Cos(math.Sqrt(3.)*x)
|
b0 := 2. * a0 * math.Cos(math.Sqrt(3.)*x)
|
||||||
|
@ -53,6 +55,7 @@ func (inc *SSF) Update(value float64) {
|
||||||
inc.Values.Push(result)
|
inc.Values.Push(result)
|
||||||
} else { // poles == 2
|
} else { // poles == 2
|
||||||
if inc.Values == nil {
|
if inc.Values == nil {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
x := math.Pi * math.Sqrt(2.) / float64(inc.Window)
|
x := math.Pi * math.Sqrt(2.) / float64(inc.Window)
|
||||||
a0 := math.Exp(-x)
|
a0 := math.Exp(-x)
|
||||||
inc.c3 = -a0 * a0
|
inc.c3 = -a0 * a0
|
||||||
|
@ -88,7 +91,7 @@ func (inc *SSF) Last() float64 {
|
||||||
return inc.Values.Last()
|
return inc.Values.Last()
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &SSF{}
|
var _ types.SeriesExtend = &SSF{}
|
||||||
|
|
||||||
func (inc *SSF) calculateAndUpdate(allKLines []types.KLine) {
|
func (inc *SSF) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
if inc.Values != nil {
|
if inc.Values != nil {
|
||||||
|
|
|
@ -12,6 +12,7 @@ var logst = logrus.WithField("indicator", "supertrend")
|
||||||
|
|
||||||
//go:generate callbackgen -type Supertrend
|
//go:generate callbackgen -type Supertrend
|
||||||
type Supertrend struct {
|
type Supertrend struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
ATRMultiplier float64 `json:"atrMultiplier"`
|
ATRMultiplier float64 `json:"atrMultiplier"`
|
||||||
|
|
||||||
|
@ -54,6 +55,10 @@ func (inc *Supertrend) Update(highPrice, lowPrice, closePrice float64) {
|
||||||
panic("window must be greater than 0")
|
panic("window must be greater than 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if inc.AverageTrueRange == nil {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
|
}
|
||||||
|
|
||||||
// Start with DirectionUp
|
// Start with DirectionUp
|
||||||
if inc.trend != types.DirectionUp && inc.trend != types.DirectionDown {
|
if inc.trend != types.DirectionUp && inc.trend != types.DirectionDown {
|
||||||
inc.trend = types.DirectionUp
|
inc.trend = types.DirectionUp
|
||||||
|
@ -120,7 +125,7 @@ func (inc *Supertrend) GetSignal() types.Direction {
|
||||||
return inc.tradeSignal
|
return inc.tradeSignal
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &Supertrend{}
|
var _ types.SeriesExtend = &Supertrend{}
|
||||||
|
|
||||||
func (inc *Supertrend) calculateAndUpdate(kLines []types.KLine) {
|
func (inc *Supertrend) calculateAndUpdate(kLines []types.KLine) {
|
||||||
for _, k := range kLines {
|
for _, k := range kLines {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
//go:generate callbackgen -type TEMA
|
//go:generate callbackgen -type TEMA
|
||||||
type TEMA struct {
|
type TEMA struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
A1 *EWMA
|
A1 *EWMA
|
||||||
|
@ -20,6 +21,7 @@ type TEMA struct {
|
||||||
|
|
||||||
func (inc *TEMA) Update(value float64) {
|
func (inc *TEMA) Update(value float64) {
|
||||||
if len(inc.Values) == 0 {
|
if len(inc.Values) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.A1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
inc.A1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
||||||
inc.A2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
inc.A2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
||||||
inc.A3 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
inc.A3 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
||||||
|
@ -51,7 +53,7 @@ func (inc *TEMA) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &TEMA{}
|
var _ types.SeriesExtend = &TEMA{}
|
||||||
|
|
||||||
func (inc *TEMA) calculateAndUpdate(allKLines []types.KLine) {
|
func (inc *TEMA) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
if inc.A1 == nil {
|
if inc.A1 == nil {
|
||||||
|
|
|
@ -10,6 +10,7 @@ const defaultVolumeFactor = 0.7
|
||||||
// Refer URL: https://tradingpedia.com/forex-trading-indicator/t3-moving-average-indicator/
|
// Refer URL: https://tradingpedia.com/forex-trading-indicator/t3-moving-average-indicator/
|
||||||
//go:generate callbackgen -type TILL
|
//go:generate callbackgen -type TILL
|
||||||
type TILL struct {
|
type TILL struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
VolumeFactor float64
|
VolumeFactor float64
|
||||||
e1 *EWMA
|
e1 *EWMA
|
||||||
|
@ -30,6 +31,7 @@ func (inc *TILL) Update(value float64) {
|
||||||
if inc.VolumeFactor == 0 {
|
if inc.VolumeFactor == 0 {
|
||||||
inc.VolumeFactor = defaultVolumeFactor
|
inc.VolumeFactor = defaultVolumeFactor
|
||||||
}
|
}
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.e1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
inc.e1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
||||||
inc.e2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
inc.e2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
||||||
inc.e3 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
inc.e3 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
// Refer URL: https://ja.wikipedia.org/wiki/移動平均
|
// Refer URL: https://ja.wikipedia.org/wiki/移動平均
|
||||||
//go:generate callbackgen -type TMA
|
//go:generate callbackgen -type TMA
|
||||||
type TMA struct {
|
type TMA struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
s1 *SMA
|
s1 *SMA
|
||||||
s2 *SMA
|
s2 *SMA
|
||||||
|
@ -16,6 +17,7 @@ type TMA struct {
|
||||||
|
|
||||||
func (inc *TMA) Update(value float64) {
|
func (inc *TMA) Update(value float64) {
|
||||||
if inc.s1 == nil {
|
if inc.s1 == nil {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
w := (inc.Window + 1) / 2
|
w := (inc.Window + 1) / 2
|
||||||
inc.s1 = &SMA{IntervalWindow: types.IntervalWindow{inc.Interval, w}}
|
inc.s1 = &SMA{IntervalWindow: types.IntervalWindow{inc.Interval, w}}
|
||||||
inc.s2 = &SMA{IntervalWindow: types.IntervalWindow{inc.Interval, w}}
|
inc.s2 = &SMA{IntervalWindow: types.IntervalWindow{inc.Interval, w}}
|
||||||
|
@ -46,7 +48,7 @@ func (inc *TMA) Length() int {
|
||||||
return inc.s2.Length()
|
return inc.s2.Length()
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &TMA{}
|
var _ types.SeriesExtend = &TMA{}
|
||||||
|
|
||||||
func (inc *TMA) calculateAndUpdate(allKLines []types.KLine) {
|
func (inc *TMA) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
if inc.s1 == nil {
|
if inc.s1 == nil {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
// Refer URL: https://metatrader5.com/en/terminal/help/indicators/trend_indicators/vida
|
// Refer URL: https://metatrader5.com/en/terminal/help/indicators/trend_indicators/vida
|
||||||
//go:generate callbackgen -type VIDYA
|
//go:generate callbackgen -type VIDYA
|
||||||
type VIDYA struct {
|
type VIDYA struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
input types.Float64Slice
|
input types.Float64Slice
|
||||||
|
@ -19,6 +20,7 @@ type VIDYA struct {
|
||||||
|
|
||||||
func (inc *VIDYA) Update(value float64) {
|
func (inc *VIDYA) Update(value float64) {
|
||||||
if inc.Values.Length() == 0 {
|
if inc.Values.Length() == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.Values.Push(value)
|
inc.Values.Push(value)
|
||||||
inc.input.Push(value)
|
inc.input.Push(value)
|
||||||
return
|
return
|
||||||
|
@ -66,7 +68,7 @@ func (inc *VIDYA) Length() int {
|
||||||
return inc.Values.Length()
|
return inc.Values.Length()
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &VIDYA{}
|
var _ types.SeriesExtend = &VIDYA{}
|
||||||
|
|
||||||
func (inc *VIDYA) calculateAndUpdate(allKLines []types.KLine) {
|
func (inc *VIDYA) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
if inc.input.Length() == 0 {
|
if inc.input.Length() == 0 {
|
||||||
|
|
|
@ -17,6 +17,7 @@ const MaxNumOfVOLTruncateSize = 100
|
||||||
|
|
||||||
//go:generate callbackgen -type VOLATILITY
|
//go:generate callbackgen -type VOLATILITY
|
||||||
type VOLATILITY struct {
|
type VOLATILITY struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
EndTime time.Time
|
EndTime time.Time
|
||||||
|
@ -31,6 +32,19 @@ func (inc *VOLATILITY) Last() float64 {
|
||||||
return inc.Values[len(inc.Values)-1]
|
return inc.Values[len(inc.Values)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (inc *VOLATILITY) Index(i int) float64 {
|
||||||
|
if len(inc.Values)-i <= 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
return inc.Values[len(inc.Values)-i-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *VOLATILITY) Length() int {
|
||||||
|
return len(inc.Values)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ types.SeriesExtend = &VOLATILITY{}
|
||||||
|
|
||||||
func (inc *VOLATILITY) calculateAndUpdate(klines []types.KLine) {
|
func (inc *VOLATILITY) calculateAndUpdate(klines []types.KLine) {
|
||||||
if len(klines) < inc.Window {
|
if len(klines) < inc.Window {
|
||||||
return
|
return
|
||||||
|
@ -42,6 +56,9 @@ func (inc *VOLATILITY) calculateAndUpdate(klines []types.KLine) {
|
||||||
if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) {
|
if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(inc.Values) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
|
}
|
||||||
|
|
||||||
var recentT = klines[end-(inc.Window-1) : end+1]
|
var recentT = klines[end-(inc.Window-1) : end+1]
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ Volume-Weighted Average Price (VWAP) Explained
|
||||||
*/
|
*/
|
||||||
//go:generate callbackgen -type VWAP
|
//go:generate callbackgen -type VWAP
|
||||||
type VWAP struct {
|
type VWAP struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
Prices types.Float64Slice
|
Prices types.Float64Slice
|
||||||
|
@ -29,6 +30,9 @@ type VWAP struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *VWAP) Update(price, volume float64) {
|
func (inc *VWAP) Update(price, volume float64) {
|
||||||
|
if len(inc.Prices) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
|
}
|
||||||
inc.Prices.Push(price)
|
inc.Prices.Push(price)
|
||||||
inc.Volumes.Push(volume)
|
inc.Volumes.Push(volume)
|
||||||
|
|
||||||
|
@ -65,7 +69,7 @@ func (inc *VWAP) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &VWAP{}
|
var _ types.SeriesExtend = &VWAP{}
|
||||||
|
|
||||||
func (inc *VWAP) calculateAndUpdate(kLines []types.KLine) {
|
func (inc *VWAP) calculateAndUpdate(kLines []types.KLine) {
|
||||||
var priceF = KLineTypicalPriceMapper
|
var priceF = KLineTypicalPriceMapper
|
||||||
|
|
|
@ -20,6 +20,7 @@ Volume Weighted Moving Average
|
||||||
*/
|
*/
|
||||||
//go:generate callbackgen -type VWMA
|
//go:generate callbackgen -type VWMA
|
||||||
type VWMA struct {
|
type VWMA struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
EndTime time.Time
|
EndTime time.Time
|
||||||
|
@ -46,7 +47,7 @@ func (inc *VWMA) Length() int {
|
||||||
return len(inc.Values)
|
return len(inc.Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &VWMA{}
|
var _ types.SeriesExtend = &VWMA{}
|
||||||
|
|
||||||
func KLinePriceVolumeMapper(k types.KLine) float64 {
|
func KLinePriceVolumeMapper(k types.KLine) float64 {
|
||||||
return k.Close.Mul(k.Volume).Float64()
|
return k.Close.Mul(k.Volume).Float64()
|
||||||
|
@ -81,6 +82,10 @@ func (inc *VWMA) calculateAndUpdate(kLines []types.KLine) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(inc.Values) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
|
}
|
||||||
|
|
||||||
vwma := pv / v
|
vwma := pv / v
|
||||||
inc.Values.Push(vwma)
|
inc.Values.Push(vwma)
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ const MaxNumOfWWMATruncateSize = 100
|
||||||
|
|
||||||
//go:generate callbackgen -type WWMA
|
//go:generate callbackgen -type WWMA
|
||||||
type WWMA struct {
|
type WWMA struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
LastOpenTime time.Time
|
LastOpenTime time.Time
|
||||||
|
@ -23,6 +24,7 @@ type WWMA struct {
|
||||||
|
|
||||||
func (inc *WWMA) Update(value float64) {
|
func (inc *WWMA) Update(value float64) {
|
||||||
if len(inc.Values) == 0 {
|
if len(inc.Values) == 0 {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.Values.Push(value)
|
inc.Values.Push(value)
|
||||||
return
|
return
|
||||||
} else if len(inc.Values) > MaxNumOfWWMA {
|
} else if len(inc.Values) > MaxNumOfWWMA {
|
||||||
|
@ -85,4 +87,4 @@ func (inc *WWMA) Bind(updater KLineWindowUpdater) {
|
||||||
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
|
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &WWMA{}
|
var _ types.SeriesExtend = &WWMA{}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
//go:generate callbackgen -type ZLEMA
|
//go:generate callbackgen -type ZLEMA
|
||||||
type ZLEMA struct {
|
type ZLEMA struct {
|
||||||
|
types.SeriesBase
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
|
|
||||||
data types.Float64Slice
|
data types.Float64Slice
|
||||||
|
@ -41,6 +42,7 @@ func (inc *ZLEMA) Length() int {
|
||||||
|
|
||||||
func (inc *ZLEMA) Update(value float64) {
|
func (inc *ZLEMA) Update(value float64) {
|
||||||
if inc.lag == 0 || inc.zlema == nil {
|
if inc.lag == 0 || inc.zlema == nil {
|
||||||
|
inc.SeriesBase.Series = inc
|
||||||
inc.zlema = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
inc.zlema = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
||||||
inc.lag = int((float64(inc.Window)-1.)/2. + 0.5)
|
inc.lag = int((float64(inc.Window)-1.)/2. + 0.5)
|
||||||
}
|
}
|
||||||
|
@ -55,7 +57,7 @@ func (inc *ZLEMA) Update(value float64) {
|
||||||
inc.zlema.Update(emaData)
|
inc.zlema.Update(emaData)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.Series = &ZLEMA{}
|
var _ types.SeriesExtend = &ZLEMA{}
|
||||||
|
|
||||||
func (inc *ZLEMA) calculateAndUpdate(allKLines []types.KLine) {
|
func (inc *ZLEMA) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
if inc.zlema == nil {
|
if inc.zlema == nil {
|
||||||
|
|
|
@ -112,7 +112,7 @@ func (it *Interact) handleResponse(session Session, text string, ctxObjects ...i
|
||||||
}
|
}
|
||||||
|
|
||||||
ctxObjects = append(ctxObjects, session)
|
ctxObjects = append(ctxObjects, session)
|
||||||
_, err := parseFuncArgsAndCall(f, args, ctxObjects...)
|
_, err := ParseFuncArgsAndCall(f, args, ctxObjects...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ func (it *Interact) runCommand(session Session, command string, args []string, c
|
||||||
|
|
||||||
ctxObjects = append(ctxObjects, session)
|
ctxObjects = append(ctxObjects, session)
|
||||||
session.SetState(cmd.initState)
|
session.SetState(cmd.initState)
|
||||||
if _, err := parseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil {
|
if _, err := ParseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ func Test_parseFuncArgsAndCall_NoErrorFunction(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := parseFuncArgsAndCall(noErrorFunc, []string{"BTCUSDT", "0.123", "true"})
|
_, err := ParseFuncArgsAndCall(noErrorFunc, []string{"BTCUSDT", "0.123", "true"})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ func Test_parseFuncArgsAndCall_ErrorFunction(t *testing.T) {
|
||||||
return errors.New("error")
|
return errors.New("error")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := parseFuncArgsAndCall(errorFunc, []string{"BTCUSDT", "0.123"})
|
_, err := ParseFuncArgsAndCall(errorFunc, []string{"BTCUSDT", "0.123"})
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ func Test_parseFuncArgsAndCall_InterfaceInjection(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
_, err := parseFuncArgsAndCall(f, []string{"BTCUSDT", "0.123"}, buf)
|
_, err := ParseFuncArgsAndCall(f, []string{"BTCUSDT", "0.123"}, buf)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "123", buf.String())
|
assert.Equal(t, "123", buf.String())
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,21 +10,20 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) (State, error) {
|
func ParseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) (State, error) {
|
||||||
fv := reflect.ValueOf(f)
|
fv := reflect.ValueOf(f)
|
||||||
ft := reflect.TypeOf(f)
|
ft := reflect.TypeOf(f)
|
||||||
|
|
||||||
argIndex := 0
|
argIndex := 0
|
||||||
|
|
||||||
var rArgs []reflect.Value
|
var rArgs []reflect.Value
|
||||||
for i := 0; i < ft.NumIn(); i++ {
|
for i := 0; i < ft.NumIn(); i++ {
|
||||||
at := ft.In(i)
|
at := ft.In(i)
|
||||||
|
|
||||||
|
// get the kind of argument
|
||||||
switch k := at.Kind(); k {
|
switch k := at.Kind(); k {
|
||||||
|
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
found := false
|
found := false
|
||||||
|
|
||||||
for oi := 0; oi < len(objects); oi++ {
|
for oi := 0; oi < len(objects); oi++ {
|
||||||
obj := objects[oi]
|
obj := objects[oi]
|
||||||
objT := reflect.TypeOf(obj)
|
objT := reflect.TypeOf(obj)
|
||||||
|
@ -90,8 +89,8 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to get the error object from the return value
|
// try to get the error object from the return value
|
||||||
var state State
|
|
||||||
var err error
|
var err error
|
||||||
|
var state State
|
||||||
for i := 0; i < ft.NumOut(); i++ {
|
for i := 0; i < ft.NumOut(); i++ {
|
||||||
outType := ft.Out(i)
|
outType := ft.Out(i)
|
||||||
switch outType.Kind() {
|
switch outType.Kind() {
|
||||||
|
@ -107,7 +106,6 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{})
|
||||||
err = ov
|
err = ov
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state, err
|
return state, err
|
||||||
|
|
|
@ -4,10 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/cheggaaa/pb/v3"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/evanphx/json-patch/v5"
|
"github.com/cheggaaa/pb/v3"
|
||||||
|
|
||||||
|
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/backtest"
|
"github.com/c9s/bbgo/pkg/backtest"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
@ -19,9 +20,27 @@ var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fix
|
||||||
return summaryReport.TotalProfit
|
return summaryReport.TotalProfit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var TotalVolume = func(summaryReport *backtest.SummaryReport) fixedpoint.Value {
|
||||||
|
if len(summaryReport.SymbolReports) == 0 {
|
||||||
|
return fixedpoint.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
buyVolume := summaryReport.SymbolReports[0].PnL.BuyVolume
|
||||||
|
sellVolume := summaryReport.SymbolReports[0].PnL.SellVolume
|
||||||
|
return buyVolume.Add(sellVolume)
|
||||||
|
}
|
||||||
|
|
||||||
type Metric struct {
|
type Metric struct {
|
||||||
|
// Labels is the labels of the given parameters
|
||||||
Labels []string `json:"labels,omitempty"`
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
|
||||||
|
// Params is the parameters used to output the metrics result
|
||||||
Params []interface{} `json:"params,omitempty"`
|
Params []interface{} `json:"params,omitempty"`
|
||||||
|
|
||||||
|
// Key is the metric name
|
||||||
|
Key string `json:"key"`
|
||||||
|
|
||||||
|
// Value is the metric value of the metric
|
||||||
Value fixedpoint.Value `json:"value,omitempty"`
|
Value fixedpoint.Value `json:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,6 +187,7 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][]
|
||||||
|
|
||||||
var valueFunctions = map[string]MetricValueFunc{
|
var valueFunctions = map[string]MetricValueFunc{
|
||||||
"totalProfit": TotalProfitMetricValueFunc,
|
"totalProfit": TotalProfitMetricValueFunc,
|
||||||
|
"totalVolume": TotalVolume,
|
||||||
}
|
}
|
||||||
var metrics = map[string][]Metric{}
|
var metrics = map[string][]Metric{}
|
||||||
|
|
||||||
|
@ -216,13 +236,20 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][]
|
||||||
close(taskC) // this will shut down the executor
|
close(taskC) // this will shut down the executor
|
||||||
|
|
||||||
for result := range resultsC {
|
for result := range resultsC {
|
||||||
for metricName, metricFunc := range valueFunctions {
|
if result.Report == nil {
|
||||||
|
log.Errorf("no summaryReport found for params: %+v", result.Params)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for metricKey, metricFunc := range valueFunctions {
|
||||||
var metricValue = metricFunc(result.Report)
|
var metricValue = metricFunc(result.Report)
|
||||||
bar.Set("log", fmt.Sprintf("params: %+v => %s %+v", result.Params, metricName, metricValue))
|
bar.Set("log", fmt.Sprintf("params: %+v => %s %+v", result.Params, metricKey, metricValue))
|
||||||
bar.Increment()
|
bar.Increment()
|
||||||
metrics[metricName] = append(metrics[metricName], Metric{
|
|
||||||
|
metrics[metricKey] = append(metrics[metricKey], Metric{
|
||||||
Params: result.Params,
|
Params: result.Params,
|
||||||
Labels: result.Labels,
|
Labels: result.Labels,
|
||||||
|
Key: metricKey,
|
||||||
Value: metricValue,
|
Value: metricValue,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
1
pkg/statistics/omega.go
Normal file
1
pkg/statistics/omega.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package statistics
|
34
pkg/statistics/sharp.go
Normal file
34
pkg/statistics/sharp.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package statistics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sharpe: Calcluates the sharpe ratio of access returns
|
||||||
|
//
|
||||||
|
// @param periods (int): Freq. of returns (252/365 for daily, 12 for monthy)
|
||||||
|
// @param annualize (bool): return annualize sharpe?
|
||||||
|
// @param smart (bool): return smart sharpe ratio
|
||||||
|
func Sharpe(returns types.Series, periods int, annualize bool, smart bool) float64 {
|
||||||
|
data := returns
|
||||||
|
num := data.Length()
|
||||||
|
if types.Lowest(data, num) >= 0 && types.Highest(data, num) > 1 {
|
||||||
|
data = types.PercentageChange(returns)
|
||||||
|
}
|
||||||
|
divisor := types.Stdev(data, data.Length(), 1)
|
||||||
|
if smart {
|
||||||
|
sum := 0.
|
||||||
|
coef := math.Abs(types.Correlation(data, types.Shift(data, 1), num-1))
|
||||||
|
for i := 1; i < num; i++ {
|
||||||
|
sum += float64(num-i) / float64(num) * math.Pow(coef, float64(i))
|
||||||
|
}
|
||||||
|
divisor = divisor * math.Sqrt(1.+2.*sum)
|
||||||
|
}
|
||||||
|
result := types.Mean(data) / divisor
|
||||||
|
if annualize {
|
||||||
|
return result * math.Sqrt(float64(periods))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
27
pkg/statistics/sharp_test.go
Normal file
27
pkg/statistics/sharp_test.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package statistics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
python
|
||||||
|
|
||||||
|
import quantstats as qx
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 0, False, False))
|
||||||
|
print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 252, False, False))
|
||||||
|
print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 252, True, False))
|
||||||
|
*/
|
||||||
|
func TestSharpe(t *testing.T) {
|
||||||
|
var a types.Series = &types.Float64Slice{0.01, 0.1, 0.001}
|
||||||
|
output := Sharpe(a, 0, false, false)
|
||||||
|
assert.InDelta(t, output, 0.67586, 0.0001)
|
||||||
|
output = Sharpe(a, 252, false, false)
|
||||||
|
assert.InDelta(t, output, 0.67586, 0.0001)
|
||||||
|
output = Sharpe(a, 252, true, false)
|
||||||
|
assert.InDelta(t, output, 10.7289, 0.0001)
|
||||||
|
}
|
1
pkg/statistics/sortino.go
Normal file
1
pkg/statistics/sortino.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package statistics
|
|
@ -41,9 +41,6 @@ type Strategy struct {
|
||||||
// This field will be injected automatically since we defined the Symbol field.
|
// This field will be injected automatically since we defined the Symbol field.
|
||||||
*bbgo.StandardIndicatorSet
|
*bbgo.StandardIndicatorSet
|
||||||
|
|
||||||
// Graceful let you define the graceful shutdown handler
|
|
||||||
*bbgo.Graceful
|
|
||||||
|
|
||||||
// Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc
|
// Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc
|
||||||
// This field will be injected automatically since we defined the Symbol field.
|
// This field will be injected automatically since we defined the Symbol field.
|
||||||
types.Market
|
types.Market
|
||||||
|
@ -350,7 +347,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
s.profitOrders.BindStream(session.UserDataStream)
|
s.profitOrders.BindStream(session.UserDataStream)
|
||||||
|
|
||||||
// setup graceful shutting down handler
|
// setup graceful shutting down handler
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
// call Done to notify the main process.
|
// call Done to notify the main process.
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
log.Infof("canceling active orders...")
|
log.Infof("canceling active orders...")
|
||||||
|
|
|
@ -49,7 +49,6 @@ type BollingerSetting struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
*bbgo.Persistence
|
*bbgo.Persistence
|
||||||
|
|
||||||
Environment *bbgo.Environment
|
Environment *bbgo.Environment
|
||||||
|
@ -216,18 +215,6 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
|
||||||
return s.orderExecutor.ClosePosition(ctx, percentage)
|
return s.orderExecutor.ClosePosition(ctx, percentage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: LoadState method is migrated to the persistence struct tag.
|
|
||||||
func (s *Strategy) LoadState() error {
|
|
||||||
var state State
|
|
||||||
|
|
||||||
// load position
|
|
||||||
if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err == nil {
|
|
||||||
s.state = &state
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Strategy) getCurrentAllowedExposurePosition(bandPercentage float64) (fixedpoint.Value, error) {
|
func (s *Strategy) getCurrentAllowedExposurePosition(bandPercentage float64) (fixedpoint.Value, error) {
|
||||||
if s.DynamicExposurePositionScale != nil {
|
if s.DynamicExposurePositionScale != nil {
|
||||||
v, err := s.DynamicExposurePositionScale.Scale(bandPercentage)
|
v, err := s.DynamicExposurePositionScale.Scale(bandPercentage)
|
||||||
|
@ -506,18 +493,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
|
|
||||||
// If position is nil, we need to allocate a new position for calculation
|
// If position is nil, we need to allocate a new position for calculation
|
||||||
if s.Position == nil {
|
if s.Position == nil {
|
||||||
// restore state (legacy)
|
|
||||||
if err := s.LoadState(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback to the legacy position struct in the state
|
|
||||||
if s.state != nil && s.state.Position != nil && !s.state.Position.Base.IsZero() {
|
|
||||||
s.Position = s.state.Position
|
|
||||||
} else {
|
|
||||||
s.Position = types.NewPositionFromMarket(s.Market)
|
s.Position = types.NewPositionFromMarket(s.Market)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 {
|
if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 {
|
||||||
s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{
|
s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{
|
||||||
|
@ -527,14 +504,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.ProfitStats == nil {
|
if s.ProfitStats == nil {
|
||||||
if s.state != nil {
|
|
||||||
// copy profit stats
|
|
||||||
p2 := s.state.ProfitStats
|
|
||||||
s.ProfitStats = &p2
|
|
||||||
} else {
|
|
||||||
s.ProfitStats = types.NewProfitStats(s.Market)
|
s.ProfitStats = types.NewProfitStats(s.Market)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Always update the position fields
|
// Always update the position fields
|
||||||
s.Position.Strategy = ID
|
s.Position.Strategy = ID
|
||||||
|
@ -616,7 +587,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
// s.book = types.NewStreamBook(s.Symbol)
|
// s.book = types.NewStreamBook(s.Symbol)
|
||||||
// s.book.BindStreamForBackground(session.MarketDataStream)
|
// s.book.BindStreamForBackground(session.MarketDataStream)
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
_ = s.orderExecutor.GracefulCancel(ctx)
|
_ = s.orderExecutor.GracefulCancel(ctx)
|
||||||
|
|
|
@ -47,7 +47,6 @@ func (b BudgetPeriod) Duration() time.Duration {
|
||||||
|
|
||||||
// Strategy is the Dollar-Cost-Average strategy
|
// Strategy is the Dollar-Cost-Average strategy
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
|
|
||||||
Environment *bbgo.Environment
|
Environment *bbgo.Environment
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
|
|
|
@ -25,7 +25,6 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
|
|
||||||
SourceExchangeName string `json:"sourceExchange"`
|
SourceExchangeName string `json:"sourceExchange"`
|
||||||
|
|
||||||
|
@ -217,7 +216,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
s.place(ctx, orderExecutor, session, indicator, closePrice)
|
s.place(ctx, orderExecutor, session, indicator, closePrice)
|
||||||
})
|
})
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
log.Infof("canceling trailingstop order...")
|
log.Infof("canceling trailingstop order...")
|
||||||
s.clear(ctx, orderExecutor)
|
s.clear(ctx, orderExecutor)
|
||||||
|
@ -261,7 +260,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
|
||||||
s.place(ctx, &orderExecutor, session, indicator, closePrice)
|
s.place(ctx, &orderExecutor, session, indicator, closePrice)
|
||||||
})
|
})
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
log.Infof("canceling trailingstop order...")
|
log.Infof("canceling trailingstop order...")
|
||||||
s.clear(ctx, &orderExecutor)
|
s.clear(ctx, &orderExecutor)
|
||||||
|
|
|
@ -51,7 +51,6 @@ type Strategy struct {
|
||||||
KLineEndTime types.Time
|
KLineEndTime types.Time
|
||||||
|
|
||||||
*bbgo.Environment
|
*bbgo.Environment
|
||||||
*bbgo.Graceful
|
|
||||||
bbgo.StrategyController
|
bbgo.StrategyController
|
||||||
|
|
||||||
activeMakerOrders *bbgo.ActiveOrderBook
|
activeMakerOrders *bbgo.ActiveOrderBook
|
||||||
|
@ -63,11 +62,11 @@ type Strategy struct {
|
||||||
atr *indicator.ATR
|
atr *indicator.ATR
|
||||||
emv *indicator.EMV
|
emv *indicator.EMV
|
||||||
ccis *CCISTOCH
|
ccis *CCISTOCH
|
||||||
ma5 types.Series
|
ma5 types.SeriesExtend
|
||||||
ma34 types.Series
|
ma34 types.SeriesExtend
|
||||||
ewo types.Series
|
ewo types.SeriesExtend
|
||||||
ewoSignal types.Series
|
ewoSignal types.SeriesExtend
|
||||||
ewoHistogram types.Series
|
ewoHistogram types.SeriesExtend
|
||||||
ewoChangeRate float64
|
ewoChangeRate float64
|
||||||
heikinAshi *HeikinAshi
|
heikinAshi *HeikinAshi
|
||||||
peakPrice fixedpoint.Value
|
peakPrice fixedpoint.Value
|
||||||
|
@ -331,12 +330,12 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) {
|
||||||
evwma34.UpdateVal(price, vol)
|
evwma34.UpdateVal(price, vol)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
s.ma5 = evwma5
|
s.ma5 = types.NewSeries(evwma5)
|
||||||
s.ma34 = evwma34
|
s.ma34 = types.NewSeries(evwma34)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.ewo = types.Mul(types.Minus(types.Div(s.ma5, s.ma34), 1.0), 100.)
|
s.ewo = s.ma5.Div(s.ma34).Minus(1.0).Mul(100.)
|
||||||
s.ewoHistogram = types.Minus(s.ma5, s.ma34)
|
s.ewoHistogram = s.ma5.Minus(s.ma34)
|
||||||
windowSignal := types.IntervalWindow{Interval: s.Interval, Window: s.SignalWindow}
|
windowSignal := types.IntervalWindow{Interval: s.Interval, Window: s.SignalWindow}
|
||||||
if s.UseEma {
|
if s.UseEma {
|
||||||
sig := &indicator.EWMA{IntervalWindow: windowSignal}
|
sig := &indicator.EWMA{IntervalWindow: windowSignal}
|
||||||
|
@ -365,7 +364,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) {
|
||||||
|
|
||||||
if sig.Length() == 0 {
|
if sig.Length() == 0 {
|
||||||
// lazy init
|
// lazy init
|
||||||
ewoVals := types.Reverse(s.ewo)
|
ewoVals := s.ewo.Reverse()
|
||||||
for _, ewoValue := range ewoVals {
|
for _, ewoValue := range ewoVals {
|
||||||
sig.Update(ewoValue)
|
sig.Update(ewoValue)
|
||||||
}
|
}
|
||||||
|
@ -385,7 +384,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) {
|
||||||
}
|
}
|
||||||
if sig.Length() == 0 {
|
if sig.Length() == 0 {
|
||||||
// lazy init
|
// lazy init
|
||||||
ewoVals := types.Reverse(s.ewo)
|
ewoVals := s.ewo.Reverse()
|
||||||
for i, ewoValue := range ewoVals {
|
for i, ewoValue := range ewoVals {
|
||||||
vol := window.Volume().Index(i)
|
vol := window.Volume().Index(i)
|
||||||
sig.PV.Update(ewoValue * vol)
|
sig.PV.Update(ewoValue * vol)
|
||||||
|
@ -397,7 +396,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) {
|
||||||
sig.V.Update(vol)
|
sig.V.Update(vol)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
s.ewoSignal = sig
|
s.ewoSignal = types.NewSeries(sig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1221,7 +1220,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
|
||||||
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
log.Infof("canceling active orders...")
|
log.Infof("canceling active orders...")
|
||||||
s.CancelAll(ctx)
|
s.CancelAll(ctx)
|
||||||
|
|
|
@ -49,10 +49,6 @@ type Strategy struct {
|
||||||
// This field will be injected automatically since we defined the Symbol field.
|
// This field will be injected automatically since we defined the Symbol field.
|
||||||
*bbgo.StandardIndicatorSet
|
*bbgo.StandardIndicatorSet
|
||||||
|
|
||||||
// Graceful shutdown function
|
|
||||||
*bbgo.Graceful
|
|
||||||
// --------------------------
|
|
||||||
|
|
||||||
// ewma is the exponential weighted moving average indicator
|
// ewma is the exponential weighted moving average indicator
|
||||||
ewma *indicator.EWMA
|
ewma *indicator.EWMA
|
||||||
}
|
}
|
||||||
|
@ -114,7 +110,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
|
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
|
||||||
s.activeOrders.BindStream(session.UserDataStream)
|
s.activeOrders.BindStream(session.UserDataStream)
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
log.Infof("canceling active orders...")
|
log.Infof("canceling active orders...")
|
||||||
|
|
|
@ -31,7 +31,6 @@ type IntervalWindowSetting struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
*bbgo.Persistence
|
*bbgo.Persistence
|
||||||
|
|
||||||
Environment *bbgo.Environment
|
Environment *bbgo.Environment
|
||||||
|
|
|
@ -45,8 +45,6 @@ type State struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful `json:"-" yaml:"-"`
|
|
||||||
|
|
||||||
*bbgo.Persistence
|
*bbgo.Persistence
|
||||||
|
|
||||||
// OrderExecutor is an interface for submitting order.
|
// OrderExecutor is an interface for submitting order.
|
||||||
|
@ -621,7 +619,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
})
|
})
|
||||||
s.tradeCollector.BindStream(session.UserDataStream)
|
s.tradeCollector.BindStream(session.UserDataStream)
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
if err := s.SaveState(); err != nil {
|
if err := s.SaveState(); err != nil {
|
||||||
|
|
191
pkg/strategy/pivotshort/breaklow.go
Normal file
191
pkg/strategy/pivotshort/breaklow.go
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
package pivotshort
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/indicator"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
|
||||||
|
type BreakLow struct {
|
||||||
|
Symbol string
|
||||||
|
Market types.Market
|
||||||
|
types.IntervalWindow
|
||||||
|
|
||||||
|
// Ratio is a number less than 1.0, price * ratio will be the price triggers the short order.
|
||||||
|
Ratio fixedpoint.Value `json:"ratio"`
|
||||||
|
|
||||||
|
// MarketOrder is the option to enable market order short.
|
||||||
|
MarketOrder bool `json:"marketOrder"`
|
||||||
|
|
||||||
|
// BounceRatio is a ratio used for placing the limit order sell price
|
||||||
|
// limit sell price = breakLowPrice * (1 + BounceRatio)
|
||||||
|
BounceRatio fixedpoint.Value `json:"bounceRatio"`
|
||||||
|
|
||||||
|
Quantity fixedpoint.Value `json:"quantity"`
|
||||||
|
StopEMARange fixedpoint.Value `json:"stopEMARange"`
|
||||||
|
StopEMA *types.IntervalWindow `json:"stopEMA"`
|
||||||
|
|
||||||
|
lastLow fixedpoint.Value
|
||||||
|
pivot *indicator.Pivot
|
||||||
|
stopEWMA *indicator.EWMA
|
||||||
|
pivotLowPrices []fixedpoint.Value
|
||||||
|
|
||||||
|
orderExecutor *bbgo.GeneralOrderExecutor
|
||||||
|
session *bbgo.ExchangeSession
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) {
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
|
||||||
|
s.session = session
|
||||||
|
s.orderExecutor = orderExecutor
|
||||||
|
|
||||||
|
position := orderExecutor.Position()
|
||||||
|
symbol := position.Symbol
|
||||||
|
store, _ := session.MarketDataStore(symbol)
|
||||||
|
standardIndicator, _ := session.StandardIndicatorSet(symbol)
|
||||||
|
|
||||||
|
s.lastLow = fixedpoint.Zero
|
||||||
|
|
||||||
|
s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
|
||||||
|
s.pivot.Bind(store)
|
||||||
|
preloadPivot(s.pivot, store)
|
||||||
|
|
||||||
|
if s.StopEMA != nil {
|
||||||
|
s.stopEWMA = standardIndicator.EWMA(*s.StopEMA)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update pivot low data
|
||||||
|
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) {
|
||||||
|
lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow())
|
||||||
|
if lastLow.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastLow.Compare(s.lastLow) != 0 {
|
||||||
|
log.Infof("new pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time())
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastLow = lastLow
|
||||||
|
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
|
||||||
|
}))
|
||||||
|
|
||||||
|
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, types.Interval1m, func(kline types.KLine) {
|
||||||
|
if position.IsOpened(kline.Close) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.pivotLowPrices) == 0 {
|
||||||
|
log.Infof("currently there is no pivot low prices, can not check break low...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
previousLow := s.pivotLowPrices[len(s.pivotLowPrices)-1]
|
||||||
|
ratio := fixedpoint.One.Add(s.Ratio)
|
||||||
|
breakPrice := previousLow.Mul(ratio)
|
||||||
|
|
||||||
|
openPrice := kline.Open
|
||||||
|
closePrice := kline.Close
|
||||||
|
|
||||||
|
// if the previous low is not break, or the kline is not strong enough to break it, skip
|
||||||
|
if closePrice.Compare(breakPrice) >= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need the price cross the break line, or we do nothing:
|
||||||
|
// open > break price > close price
|
||||||
|
if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// force direction to be down
|
||||||
|
if closePrice.Compare(openPrice) >= 0 {
|
||||||
|
log.Infof("%s price %f is closed higher than the open price %f, skip this break", kline.Symbol, closePrice.Float64(), openPrice.Float64())
|
||||||
|
// skip UP klines
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64())
|
||||||
|
|
||||||
|
// stop EMA protection
|
||||||
|
if s.stopEWMA != nil {
|
||||||
|
ema := fixedpoint.NewFromFloat(s.stopEWMA.Last())
|
||||||
|
if ema.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMARange))
|
||||||
|
if closePrice.Compare(emaStopShortPrice) < 0 {
|
||||||
|
log.Infof("stopEMA protection: close price %f < EMA(%v) = %f", closePrice.Float64(), s.StopEMA, ema.Float64())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// graceful cancel all active orders
|
||||||
|
_ = orderExecutor.GracefulCancel(ctx)
|
||||||
|
|
||||||
|
quantity := s.useQuantityOrBaseBalance(s.Quantity)
|
||||||
|
if s.MarketOrder {
|
||||||
|
bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting market sell to open a short position", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64())
|
||||||
|
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||||
|
Symbol: s.Symbol,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Type: types.OrderTypeMarket,
|
||||||
|
Quantity: quantity,
|
||||||
|
MarginSideEffect: types.SideEffectTypeMarginBuy,
|
||||||
|
Tag: "breakLowMarket",
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
sellPrice := previousLow.Mul(fixedpoint.One.Add(s.BounceRatio))
|
||||||
|
|
||||||
|
bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64(), sellPrice.Float64())
|
||||||
|
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||||
|
Symbol: kline.Symbol,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Price: sellPrice,
|
||||||
|
Quantity: quantity,
|
||||||
|
MarginSideEffect: types.SideEffectTypeMarginBuy,
|
||||||
|
Tag: "breakLowLimit",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
if !bbgo.IsBackTesting {
|
||||||
|
// use market trade to submit short order
|
||||||
|
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BreakLow) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value {
|
||||||
|
if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures {
|
||||||
|
return quantity
|
||||||
|
}
|
||||||
|
|
||||||
|
balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency)
|
||||||
|
if hasBalance {
|
||||||
|
if quantity.IsZero() {
|
||||||
|
bbgo.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String())
|
||||||
|
quantity = balance.Available
|
||||||
|
} else {
|
||||||
|
quantity = fixedpoint.Min(quantity, balance.Available)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if quantity.IsZero() {
|
||||||
|
log.Errorf("quantity is zero, can not submit sell order, please check settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
return quantity
|
||||||
|
}
|
66
pkg/strategy/pivotshort/math.go
Normal file
66
pkg/strategy/pivotshort/math.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package pivotshort
|
||||||
|
|
||||||
|
import "sort"
|
||||||
|
|
||||||
|
func lower(arr []float64, x float64) []float64 {
|
||||||
|
sort.Float64s(arr)
|
||||||
|
|
||||||
|
var rst []float64
|
||||||
|
for _, a := range arr {
|
||||||
|
// filter prices that are lower than the current closed price
|
||||||
|
if a > x {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rst = append(rst, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rst
|
||||||
|
}
|
||||||
|
|
||||||
|
func higher(arr []float64, x float64) []float64 {
|
||||||
|
sort.Float64s(arr)
|
||||||
|
|
||||||
|
var rst []float64
|
||||||
|
for _, a := range arr {
|
||||||
|
// filter prices that are lower than the current closed price
|
||||||
|
if a < x {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rst = append(rst, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rst
|
||||||
|
}
|
||||||
|
|
||||||
|
func group(arr []float64, minDistance float64) []float64 {
|
||||||
|
if len(arr) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups []float64
|
||||||
|
var grp = []float64{arr[0]}
|
||||||
|
for _, price := range arr {
|
||||||
|
avg := average(grp)
|
||||||
|
if (price / avg) > (1.0 + minDistance) {
|
||||||
|
groups = append(groups, avg)
|
||||||
|
grp = []float64{price}
|
||||||
|
} else {
|
||||||
|
grp = append(grp, price)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(grp) > 0 {
|
||||||
|
groups = append(groups, average(grp))
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
func average(arr []float64) float64 {
|
||||||
|
s := 0.0
|
||||||
|
for _, a := range arr {
|
||||||
|
s += a
|
||||||
|
}
|
||||||
|
return s / float64(len(arr))
|
||||||
|
}
|
190
pkg/strategy/pivotshort/resistance.go
Normal file
190
pkg/strategy/pivotshort/resistance.go
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
package pivotshort
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/indicator"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResistanceShort struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Symbol string `json:"-"`
|
||||||
|
Market types.Market `json:"-"`
|
||||||
|
|
||||||
|
types.IntervalWindow
|
||||||
|
|
||||||
|
MinDistance fixedpoint.Value `json:"minDistance"`
|
||||||
|
GroupDistance fixedpoint.Value `json:"groupDistance"`
|
||||||
|
NumLayers int `json:"numLayers"`
|
||||||
|
LayerSpread fixedpoint.Value `json:"layerSpread"`
|
||||||
|
Quantity fixedpoint.Value `json:"quantity"`
|
||||||
|
Ratio fixedpoint.Value `json:"ratio"`
|
||||||
|
|
||||||
|
session *bbgo.ExchangeSession
|
||||||
|
orderExecutor *bbgo.GeneralOrderExecutor
|
||||||
|
|
||||||
|
resistancePivot *indicator.Pivot
|
||||||
|
resistancePrices []float64
|
||||||
|
currentResistancePrice fixedpoint.Value
|
||||||
|
|
||||||
|
activeOrders *bbgo.ActiveOrderBook
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
|
||||||
|
s.session = session
|
||||||
|
s.orderExecutor = orderExecutor
|
||||||
|
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
|
||||||
|
s.activeOrders.BindStream(session.UserDataStream)
|
||||||
|
|
||||||
|
if s.GroupDistance.IsZero() {
|
||||||
|
s.GroupDistance = fixedpoint.NewFromFloat(0.01)
|
||||||
|
}
|
||||||
|
|
||||||
|
store, _ := session.MarketDataStore(s.Symbol)
|
||||||
|
|
||||||
|
s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
|
||||||
|
s.resistancePivot.Bind(store)
|
||||||
|
|
||||||
|
// preload history kline data to the resistance pivot indicator
|
||||||
|
// we use the last kline to find the higher lows
|
||||||
|
lastKLine := preloadPivot(s.resistancePivot, store)
|
||||||
|
|
||||||
|
// use the last kline from the history before we get the next closed kline
|
||||||
|
if lastKLine != nil {
|
||||||
|
s.findNextResistancePriceAndPlaceOrders(lastKLine.Close)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||||
|
position := s.orderExecutor.Position()
|
||||||
|
if position.IsOpened(kline.Close) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.findNextResistancePriceAndPlaceOrders(kline.Close)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func tail(arr []float64, length int) []float64 {
|
||||||
|
if len(arr) < length {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr[len(arr)-1-length:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ResistanceShort) updateNextResistancePrice(closePrice fixedpoint.Value) bool {
|
||||||
|
minDistance := s.MinDistance.Float64()
|
||||||
|
groupDistance := s.GroupDistance.Float64()
|
||||||
|
resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, tail(s.resistancePivot.Lows, 6))
|
||||||
|
|
||||||
|
if len(resistancePrices) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%s close price: %f, min distance: %f, possible resistance prices: %+v", s.Symbol, closePrice.Float64(), minDistance, resistancePrices)
|
||||||
|
|
||||||
|
nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0])
|
||||||
|
|
||||||
|
// if currentResistancePrice is not set or the close price is already higher than the current resistance price,
|
||||||
|
// we should update the resistance price
|
||||||
|
// if the detected resistance price is lower than the current one, we should also update it too
|
||||||
|
if s.currentResistancePrice.IsZero() {
|
||||||
|
s.currentResistancePrice = nextResistancePrice
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the current sell price is out-dated
|
||||||
|
// or
|
||||||
|
// the next resistance is lower than the current one.
|
||||||
|
currentSellPrice := s.currentResistancePrice.Mul(one.Add(s.Ratio))
|
||||||
|
if closePrice.Compare(currentSellPrice) > 0 ||
|
||||||
|
nextResistancePrice.Compare(currentSellPrice) < 0 {
|
||||||
|
s.currentResistancePrice = nextResistancePrice
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) {
|
||||||
|
ctx := context.Background()
|
||||||
|
resistanceUpdated := s.updateNextResistancePrice(closePrice)
|
||||||
|
if resistanceUpdated {
|
||||||
|
bbgo.Notify("Found next resistance price: %f, updating resistance order...", s.currentResistancePrice.Float64())
|
||||||
|
s.placeResistanceOrders(ctx, s.currentResistancePrice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistancePrice fixedpoint.Value) {
|
||||||
|
futuresMode := s.session.Futures || s.session.IsolatedFutures
|
||||||
|
_ = futuresMode
|
||||||
|
|
||||||
|
totalQuantity := s.Quantity
|
||||||
|
numLayers := s.NumLayers
|
||||||
|
if numLayers == 0 {
|
||||||
|
numLayers = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
numLayersF := fixedpoint.NewFromInt(int64(numLayers))
|
||||||
|
layerSpread := s.LayerSpread
|
||||||
|
quantity := totalQuantity.Div(numLayersF)
|
||||||
|
|
||||||
|
if s.activeOrders.NumOfOrders() > 0 {
|
||||||
|
if err := s.orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil {
|
||||||
|
log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.activeOrders.Orders())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers)
|
||||||
|
|
||||||
|
var sellPriceStart = resistancePrice.Mul(fixedpoint.One.Add(s.Ratio))
|
||||||
|
var orderForms []types.SubmitOrder
|
||||||
|
for i := 0; i < numLayers; i++ {
|
||||||
|
balances := s.session.GetAccount().Balances()
|
||||||
|
quoteBalance := balances[s.Market.QuoteCurrency]
|
||||||
|
baseBalance := balances[s.Market.BaseCurrency]
|
||||||
|
_ = quoteBalance
|
||||||
|
_ = baseBalance
|
||||||
|
|
||||||
|
spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i)))
|
||||||
|
price := sellPriceStart.Mul(one.Add(spread))
|
||||||
|
log.Infof("price = %f", price.Float64())
|
||||||
|
|
||||||
|
log.Infof("placing resistance short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64())
|
||||||
|
|
||||||
|
orderForms = append(orderForms, types.SubmitOrder{
|
||||||
|
Symbol: s.Symbol,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Type: types.OrderTypeLimitMaker,
|
||||||
|
Price: price,
|
||||||
|
Quantity: quantity,
|
||||||
|
Tag: "resistanceShort",
|
||||||
|
MarginSideEffect: types.SideEffectTypeMarginBuy,
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: fix futures mode later
|
||||||
|
/*
|
||||||
|
if futuresMode {
|
||||||
|
if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orderForms...)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Errorf("can not place resistance order")
|
||||||
|
}
|
||||||
|
s.activeOrders.Add(createdOrders...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPossibleSupportPrices(closePrice float64, groupDistance float64, lows []float64) []float64 {
|
||||||
|
return group(lower(lows, closePrice), groupDistance)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPossibleResistancePrices(closePrice float64, groupDistance float64, lows []float64) []float64 {
|
||||||
|
return group(higher(lows, closePrice), groupDistance)
|
||||||
|
}
|
28
pkg/strategy/pivotshort/resistance_test.go
Normal file
28
pkg/strategy/pivotshort/resistance_test.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package pivotshort
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_findPossibleResistancePrices(t *testing.T) {
|
||||||
|
prices := findPossibleResistancePrices(19000.0, 0.01, []float64{
|
||||||
|
23020.0,
|
||||||
|
23040.0,
|
||||||
|
23060.0,
|
||||||
|
|
||||||
|
24020.0,
|
||||||
|
24040.0,
|
||||||
|
24060.0,
|
||||||
|
})
|
||||||
|
assert.Equal(t, []float64{23035, 24040}, prices)
|
||||||
|
|
||||||
|
|
||||||
|
prices = findPossibleResistancePrices(19000.0, 0.01, []float64{
|
||||||
|
23020.0,
|
||||||
|
23040.0,
|
||||||
|
23060.0,
|
||||||
|
})
|
||||||
|
assert.Equal(t, []float64{23035}, prices)
|
||||||
|
}
|
|
@ -4,12 +4,12 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo"
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
|
"github.com/c9s/bbgo/pkg/dynamic"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/indicator"
|
"github.com/c9s/bbgo/pkg/indicator"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
@ -30,47 +30,124 @@ type IntervalWindowSetting struct {
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
|
type SupportTakeProfit struct {
|
||||||
type BreakLow struct {
|
Symbol string
|
||||||
// Ratio is a number less than 1.0, price * ratio will be the price triggers the short order.
|
|
||||||
Ratio fixedpoint.Value `json:"ratio"`
|
|
||||||
|
|
||||||
// MarketOrder is the option to enable market order short.
|
|
||||||
MarketOrder bool `json:"marketOrder"`
|
|
||||||
|
|
||||||
// BounceRatio is a ratio used for placing the limit order sell price
|
|
||||||
// limit sell price = breakLowPrice * (1 + BounceRatio)
|
|
||||||
BounceRatio fixedpoint.Value `json:"bounceRatio"`
|
|
||||||
|
|
||||||
Quantity fixedpoint.Value `json:"quantity"`
|
|
||||||
StopEMARange fixedpoint.Value `json:"stopEMARange"`
|
|
||||||
StopEMA *types.IntervalWindow `json:"stopEMA"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BounceShort struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
|
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
|
|
||||||
MinDistance fixedpoint.Value `json:"minDistance"`
|
|
||||||
NumLayers int `json:"numLayers"`
|
|
||||||
LayerSpread fixedpoint.Value `json:"layerSpread"`
|
|
||||||
Quantity fixedpoint.Value `json:"quantity"`
|
|
||||||
Ratio fixedpoint.Value `json:"ratio"`
|
Ratio fixedpoint.Value `json:"ratio"`
|
||||||
|
|
||||||
|
pivot *indicator.Pivot
|
||||||
|
orderExecutor *bbgo.GeneralOrderExecutor
|
||||||
|
session *bbgo.ExchangeSession
|
||||||
|
activeOrders *bbgo.ActiveOrderBook
|
||||||
|
currentSupportPrice fixedpoint.Value
|
||||||
|
|
||||||
|
triggeredPrices []fixedpoint.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
type Entry struct {
|
func (s *SupportTakeProfit) Subscribe(session *bbgo.ExchangeSession) {
|
||||||
CatBounceRatio fixedpoint.Value `json:"catBounceRatio"`
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||||
NumLayers int `json:"numLayers"`
|
}
|
||||||
TotalQuantity fixedpoint.Value `json:"totalQuantity"`
|
|
||||||
|
|
||||||
Quantity fixedpoint.Value `json:"quantity"`
|
func (s *SupportTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
|
||||||
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
|
s.session = session
|
||||||
|
s.orderExecutor = orderExecutor
|
||||||
|
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
|
||||||
|
session.UserDataStream.OnOrderUpdate(func(order types.Order) {
|
||||||
|
if s.activeOrders.Exists(order) {
|
||||||
|
if !s.currentSupportPrice.IsZero() {
|
||||||
|
s.triggeredPrices = append(s.triggeredPrices, s.currentSupportPrice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
s.activeOrders.BindStream(session.UserDataStream)
|
||||||
|
|
||||||
|
position := orderExecutor.Position()
|
||||||
|
symbol := position.Symbol
|
||||||
|
store, _ := session.MarketDataStore(symbol)
|
||||||
|
s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
|
||||||
|
s.pivot.Bind(store)
|
||||||
|
preloadPivot(s.pivot, store)
|
||||||
|
|
||||||
|
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||||
|
if !s.updateSupportPrice(kline.Close) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !position.IsOpened(kline.Close) {
|
||||||
|
log.Infof("position is not opened, skip updating support take profit order")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio))
|
||||||
|
quantity := position.GetQuantity()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil {
|
||||||
|
log.WithError(err).Errorf("cancel order failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
bbgo.Notify("placing %s take profit order at price %f", s.Symbol, buyPrice.Float64())
|
||||||
|
createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||||
|
Symbol: symbol,
|
||||||
|
Type: types.OrderTypeLimitMaker,
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Price: buyPrice,
|
||||||
|
Quantity: quantity,
|
||||||
|
Tag: "supportTakeProfit",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Errorf("can not submit orders: %+v", createdOrders)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.activeOrders.Add(createdOrders...)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SupportTakeProfit) updateSupportPrice(closePrice fixedpoint.Value) bool {
|
||||||
|
log.Infof("[supportTakeProfit] lows: %v", s.pivot.Lows)
|
||||||
|
|
||||||
|
groupDistance := 0.01
|
||||||
|
minDistance := 0.05
|
||||||
|
supportPrices := findPossibleSupportPrices(closePrice.Float64()*(1.0-minDistance), groupDistance, s.pivot.Lows)
|
||||||
|
if len(supportPrices) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("[supportTakeProfit] found possible support prices: %v", supportPrices)
|
||||||
|
|
||||||
|
// nextSupportPrice are sorted in increasing order
|
||||||
|
nextSupportPrice := fixedpoint.NewFromFloat(supportPrices[len(supportPrices)-1])
|
||||||
|
|
||||||
|
// it's price that we have been used to take profit
|
||||||
|
for _, p := range s.triggeredPrices {
|
||||||
|
var l = p.Mul(one.Sub(fixedpoint.NewFromFloat(0.01)))
|
||||||
|
var h = p.Mul(one.Add(fixedpoint.NewFromFloat(0.01)))
|
||||||
|
if p.Compare(l) > 0 && p.Compare(h) < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBuyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio))
|
||||||
|
|
||||||
|
if s.currentSupportPrice.IsZero() {
|
||||||
|
log.Infof("setup next support take profit price at %f", nextSupportPrice.Float64())
|
||||||
|
s.currentSupportPrice = nextSupportPrice
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// the close price is already lower than the support price, than we should update
|
||||||
|
if closePrice.Compare(currentBuyPrice) < 0 || nextSupportPrice.Compare(s.currentSupportPrice) > 0 {
|
||||||
|
log.Infof("setup next support take profit price at %f", nextSupportPrice.Float64())
|
||||||
|
s.currentSupportPrice = nextSupportPrice
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
|
|
||||||
Environment *bbgo.Environment
|
Environment *bbgo.Environment
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
Market types.Market
|
Market types.Market
|
||||||
|
@ -83,25 +160,19 @@ type Strategy struct {
|
||||||
ProfitStats *types.ProfitStats `persistence:"profit_stats"`
|
ProfitStats *types.ProfitStats `persistence:"profit_stats"`
|
||||||
TradeStats *types.TradeStats `persistence:"trade_stats"`
|
TradeStats *types.TradeStats `persistence:"trade_stats"`
|
||||||
|
|
||||||
BreakLow BreakLow `json:"breakLow"`
|
// BreakLow is one of the entry method
|
||||||
|
BreakLow *BreakLow `json:"breakLow"`
|
||||||
|
|
||||||
BounceShort *BounceShort `json:"bounceShort"`
|
// ResistanceShort is one of the entry method
|
||||||
|
ResistanceShort *ResistanceShort `json:"resistanceShort"`
|
||||||
|
|
||||||
Entry Entry `json:"entry"`
|
SupportTakeProfit []*SupportTakeProfit `json:"supportTakeProfit"`
|
||||||
ExitMethods []bbgo.ExitMethod `json:"exits"`
|
|
||||||
|
ExitMethods bbgo.ExitMethodSet `json:"exits"`
|
||||||
|
|
||||||
session *bbgo.ExchangeSession
|
session *bbgo.ExchangeSession
|
||||||
orderExecutor *bbgo.GeneralOrderExecutor
|
orderExecutor *bbgo.GeneralOrderExecutor
|
||||||
|
|
||||||
stopLossPrice fixedpoint.Value
|
|
||||||
lastLow fixedpoint.Value
|
|
||||||
pivot *indicator.Pivot
|
|
||||||
resistancePivot *indicator.Pivot
|
|
||||||
stopEWMA *indicator.EWMA
|
|
||||||
pivotLowPrices []fixedpoint.Value
|
|
||||||
resistancePrices []float64
|
|
||||||
currentBounceShortPrice fixedpoint.Value
|
|
||||||
|
|
||||||
// StrategyController
|
// StrategyController
|
||||||
bbgo.StrategyController
|
bbgo.StrategyController
|
||||||
}
|
}
|
||||||
|
@ -114,55 +185,27 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m})
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m})
|
||||||
|
|
||||||
if s.BounceShort != nil && s.BounceShort.Enabled {
|
if s.ResistanceShort != nil && s.ResistanceShort.Enabled {
|
||||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.BounceShort.Interval})
|
dynamic.InheritStructValues(s.ResistanceShort, s)
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ResistanceShort.Interval})
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.BreakLow != nil {
|
||||||
|
dynamic.InheritStructValues(s.BreakLow, s)
|
||||||
|
s.BreakLow.Subscribe(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range s.SupportTakeProfit {
|
||||||
|
m := s.SupportTakeProfit[i]
|
||||||
|
dynamic.InheritStructValues(m, s)
|
||||||
|
m.Subscribe(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bbgo.IsBackTesting {
|
if !bbgo.IsBackTesting {
|
||||||
session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
|
session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value {
|
s.ExitMethods.SetAndSubscribe(session, s)
|
||||||
balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency)
|
|
||||||
|
|
||||||
if hasBalance {
|
|
||||||
if quantity.IsZero() {
|
|
||||||
bbgo.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String())
|
|
||||||
quantity = balance.Available
|
|
||||||
} else {
|
|
||||||
quantity = fixedpoint.Min(quantity, balance.Available)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if quantity.IsZero() {
|
|
||||||
log.Errorf("quantity is zero, can not submit sell order, please check settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
return quantity
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value, tag string) {
|
|
||||||
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
|
||||||
Symbol: s.Symbol,
|
|
||||||
Price: price,
|
|
||||||
Side: types.SideTypeSell,
|
|
||||||
Type: types.OrderTypeLimit,
|
|
||||||
Quantity: quantity,
|
|
||||||
MarginSideEffect: types.SideEffectTypeMarginBuy,
|
|
||||||
Tag: tag,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Value, tag string) {
|
|
||||||
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
|
||||||
Symbol: s.Symbol,
|
|
||||||
Side: types.SideTypeSell,
|
|
||||||
Type: types.OrderTypeMarket,
|
|
||||||
Quantity: quantity,
|
|
||||||
MarginSideEffect: types.SideEffectTypeMarginBuy,
|
|
||||||
Tag: tag,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) InstanceID() string {
|
func (s *Strategy) InstanceID() string {
|
||||||
|
@ -174,7 +217,6 @@ func (s *Strategy) CurrentPosition() *types.Position {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
|
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
|
||||||
bbgo.Notify("Closing position", s.Position)
|
|
||||||
return s.orderExecutor.ClosePosition(ctx, percentage)
|
return s.orderExecutor.ClosePosition(ctx, percentage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,11 +232,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.TradeStats == nil {
|
if s.TradeStats == nil {
|
||||||
s.TradeStats = &types.TradeStats{}
|
s.TradeStats = types.NewTradeStats(s.Symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.lastLow = fixedpoint.Zero
|
|
||||||
|
|
||||||
// StrategyController
|
// StrategyController
|
||||||
s.Status = types.StrategyStatusRunning
|
s.Status = types.StrategyStatusRunning
|
||||||
|
|
||||||
|
@ -221,260 +261,33 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
})
|
})
|
||||||
s.orderExecutor.Bind()
|
s.orderExecutor.Bind()
|
||||||
|
|
||||||
store, _ := session.MarketDataStore(s.Symbol)
|
|
||||||
standardIndicator, _ := session.StandardIndicatorSet(s.Symbol)
|
|
||||||
|
|
||||||
s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
|
|
||||||
s.pivot.Bind(store)
|
|
||||||
|
|
||||||
lastKLine := s.preloadPivot(s.pivot, store)
|
|
||||||
|
|
||||||
// update pivot low data
|
|
||||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
|
||||||
if kline.Symbol != s.Symbol || kline.Interval != s.Interval {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow())
|
|
||||||
if lastLow.IsZero() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastLow.Compare(s.lastLow) != 0 {
|
|
||||||
log.Infof("new pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time())
|
|
||||||
}
|
|
||||||
|
|
||||||
s.lastLow = lastLow
|
|
||||||
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
|
|
||||||
})
|
|
||||||
|
|
||||||
if s.BounceShort != nil && s.BounceShort.Enabled {
|
|
||||||
s.resistancePivot = &indicator.Pivot{IntervalWindow: s.BounceShort.IntervalWindow}
|
|
||||||
s.resistancePivot.Bind(store)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.BreakLow.StopEMA != nil {
|
|
||||||
s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, method := range s.ExitMethods {
|
for _, method := range s.ExitMethods {
|
||||||
method.Bind(session, s.orderExecutor)
|
method.Bind(session, s.orderExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.BounceShort != nil && s.BounceShort.Enabled {
|
if s.ResistanceShort != nil && s.ResistanceShort.Enabled {
|
||||||
if s.resistancePivot != nil {
|
s.ResistanceShort.Bind(session, s.orderExecutor)
|
||||||
s.preloadPivot(s.resistancePivot, store)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session.UserDataStream.OnStart(func() {
|
if s.BreakLow != nil {
|
||||||
if lastKLine == nil {
|
s.BreakLow.Bind(session, s.orderExecutor)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.resistancePivot != nil {
|
for i := range s.SupportTakeProfit {
|
||||||
lows := s.resistancePivot.Lows
|
s.SupportTakeProfit[i].Bind(session, s.orderExecutor)
|
||||||
minDistance := s.BounceShort.MinDistance.Float64()
|
|
||||||
closePrice := lastKLine.Close.Float64()
|
|
||||||
s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows)
|
|
||||||
log.Infof("last price: %f, possible resistance prices: %+v", closePrice, s.resistancePrices)
|
|
||||||
|
|
||||||
if len(s.resistancePrices) > 0 {
|
|
||||||
resistancePrice := fixedpoint.NewFromFloat(s.resistancePrices[0])
|
|
||||||
if resistancePrice.Compare(s.currentBounceShortPrice) != 0 {
|
|
||||||
log.Infof("updating resistance price... possible resistance prices: %+v", s.resistancePrices)
|
|
||||||
|
|
||||||
_ = s.orderExecutor.GracefulCancel(ctx)
|
|
||||||
|
|
||||||
s.currentBounceShortPrice = resistancePrice
|
|
||||||
s.placeBounceSellOrders(ctx, s.currentBounceShortPrice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always check whether you can open a short position or not
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
defer wg.Done()
|
||||||
if s.Status != types.StrategyStatusRunning {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if kline.Symbol != s.Symbol || kline.Interval != types.Interval1m {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.Position.IsClosed() && !s.Position.IsDust(kline.Close) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.pivotLowPrices) == 0 {
|
|
||||||
log.Infof("currently there is no pivot low prices, skip placing orders...")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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:]
|
|
||||||
}
|
|
||||||
|
|
||||||
ratio := fixedpoint.One.Add(s.BreakLow.Ratio)
|
|
||||||
breakPrice := previousLow.Mul(ratio)
|
|
||||||
|
|
||||||
openPrice := kline.Open
|
|
||||||
closePrice := kline.Close
|
|
||||||
// if previous low is not break, skip
|
|
||||||
if closePrice.Compare(breakPrice) >= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need the price cross the break line
|
|
||||||
// or we do nothing
|
|
||||||
if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64())
|
|
||||||
|
|
||||||
// stop EMA protection
|
|
||||||
if s.stopEWMA != nil && !s.BreakLow.StopEMARange.IsZero() {
|
|
||||||
ema := fixedpoint.NewFromFloat(s.stopEWMA.Last())
|
|
||||||
if ema.IsZero() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.BreakLow.StopEMARange))
|
|
||||||
if closePrice.Compare(emaStopShortPrice) < 0 {
|
|
||||||
log.Infof("stopEMA protection: close price %f < EMA(%v) = %f", closePrice.Float64(), s.BreakLow.StopEMA, ema.Float64())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = s.orderExecutor.GracefulCancel(ctx)
|
|
||||||
|
|
||||||
quantity := s.useQuantityOrBaseBalance(s.BreakLow.Quantity)
|
|
||||||
if s.BreakLow.MarketOrder {
|
|
||||||
bbgo.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())
|
|
||||||
s.placeMarketSell(ctx, quantity, "breakLowMarket")
|
|
||||||
} else {
|
|
||||||
sellPrice := previousLow.Mul(fixedpoint.One.Add(s.BreakLow.BounceRatio))
|
|
||||||
|
|
||||||
bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f", s.Symbol, kline.Close.Float64(), previousLow.Float64(), s.BreakLow.Ratio.Float64(), sellPrice.Float64())
|
|
||||||
s.placeLimitSell(ctx, sellPrice, quantity, "breakLowLimit")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
|
||||||
// StrategyController
|
|
||||||
if s.Status != types.StrategyStatusRunning {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.BounceShort == nil || !s.BounceShort.Enabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if kline.Symbol != s.Symbol || kline.Interval != s.BounceShort.Interval {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.resistancePivot != nil {
|
|
||||||
closePrice := kline.Close.Float64()
|
|
||||||
minDistance := s.BounceShort.MinDistance.Float64()
|
|
||||||
lows := s.resistancePivot.Lows
|
|
||||||
s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows)
|
|
||||||
|
|
||||||
if len(s.resistancePrices) > 0 {
|
|
||||||
resistancePrice := fixedpoint.NewFromFloat(s.resistancePrices[0])
|
|
||||||
if resistancePrice.Compare(s.currentBounceShortPrice) != 0 {
|
|
||||||
log.Infof("updating resistance price... possible resistance prices: %+v", s.resistancePrices)
|
|
||||||
|
|
||||||
_ = s.orderExecutor.GracefulCancel(ctx)
|
|
||||||
|
|
||||||
s.currentBounceShortPrice = resistancePrice
|
|
||||||
s.placeBounceSellOrders(ctx, s.currentBounceShortPrice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if !bbgo.IsBackTesting {
|
|
||||||
// use market trade to submit short order
|
|
||||||
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
|
||||||
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
|
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
|
||||||
wg.Done()
|
_ = s.orderExecutor.GracefulCancel(ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) findHigherPivotLow(price fixedpoint.Value) (fixedpoint.Value, bool) {
|
func preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataStore) *types.KLine {
|
||||||
for l := len(s.pivotLowPrices) - 1; l > 0; l-- {
|
|
||||||
if s.pivotLowPrices[l].Compare(price) > 0 {
|
|
||||||
return s.pivotLowPrices[l], true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return price, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Strategy) placeBounceSellOrders(ctx context.Context, resistancePrice fixedpoint.Value) {
|
|
||||||
futuresMode := s.session.Futures || s.session.IsolatedFutures
|
|
||||||
totalQuantity := s.BounceShort.Quantity
|
|
||||||
numLayers := s.BounceShort.NumLayers
|
|
||||||
if numLayers == 0 {
|
|
||||||
numLayers = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
numLayersF := fixedpoint.NewFromInt(int64(numLayers))
|
|
||||||
|
|
||||||
layerSpread := s.BounceShort.LayerSpread
|
|
||||||
quantity := totalQuantity.Div(numLayersF)
|
|
||||||
|
|
||||||
log.Infof("placing bounce short orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers)
|
|
||||||
|
|
||||||
for i := 0; i < numLayers; i++ {
|
|
||||||
balances := s.session.GetAccount().Balances()
|
|
||||||
quoteBalance := balances[s.Market.QuoteCurrency]
|
|
||||||
baseBalance := balances[s.Market.BaseCurrency]
|
|
||||||
|
|
||||||
// price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i)
|
|
||||||
price := resistancePrice.Mul(fixedpoint.One.Add(s.BounceShort.Ratio))
|
|
||||||
spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i)))
|
|
||||||
price = price.Add(spread)
|
|
||||||
log.Infof("price = %f", price.Float64())
|
|
||||||
|
|
||||||
log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64())
|
|
||||||
|
|
||||||
if futuresMode {
|
|
||||||
if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 {
|
|
||||||
s.placeOrder(ctx, price, quantity)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if quantity.Compare(baseBalance.Available) <= 0 {
|
|
||||||
s.placeOrder(ctx, price, quantity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quantity fixedpoint.Value) {
|
|
||||||
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
|
||||||
Symbol: s.Symbol,
|
|
||||||
Side: types.SideTypeSell,
|
|
||||||
Type: types.OrderTypeLimitMaker,
|
|
||||||
Price: price,
|
|
||||||
Quantity: quantity,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Strategy) preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataStore) *types.KLine {
|
|
||||||
klines, ok := store.KLinesOfInterval(pivot.Interval)
|
klines, ok := store.KLinesOfInterval(pivot.Interval)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
|
@ -487,31 +300,7 @@ func (s *Strategy) preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataSt
|
||||||
pivot.Update((*klines)[0 : i+1])
|
pivot.Update((*klines)[0 : i+1])
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("found %s %v previous lows: %v", s.Symbol, pivot.IntervalWindow, pivot.Lows)
|
log.Debugf("found %v previous lows: %v", pivot.IntervalWindow, pivot.Lows)
|
||||||
log.Infof("found %s %v previous highs: %v", s.Symbol, pivot.IntervalWindow, pivot.Highs)
|
log.Debugf("found %v previous highs: %v", pivot.IntervalWindow, pivot.Highs)
|
||||||
return &last
|
return &last
|
||||||
}
|
}
|
||||||
|
|
||||||
func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 {
|
|
||||||
// sort float64 in increasing order
|
|
||||||
sort.Float64s(lows)
|
|
||||||
|
|
||||||
var resistancePrices []float64
|
|
||||||
for _, low := range lows {
|
|
||||||
if low < closePrice {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
last := closePrice
|
|
||||||
if len(resistancePrices) > 0 {
|
|
||||||
last = resistancePrices[len(resistancePrices)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (low / last) < (1.0 + minDistance) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
resistancePrices = append(resistancePrices, low)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resistancePrices
|
|
||||||
}
|
|
||||||
|
|
|
@ -29,9 +29,6 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
*bbgo.Notifiability
|
|
||||||
|
|
||||||
Environment *bbgo.Environment
|
Environment *bbgo.Environment
|
||||||
StandardIndicatorSet *bbgo.StandardIndicatorSet
|
StandardIndicatorSet *bbgo.StandardIndicatorSet
|
||||||
Market types.Market
|
Market types.Market
|
||||||
|
@ -416,7 +413,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.TradeStats == nil {
|
if s.TradeStats == nil {
|
||||||
s.TradeStats = &types.TradeStats{}
|
s.TradeStats = types.NewTradeStats(s.Symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initial required information
|
// initial required information
|
||||||
|
|
|
@ -33,7 +33,6 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
*bbgo.Persistence
|
*bbgo.Persistence
|
||||||
|
|
||||||
Environment *bbgo.Environment
|
Environment *bbgo.Environment
|
||||||
|
@ -430,7 +429,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
})
|
})
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
close(s.stopC)
|
close(s.stopC)
|
||||||
|
|
||||||
|
|
|
@ -134,7 +134,6 @@ func (control *TrailingStopControl) GenerateStopOrder(quantity fixedpoint.Value)
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Persistence `json:"-"`
|
*bbgo.Persistence `json:"-"`
|
||||||
*bbgo.Environment `json:"-"`
|
*bbgo.Environment `json:"-"`
|
||||||
*bbgo.Graceful `json:"-"`
|
|
||||||
|
|
||||||
session *bbgo.ExchangeSession
|
session *bbgo.ExchangeSession
|
||||||
|
|
||||||
|
@ -335,7 +334,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
|
|
||||||
// trade stats
|
// trade stats
|
||||||
if s.TradeStats == nil {
|
if s.TradeStats == nil {
|
||||||
s.TradeStats = &types.TradeStats{}
|
s.TradeStats = types.NewTradeStats(s.Symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
|
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
|
||||||
|
@ -582,7 +581,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
// Cancel trailing stop order
|
// Cancel trailing stop order
|
||||||
|
|
|
@ -30,7 +30,6 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
*bbgo.Persistence
|
*bbgo.Persistence
|
||||||
|
|
||||||
Environment *bbgo.Environment
|
Environment *bbgo.Environment
|
||||||
|
@ -377,7 +376,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
close(s.stopC)
|
close(s.stopC)
|
||||||
|
|
||||||
|
|
|
@ -135,8 +135,6 @@ func (a *Address) UnmarshalJSON(body []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
|
|
||||||
Interval types.Duration `json:"interval"`
|
Interval types.Duration `json:"interval"`
|
||||||
|
|
||||||
Addresses map[string]Address `json:"addresses"`
|
Addresses map[string]Address `json:"addresses"`
|
||||||
|
@ -342,7 +340,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
|
||||||
s.State = s.newDefaultState()
|
s.State = s.newDefaultState()
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,6 @@ func (s *State) Reset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
*bbgo.Persistence
|
*bbgo.Persistence
|
||||||
|
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
|
@ -193,7 +192,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
close(s.stopC)
|
close(s.stopC)
|
||||||
|
|
|
@ -33,7 +33,6 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
*bbgo.Persistence
|
*bbgo.Persistence
|
||||||
Environment *bbgo.Environment
|
Environment *bbgo.Environment
|
||||||
|
|
||||||
|
@ -879,7 +878,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
close(s.stopC)
|
close(s.stopC)
|
||||||
|
|
|
@ -58,7 +58,6 @@ func (s *State) Reset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
*bbgo.Graceful
|
|
||||||
*bbgo.Persistence
|
*bbgo.Persistence
|
||||||
*bbgo.Environment
|
*bbgo.Environment
|
||||||
|
|
||||||
|
@ -180,7 +179,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
s.SaveState()
|
s.SaveState()
|
||||||
|
|
|
@ -74,6 +74,7 @@ func ValidExchangeName(a string) (ExchangeName, error) {
|
||||||
return "", fmt.Errorf("invalid exchange name: %s", a)
|
return "", fmt.Errorf("invalid exchange name: %s", a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:generate mockgen -destination=mocks/mock_exchange.go -package=mocks . Exchange
|
||||||
type Exchange interface {
|
type Exchange interface {
|
||||||
Name() ExchangeName
|
Name() ExchangeName
|
||||||
|
|
||||||
|
|
|
@ -11,15 +11,18 @@ import (
|
||||||
// Super basic Series type that simply holds the float64 data
|
// Super basic Series type that simply holds the float64 data
|
||||||
// with size limit (the only difference compare to float64slice)
|
// with size limit (the only difference compare to float64slice)
|
||||||
type Queue struct {
|
type Queue struct {
|
||||||
|
SeriesBase
|
||||||
arr []float64
|
arr []float64
|
||||||
size int
|
size int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQueue(size int) *Queue {
|
func NewQueue(size int) *Queue {
|
||||||
return &Queue{
|
out := &Queue{
|
||||||
arr: make([]float64, 0, size),
|
arr: make([]float64, 0, size),
|
||||||
size: size,
|
size: size,
|
||||||
}
|
}
|
||||||
|
out.SeriesBase.Series = out
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *Queue) Last() float64 {
|
func (inc *Queue) Last() float64 {
|
||||||
|
@ -47,7 +50,7 @@ func (inc *Queue) Update(v float64) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Series = &Queue{}
|
var _ SeriesExtend = &Queue{}
|
||||||
|
|
||||||
// Float64Indicator is the indicators (SMA and EWMA) that we want to use are returning float64 data.
|
// Float64Indicator is the indicators (SMA and EWMA) that we want to use are returning float64 data.
|
||||||
type Float64Indicator interface {
|
type Float64Indicator interface {
|
||||||
|
@ -82,24 +85,24 @@ type SeriesExtend interface {
|
||||||
Array(limit ...int) (result []float64)
|
Array(limit ...int) (result []float64)
|
||||||
Reverse(limit ...int) (result Float64Slice)
|
Reverse(limit ...int) (result Float64Slice)
|
||||||
Change(offset ...int) SeriesExtend
|
Change(offset ...int) SeriesExtend
|
||||||
Stdev(length int) float64
|
PercentageChange(offset ...int) SeriesExtend
|
||||||
|
Stdev(params ...int) float64
|
||||||
|
Rolling(window int) *RollingResult
|
||||||
|
Shift(offset int) SeriesExtend
|
||||||
|
Skew(length int) float64
|
||||||
|
Variance(length int) float64
|
||||||
|
Covariance(b Series, length int) float64
|
||||||
|
Correlation(b Series, length int, method ...CorrFunc) float64
|
||||||
|
Rank(length int) SeriesExtend
|
||||||
}
|
}
|
||||||
|
|
||||||
type IndexFuncType func(int) float64
|
|
||||||
type LastFuncType func() float64
|
|
||||||
type LengthFuncType func() int
|
|
||||||
|
|
||||||
type SeriesBase struct {
|
type SeriesBase struct {
|
||||||
index IndexFuncType
|
Series
|
||||||
last LastFuncType
|
|
||||||
length LengthFuncType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSeries(a Series) SeriesExtend {
|
func NewSeries(a Series) SeriesExtend {
|
||||||
return &SeriesBase{
|
return &SeriesBase{
|
||||||
index: a.Index,
|
Series: a,
|
||||||
last: a.Last,
|
|
||||||
length: a.Length,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +111,11 @@ type UpdatableSeries interface {
|
||||||
Update(float64)
|
Update(float64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdatableSeriesExtend interface {
|
||||||
|
SeriesExtend
|
||||||
|
Update(float64)
|
||||||
|
}
|
||||||
|
|
||||||
// The interface maps to pinescript basic type `series` for bool type
|
// The interface maps to pinescript basic type `series` for bool type
|
||||||
// Access the internal historical data from the latest to the oldest
|
// Access the internal historical data from the latest to the oldest
|
||||||
// Index(0) always maps to Last()
|
// Index(0) always maps to Last()
|
||||||
|
@ -595,14 +603,282 @@ func Change(a Series, offset ...int) SeriesExtend {
|
||||||
return NewSeries(&ChangeResult{a, o})
|
return NewSeries(&ChangeResult{a, o})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Stdev(a Series, length int) float64 {
|
type PercentageChangeResult struct {
|
||||||
|
a Series
|
||||||
|
offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PercentageChangeResult) Last() float64 {
|
||||||
|
if c.offset >= c.a.Length() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return c.a.Last()/c.a.Index(c.offset) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PercentageChangeResult) Index(i int) float64 {
|
||||||
|
if i+c.offset >= c.a.Length() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return c.a.Index(i)/c.a.Index(i+c.offset) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PercentageChangeResult) Length() int {
|
||||||
|
length := c.a.Length()
|
||||||
|
if length >= c.offset {
|
||||||
|
return length - c.offset
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Percentage change between current and a prior element, a / a[offset] - 1.
|
||||||
|
// offset: if not give, offset is 1.
|
||||||
|
func PercentageChange(a Series, offset ...int) SeriesExtend {
|
||||||
|
o := 1
|
||||||
|
if len(offset) > 0 {
|
||||||
|
o = offset[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewSeries(&PercentageChangeResult{a, o})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stdev(a Series, params ...int) float64 {
|
||||||
|
length := a.Length()
|
||||||
|
if len(params) > 0 {
|
||||||
|
if params[0] < length {
|
||||||
|
length = params[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ddof := 0
|
||||||
|
if len(params) > 1 {
|
||||||
|
ddof = params[1]
|
||||||
|
}
|
||||||
avg := Mean(a, length)
|
avg := Mean(a, length)
|
||||||
s := .0
|
s := .0
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < length; i++ {
|
||||||
diff := a.Index(i) - avg
|
diff := a.Index(i) - avg
|
||||||
s += diff * diff
|
s += diff * diff
|
||||||
}
|
}
|
||||||
return math.Sqrt(s / float64(length))
|
return math.Sqrt(s / float64(length-ddof))
|
||||||
|
}
|
||||||
|
|
||||||
|
type CorrFunc func(Series, Series, int) float64
|
||||||
|
|
||||||
|
func Kendall(a, b Series, length int) float64 {
|
||||||
|
if a.Length() < length {
|
||||||
|
length = a.Length()
|
||||||
|
}
|
||||||
|
if b.Length() < length {
|
||||||
|
length = b.Length()
|
||||||
|
}
|
||||||
|
aRanks := Rank(a, length)
|
||||||
|
bRanks := Rank(b, length)
|
||||||
|
concordant, discordant := 0, 0
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
for j := i + 1; j < length; j++ {
|
||||||
|
value := (aRanks.Index(i) - aRanks.Index(j)) * (bRanks.Index(i) - bRanks.Index(j))
|
||||||
|
if value > 0 {
|
||||||
|
concordant++
|
||||||
|
} else {
|
||||||
|
discordant++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return float64(concordant-discordant) * 2.0 / float64(length*(length-1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rank(a Series, length int) SeriesExtend {
|
||||||
|
if length > a.Length() {
|
||||||
|
length = a.Length()
|
||||||
|
}
|
||||||
|
rank := make([]float64, length)
|
||||||
|
mapper := make([]float64, length+1)
|
||||||
|
for i := length - 1; i >= 0; i-- {
|
||||||
|
ii := a.Index(i)
|
||||||
|
counter := 0.
|
||||||
|
for j := 0; j < length; j++ {
|
||||||
|
if a.Index(j) <= ii {
|
||||||
|
counter += 1.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rank[i] = counter
|
||||||
|
mapper[int(counter)] += 1.
|
||||||
|
}
|
||||||
|
output := NewQueue(length)
|
||||||
|
for i := length - 1; i >= 0; i-- {
|
||||||
|
output.Update(rank[i] - (mapper[int(rank[i])]-1.)/2)
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pearson(a, b Series, length int) float64 {
|
||||||
|
if a.Length() < length {
|
||||||
|
length = a.Length()
|
||||||
|
}
|
||||||
|
if b.Length() < length {
|
||||||
|
length = b.Length()
|
||||||
|
}
|
||||||
|
x := make([]float64, length)
|
||||||
|
y := make([]float64, length)
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
x[i] = a.Index(i)
|
||||||
|
y[i] = b.Index(i)
|
||||||
|
}
|
||||||
|
return stat.Correlation(x, y, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Spearman(a, b Series, length int) float64 {
|
||||||
|
if a.Length() < length {
|
||||||
|
length = a.Length()
|
||||||
|
}
|
||||||
|
if b.Length() < length {
|
||||||
|
length = b.Length()
|
||||||
|
}
|
||||||
|
aRank := Rank(a, length)
|
||||||
|
bRank := Rank(b, length)
|
||||||
|
return Pearson(aRank, bRank, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// similar to pandas.Series.corr() function.
|
||||||
|
//
|
||||||
|
// method could either be `types.Pearson`, `types.Spearman` or `types.Kendall`
|
||||||
|
func Correlation(a Series, b Series, length int, method ...CorrFunc) float64 {
|
||||||
|
var runner CorrFunc
|
||||||
|
if len(method) == 0 {
|
||||||
|
runner = Pearson
|
||||||
|
} else {
|
||||||
|
runner = method[0]
|
||||||
|
}
|
||||||
|
return runner(a, b, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// similar to pandas.Series.cov() function with ddof=0
|
||||||
|
//
|
||||||
|
// Compute covariance with Series
|
||||||
|
func Covariance(a Series, b Series, length int) float64 {
|
||||||
|
if a.Length() < length {
|
||||||
|
length = a.Length()
|
||||||
|
}
|
||||||
|
if b.Length() < length {
|
||||||
|
length = b.Length()
|
||||||
|
}
|
||||||
|
|
||||||
|
meana := Mean(a, length)
|
||||||
|
meanb := Mean(b, length)
|
||||||
|
sum := 0.0
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
sum += (a.Index(i) - meana) * (b.Index(i) - meanb)
|
||||||
|
}
|
||||||
|
sum /= float64(length)
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
func Variance(a Series, length int) float64 {
|
||||||
|
return Covariance(a, a, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// similar to pandas.Series.skew() function.
|
||||||
|
//
|
||||||
|
// Return unbiased skew over input series
|
||||||
|
func Skew(a Series, length int) float64 {
|
||||||
|
if length > a.Length() {
|
||||||
|
length = a.Length()
|
||||||
|
}
|
||||||
|
mean := Mean(a, length)
|
||||||
|
sum2 := 0.0
|
||||||
|
sum3 := 0.0
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
diff := a.Index(i) - mean
|
||||||
|
sum2 += diff * diff
|
||||||
|
sum3 += diff * diff * diff
|
||||||
|
}
|
||||||
|
if length <= 2 || sum2 == 0 {
|
||||||
|
return math.NaN()
|
||||||
|
}
|
||||||
|
l := float64(length)
|
||||||
|
return l * math.Sqrt(l-1) / (l - 2) * sum3 / math.Pow(sum2, 1.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShiftResult struct {
|
||||||
|
a Series
|
||||||
|
offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *ShiftResult) Last() float64 {
|
||||||
|
if inc.offset < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if inc.offset > inc.a.Length() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return inc.a.Index(inc.offset)
|
||||||
|
}
|
||||||
|
func (inc *ShiftResult) Index(i int) float64 {
|
||||||
|
if inc.offset+i < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if inc.offset+i > inc.a.Length() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return inc.a.Index(inc.offset + i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *ShiftResult) Length() int {
|
||||||
|
return inc.a.Length() - inc.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func Shift(a Series, offset int) SeriesExtend {
|
||||||
|
return NewSeries(&ShiftResult{a, offset})
|
||||||
|
}
|
||||||
|
|
||||||
|
type RollingResult struct {
|
||||||
|
a Series
|
||||||
|
window int
|
||||||
|
}
|
||||||
|
|
||||||
|
type SliceView struct {
|
||||||
|
a Series
|
||||||
|
start int
|
||||||
|
length int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SliceView) Last() float64 {
|
||||||
|
return s.a.Index(s.start)
|
||||||
|
}
|
||||||
|
func (s *SliceView) Index(i int) float64 {
|
||||||
|
if i >= s.length {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return s.a.Index(i + s.start)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SliceView) Length() int {
|
||||||
|
return s.length
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Series = &SliceView{}
|
||||||
|
|
||||||
|
func (r *RollingResult) Last() SeriesExtend {
|
||||||
|
return NewSeries(&SliceView{r.a, 0, r.window})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RollingResult) Index(i int) SeriesExtend {
|
||||||
|
if i*r.window > r.a.Length() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return NewSeries(&SliceView{r.a, i * r.window, r.window})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RollingResult) Length() int {
|
||||||
|
mod := r.a.Length() % r.window
|
||||||
|
if mod > 0 {
|
||||||
|
return r.a.Length()/r.window + 1
|
||||||
|
} else {
|
||||||
|
return r.a.Length() / r.window
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rolling(a Series, window int) *RollingResult {
|
||||||
|
return &RollingResult{a, window}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: ta.linreg
|
// TODO: ta.linreg
|
||||||
|
|
|
@ -33,3 +33,54 @@ func TestFloat64Slice(t *testing.T) {
|
||||||
b = append(b, 3.0)
|
b = append(b, 3.0)
|
||||||
assert.Equal(t, c.Last(), 1.)
|
assert.Equal(t, c.Last(), 1.)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
python
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
s1 = pd.Series([.2, 0., .6, .2, .2])
|
||||||
|
s2 = pd.Series([.3, .6, .0, .1])
|
||||||
|
print(s1.corr(s2, method='pearson'))
|
||||||
|
print(s1.corr(s2, method='spearman')
|
||||||
|
print(s1.corr(s2, method='kendall'))
|
||||||
|
print(s1.rank())
|
||||||
|
*/
|
||||||
|
func TestCorr(t *testing.T) {
|
||||||
|
var a = Float64Slice{.2, .0, .6, .2}
|
||||||
|
var b = Float64Slice{.3, .6, .0, .1}
|
||||||
|
corr := Correlation(&a, &b, 4, Pearson)
|
||||||
|
assert.InDelta(t, corr, -0.8510644, 0.001)
|
||||||
|
out := Rank(&a, 4)
|
||||||
|
assert.Equal(t, out.Index(0), 2.5)
|
||||||
|
assert.Equal(t, out.Index(1), 4.0)
|
||||||
|
corr = Correlation(&a, &b, 4, Spearman)
|
||||||
|
assert.InDelta(t, corr, -0.94868, 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
python
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
s1 = pd.Series([.2, 0., .6, .2, .2])
|
||||||
|
s2 = pd.Series([.3, .6, .0, .1])
|
||||||
|
print(s1.cov(s2, ddof=0))
|
||||||
|
*/
|
||||||
|
func TestCov(t *testing.T) {
|
||||||
|
var a = Float64Slice{.2, .0, .6, .2}
|
||||||
|
var b = Float64Slice{.3, .6, .0, .1}
|
||||||
|
cov := Covariance(&a, &b, 4)
|
||||||
|
assert.InDelta(t, cov, -0.042499, 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
python
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
s1 = pd.Series([.2, 0., .6, .2, .2])
|
||||||
|
print(s1.skew())
|
||||||
|
*/
|
||||||
|
func TestSkew(t *testing.T) {
|
||||||
|
var a = Float64Slice{.2, .0, .6, .2}
|
||||||
|
sk := Skew(&a, 4)
|
||||||
|
assert.InDelta(t, sk, 1.129338, 0.001)
|
||||||
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user