mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 06:53:52 +00:00
Merge branch 'main' into improve/supertrend-strategy
This commit is contained in:
commit
f8777752a0
158
README.md
158
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 🧑💻
|
||||
|
||||
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
|
||||
|
||||
|
@ -44,37 +45,38 @@ You can use BBGO's underlying common exchange API, currently it supports 4+ majo
|
|||
- Built-in parameter optimization tool.
|
||||
- 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.
|
||||
- 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)
|
||||
- [Arnaud Legoux Moving Average](./pkg/indicator/alma.go)
|
||||
- [Average True Range](./pkg/indicator/atr.go)
|
||||
- [Bollinger Bands](./pkg/indicator/boll.go)
|
||||
- [Commodity Channel Index](./pkg/indicator/cci.go)
|
||||
- [Cumulative Moving Average](./pkg/indicator/cma.go)
|
||||
- [Double Exponential Moving Average](./pkg/indicator/dema.go)
|
||||
- [Directional Movement Index](./pkg/indicator/dmi.go)
|
||||
- [Brownian Motion's Drift Factor](./pkg/indicator/drift.go)
|
||||
- [Ease of Movement](./pkg/indicator/emv.go)
|
||||
- [Exponentially Weighted Moving Average](./pkg/indicator/ewma.go)
|
||||
- [Hull Moving Average](./pkg/indicator/hull.go)
|
||||
- [Trend Line (Tool)](./pkg/indicator/line.go)
|
||||
- [Moving Average Convergence Divergence Indicator](./pkg/indicator/macd.go)
|
||||
- [On-Balance Volume](./pkg/indicator/obv.go)
|
||||
- [Pivot](./pkg/indicator/pivot.go)
|
||||
- [Running Moving Average](./pkg/indicator/rma.go)
|
||||
- [Relative Strength Index](./pkg/indicator/rsi.go)
|
||||
- [Simple Moving Average](./pkg/indicator/sma.go)
|
||||
- [Ehler's Super Smoother Filter](./pkg/indicator/ssf.go)
|
||||
- [Stochastic Oscillator](./pkg/indicator/stoch.go)
|
||||
- [SuperTrend](./pkg/indicator/supertrend.go)
|
||||
- [Triple Exponential Moving Average](./pkg/indicator/tema.go)
|
||||
- [Tillson T3 Moving Average](./pkg/indicator/till.go)
|
||||
- [Triangular Moving Average](./pkg/indicator/tma.go)
|
||||
- [Variable Index Dynamic Average](./pkg/indicator/vidya.go)
|
||||
- [Volatility Indicator](./pkg/indicator/volatility.go)
|
||||
- [Volume Weighted Average Price](./pkg/indicator/vwap.go)
|
||||
- [Zero Lag Exponential Moving Average](./pkg/indicator/zlema.go)
|
||||
- And more...
|
||||
- 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)
|
||||
- [Arnaud Legoux Moving Average](./pkg/indicator/alma.go)
|
||||
- [Average True Range](./pkg/indicator/atr.go)
|
||||
- [Bollinger Bands](./pkg/indicator/boll.go)
|
||||
- [Commodity Channel Index](./pkg/indicator/cci.go)
|
||||
- [Cumulative Moving Average](./pkg/indicator/cma.go)
|
||||
- [Double Exponential Moving Average](./pkg/indicator/dema.go)
|
||||
- [Directional Movement Index](./pkg/indicator/dmi.go)
|
||||
- [Brownian Motion's Drift Factor](./pkg/indicator/drift.go)
|
||||
- [Ease of Movement](./pkg/indicator/emv.go)
|
||||
- [Exponentially Weighted Moving Average](./pkg/indicator/ewma.go)
|
||||
- [Hull Moving Average](./pkg/indicator/hull.go)
|
||||
- [Trend Line (Tool)](./pkg/indicator/line.go)
|
||||
- [Moving Average Convergence Divergence Indicator](./pkg/indicator/macd.go)
|
||||
- [On-Balance Volume](./pkg/indicator/obv.go)
|
||||
- [Pivot](./pkg/indicator/pivot.go)
|
||||
- [Running Moving Average](./pkg/indicator/rma.go)
|
||||
- [Relative Strength Index](./pkg/indicator/rsi.go)
|
||||
- [Simple Moving Average](./pkg/indicator/sma.go)
|
||||
- [Ehler's Super Smoother Filter](./pkg/indicator/ssf.go)
|
||||
- [Stochastic Oscillator](./pkg/indicator/stoch.go)
|
||||
- [SuperTrend](./pkg/indicator/supertrend.go)
|
||||
- [Triple Exponential Moving Average](./pkg/indicator/tema.go)
|
||||
- [Tillson T3 Moving Average](./pkg/indicator/till.go)
|
||||
- [Triangular Moving Average](./pkg/indicator/tma.go)
|
||||
- [Variable Index Dynamic Average](./pkg/indicator/vidya.go)
|
||||
- [Volatility Indicator](./pkg/indicator/volatility.go)
|
||||
- [Volume Weighted Average Price](./pkg/indicator/vwap.go)
|
||||
- [Zero Lag Exponential Moving Average](./pkg/indicator/zlema.go)
|
||||
- And more...
|
||||
- HeikinAshi OHLC / Normal OHLC (check [this config](https://github.com/c9s/bbgo/blob/main/config/skeleton.yaml#L5))
|
||||
- React-powered Web Dashboard.
|
||||
- Docker image ready.
|
||||
|
@ -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>
|
||||
- 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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
Since v2, we've added new float point implementation from dnum to support decimals with higher precision.
|
||||
To download & setup, please refer to [Dnum Installation](doc/topics/dnum-binary.md)
|
||||
Since v2, we've added new float point implementation from dnum to support decimals with higher precision. To download &
|
||||
setup, please refer to [Dnum Installation](doc/topics/dnum-binary.md)
|
||||
|
||||
### One-click Linode StackScript
|
||||
|
||||
|
@ -241,8 +244,8 @@ bbgo pnl --exchange binance --asset BTC --since "2019-01-01"
|
|||
|
||||
### Testnet (Paper Trading)
|
||||
|
||||
Currently only supports binance testnet.
|
||||
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:
|
||||
Currently only supports binance testnet. 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:
|
||||
|
||||
```bash
|
||||
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)
|
||||
- `grid` strategy implements the fixed price band grid strategy [grid](pkg/strategy/grid). See
|
||||
[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).
|
||||
- `support` strategy uses K-lines with high volume as support [support](pkg/strategy/support). See
|
||||
[document](./doc/strategy/support.md).
|
||||
|
@ -365,78 +369,9 @@ bbgo run --config config/buyandhold.yaml
|
|||
|
||||
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
|
||||
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"
|
||||
)
|
||||
```
|
||||
See [Developing Strategy](./doc/topics/developing-strategy.md)
|
||||
|
||||
## Write your own private strategy
|
||||
|
||||
|
@ -635,8 +570,9 @@ What's Position?
|
|||
|
||||
## 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).
|
||||
If you're interested in, DM me in telegram <https://t.me/c123456789s> or twitter <https://twitter.com/c9s>, we can discuss.
|
||||
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). If you're interested in, DM me in telegram <https://t.me/c123456789s> or
|
||||
twitter <https://twitter.com/c9s>, we can discuss.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
@ -39,26 +39,30 @@ exchangeStrategies:
|
|||
|
||||
# stopEMARange is the price range we allow short.
|
||||
# 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:
|
||||
interval: 1h
|
||||
window: 99
|
||||
|
||||
bounceShort:
|
||||
enabled: false
|
||||
interval: 1h
|
||||
window: 10
|
||||
resistanceShort:
|
||||
enabled: true
|
||||
interval: 5m
|
||||
window: 80
|
||||
|
||||
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,
|
||||
# higher the ratio, lower the price
|
||||
# first_layer_price = resistance_price * (1 - ratio)
|
||||
# second_layer_price = (resistance_price * (1 - ratio)) * (2 * layerSpread)
|
||||
ratio: 0%
|
||||
numOfLayers: 1
|
||||
layerSpread: 0.1%
|
||||
# higher the ratio, higher the sell price
|
||||
# first_layer_price = resistance_price * (1 + ratio)
|
||||
# second_layer_price = (resistance_price * (1 + ratio)) * (2 * layerSpread)
|
||||
ratio: 1.5%
|
||||
numOfLayers: 3
|
||||
layerSpread: 0.4%
|
||||
|
||||
exits:
|
||||
# (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:
|
||||
# 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:
|
||||
interval: 30m
|
||||
window: 99
|
||||
ratio: 3%
|
||||
|
||||
# (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold
|
||||
- cumulatedVolumeTakeProfit:
|
||||
minQuoteVolume: 100_000_000
|
||||
interval: 5m
|
||||
window: 2
|
||||
minQuoteVolume: 200_000_000
|
||||
|
||||
backtest:
|
||||
sessions:
|
||||
- binance
|
||||
startTime: "2022-04-01"
|
||||
startTime: "2022-01-01"
|
||||
endTime: "2022-06-18"
|
||||
symbols:
|
||||
- ETHUSDT
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
* [Support](strategy/support.md) - Support strategy that buys on high volume support
|
||||
|
||||
### Development
|
||||
* [Developing Strategy](topics/developing-strategy.md) - developing strategy
|
||||
* [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
|
||||
* [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
|
||||
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
|
||||
|
||||
|
@ -28,7 +112,7 @@ externalStrategies:
|
|||
|
||||
You can write the following struct to load the symbol setting:
|
||||
|
||||
```
|
||||
```go
|
||||
package short
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
```go
|
||||
func (s *Strategy) Run(ctx context.Context, session *bbgo.ExchangeSession) error {
|
||||
// you need to import the "log" package
|
||||
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,
|
||||
so you need to register 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
|
||||
strategy.
|
||||
|
||||
Define an ID const in your package:
|
||||
|
||||
```
|
||||
```go
|
||||
const ID = "short"
|
||||
```
|
||||
|
||||
Then call bbgo.RegisterStrategy with the ID you just defined and a struct reference:
|
||||
|
||||
```
|
||||
```go
|
||||
func init() {
|
||||
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.
|
||||
|
||||
(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/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // 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/hashicorp/hcl 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.uber.org/atomic v1.9.0 // 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/sys v0.0.0-20220615213510-4f61da869c0c // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // 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
|
||||
gopkg.in/ini.v1 v1.62.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.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.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
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.3.1/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.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
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/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
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.32/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/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||
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.2.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/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=
|
||||
|
@ -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-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
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.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
|
||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
|
|
|
@ -50,15 +50,15 @@ var log = logrus.WithField("cmd", "backtest")
|
|||
var ErrUnimplemented = errors.New("unimplemented method")
|
||||
|
||||
type Exchange struct {
|
||||
sourceName types.ExchangeName
|
||||
publicExchange types.Exchange
|
||||
srv *service.BacktestService
|
||||
startTime, endTime time.Time
|
||||
sourceName types.ExchangeName
|
||||
publicExchange types.Exchange
|
||||
srv *service.BacktestService
|
||||
currentTime time.Time
|
||||
|
||||
account *types.Account
|
||||
config *bbgo.Backtest
|
||||
|
||||
UserDataStream, MarketDataStream types.StandardStreamEmitter
|
||||
MarketDataStream types.StandardStreamEmitter
|
||||
|
||||
trades map[string][]types.Trade
|
||||
tradesMutex sync.Mutex
|
||||
|
@ -72,20 +72,6 @@ type Exchange struct {
|
|||
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) {
|
||||
ex := sourceExchange
|
||||
|
||||
|
@ -94,14 +80,7 @@ func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, s
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var startTime, endTime time.Time
|
||||
startTime = config.StartTime.Time()
|
||||
if config.EndTime != nil {
|
||||
endTime = config.EndTime.Time()
|
||||
} else {
|
||||
endTime = time.Now()
|
||||
}
|
||||
|
||||
startTime := config.StartTime.Time()
|
||||
configAccount := config.GetAccount(sourceName.String())
|
||||
|
||||
account := &types.Account{
|
||||
|
@ -120,8 +99,7 @@ func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, s
|
|||
srv: srv,
|
||||
config: config,
|
||||
account: account,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
currentTime: startTime,
|
||||
closedOrders: make(map[string][]types.Order),
|
||||
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) {
|
||||
e.matchingBooks[symbol] = &SimplePriceMatching{
|
||||
CurrentTime: e.startTime,
|
||||
CurrentTime: e.currentTime,
|
||||
Account: e.account,
|
||||
Market: market,
|
||||
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) {
|
||||
if e.UserDataStream == nil {
|
||||
return createdOrders, fmt.Errorf("SubmitOrders should be called after UserDataStream been initialized")
|
||||
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 (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) {
|
||||
for _, order := range orders {
|
||||
symbol := order.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:
|
||||
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 {
|
||||
if e.UserDataStream == nil {
|
||||
return fmt.Errorf("CancelOrders should be called after UserDataStream been initialized")
|
||||
}
|
||||
for _, order := range orders {
|
||||
matching, ok := e.matchingBook(order.Symbol)
|
||||
if !ok {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
e.UserDataStream.EmitOrderUpdate(canceledOrder)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -318,21 +300,21 @@ func (e *Exchange) matchingBook(symbol string) (*SimplePriceMatching, bool) {
|
|||
return m, ok
|
||||
}
|
||||
|
||||
func (e *Exchange) InitMarketData() {
|
||||
e.UserDataStream.OnTradeUpdate(func(trade types.Trade) {
|
||||
func (e *Exchange) BindUserData(userDataStream types.StandardStreamEmitter) {
|
||||
userDataStream.OnTradeUpdate(func(trade types.Trade) {
|
||||
e.addTrade(trade)
|
||||
})
|
||||
|
||||
e.matchingBooksMutex.Lock()
|
||||
for _, matching := range e.matchingBooks {
|
||||
matching.OnTradeUpdate(e.UserDataStream.EmitTradeUpdate)
|
||||
matching.OnOrderUpdate(e.UserDataStream.EmitOrderUpdate)
|
||||
matching.OnBalanceUpdate(e.UserDataStream.EmitBalanceUpdate)
|
||||
matching.OnTradeUpdate(userDataStream.EmitTradeUpdate)
|
||||
matching.OnOrderUpdate(userDataStream.EmitOrderUpdate)
|
||||
matching.OnBalanceUpdate(userDataStream.EmitBalanceUpdate)
|
||||
}
|
||||
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...")
|
||||
|
||||
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("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() {
|
||||
if err := <-errC; err != nil {
|
||||
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) {
|
||||
if k.Interval == types.Interval1m {
|
||||
e.currentTime = k.EndTime.Time()
|
||||
|
||||
matching, ok := e.matchingBook(k.Symbol)
|
||||
if !ok {
|
||||
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.mu.Unlock()
|
||||
|
||||
}
|
||||
|
||||
if !found {
|
||||
|
@ -191,6 +190,8 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
|
|||
order2.ExecutedQuantity = order2.Quantity
|
||||
order2.IsWorking = false
|
||||
|
||||
m.EmitOrderUpdate(order2)
|
||||
|
||||
// let the exchange emit the "FILLED" order update (we need the closed order)
|
||||
// m.EmitOrderUpdate(order2)
|
||||
return &order2, &trade, nil
|
||||
|
@ -570,6 +571,7 @@ func (m *SimplePriceMatching) getOrder(orderID uint64) (types.Order, bool) {
|
|||
|
||||
func (m *SimplePriceMatching) processKLine(kline types.KLine) {
|
||||
m.CurrentTime = kline.EndTime.Time()
|
||||
|
||||
if m.LastPrice.IsZero() {
|
||||
m.LastPrice = kline.Open
|
||||
} else {
|
||||
|
|
|
@ -131,8 +131,6 @@ func (b *ActiveOrderBook) orderUpdateHandler(order types.Order) {
|
|||
return
|
||||
}
|
||||
|
||||
log.Debugf("[ActiveOrderBook] received order update: %+v", order)
|
||||
|
||||
switch order.Status {
|
||||
case types.OrderStatusFilled:
|
||||
// make sure we have the order and we remove it
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/dynamic"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/service"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
@ -387,7 +388,7 @@ func (c *Config) GetSignature() string {
|
|||
id := strategy.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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,44 +3,80 @@ package bbgo
|
|||
import (
|
||||
"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 {
|
||||
RoiStopLoss *RoiStopLoss `json:"roiStopLoss"`
|
||||
ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"`
|
||||
RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"`
|
||||
LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"`
|
||||
CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"`
|
||||
TrailingStop *TrailingStop2 `json:"trailingStop"`
|
||||
}
|
||||
|
||||
func (m *ExitMethod) Subscribe() {
|
||||
// TODO: pull out this implementation as a simple function to reflect.go
|
||||
rv := reflect.ValueOf(m)
|
||||
rt := reflect.TypeOf(m)
|
||||
|
||||
rv = rv.Elem()
|
||||
rt = rt.Elem()
|
||||
infType := reflect.TypeOf((*types.Subscriber)(nil)).Elem()
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
fieldType := rt.Field(i)
|
||||
if fieldType.Type.Implements(infType) {
|
||||
method := rv.Field(i).MethodByName("Subscribe")
|
||||
method.Call(nil)
|
||||
// Inherit is used for inheriting properties from the given strategy struct
|
||||
// for example, some exit method requires the default interval and symbol name from the strategy param object
|
||||
func (m *ExitMethod) Inherit(parent interface{}) {
|
||||
// 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 := reflect.ValueOf(m).Elem()
|
||||
for j := 0; j < rv.NumField(); j++ {
|
||||
if !rt.Field(j).IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
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) {
|
||||
if m.ProtectiveStopLoss != nil {
|
||||
m.ProtectiveStopLoss.Bind(session, orderExecutor)
|
||||
} else if m.RoiStopLoss != nil {
|
||||
}
|
||||
|
||||
if m.RoiStopLoss != nil {
|
||||
m.RoiStopLoss.Bind(session, orderExecutor)
|
||||
} else if m.RoiTakeProfit != nil {
|
||||
}
|
||||
|
||||
if m.RoiTakeProfit != nil {
|
||||
m.RoiTakeProfit.Bind(session, orderExecutor)
|
||||
} else if m.LowerShadowTakeProfit != nil {
|
||||
}
|
||||
|
||||
if m.LowerShadowTakeProfit != nil {
|
||||
m.LowerShadowTakeProfit.Bind(session, orderExecutor)
|
||||
} else if m.CumulatedVolumeTakeProfit != nil {
|
||||
}
|
||||
|
||||
if m.CumulatedVolumeTakeProfit != nil {
|
||||
m.CumulatedVolumeTakeProfit.Bind(session, orderExecutor)
|
||||
}
|
||||
|
||||
if m.TrailingStop != nil {
|
||||
m.TrailingStop.Bind(session, orderExecutor)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package bbgo
|
|||
import (
|
||||
"context"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"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;
|
||||
//
|
||||
type CumulatedVolumeTakeProfit struct {
|
||||
Symbol string `json:"symbol"`
|
||||
|
||||
types.IntervalWindow
|
||||
|
||||
Ratio fixedpoint.Value `json:"ratio"`
|
||||
MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"`
|
||||
|
||||
|
@ -31,11 +36,7 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor
|
|||
|
||||
store, _ := session.MarketDataStore(position.Symbol)
|
||||
|
||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
||||
if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m {
|
||||
return
|
||||
}
|
||||
|
||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||
closePrice := kline.Close
|
||||
if position.IsClosed() || position.IsDust(closePrice) {
|
||||
return
|
||||
|
@ -46,25 +47,33 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor
|
|||
return
|
||||
}
|
||||
|
||||
if klines, ok := store.KLinesOfInterval(s.Interval); ok {
|
||||
var cbv = fixedpoint.Zero
|
||||
var cqv = fixedpoint.Zero
|
||||
for i := 0; i < s.Window; i++ {
|
||||
last := (*klines)[len(*klines)-1-i]
|
||||
cqv = cqv.Add(last.QuoteVolume)
|
||||
cbv = cbv.Add(last.Volume)
|
||||
}
|
||||
|
||||
if cqv.Compare(s.MinQuoteVolume) > 0 {
|
||||
Notify("%s TakeProfit triggered by cumulated volume (window: %d) %f > %f, price = %f",
|
||||
position.Symbol,
|
||||
s.Window,
|
||||
cqv.Float64(),
|
||||
s.MinQuoteVolume.Float64(), kline.Close.Float64())
|
||||
|
||||
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit")
|
||||
return
|
||||
}
|
||||
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 cqv = fixedpoint.Zero
|
||||
for i := 0; i < s.Window; i++ {
|
||||
last := (*klines)[len(*klines)-1-i]
|
||||
cqv = cqv.Add(last.QuoteVolume)
|
||||
cbv = cbv.Add(last.Volume)
|
||||
}
|
||||
|
||||
if cqv.Compare(s.MinQuoteVolume) > 0 {
|
||||
Notify("%s TakeProfit triggered by cumulated volume (window: %d) %f > %f, price = %f",
|
||||
position.Symbol,
|
||||
s.Window,
|
||||
cqv.Float64(),
|
||||
s.MinQuoteVolume.Float64(), kline.Close.Float64())
|
||||
|
||||
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit")
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -8,22 +8,31 @@ import (
|
|||
)
|
||||
|
||||
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
|
||||
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) {
|
||||
s.session = session
|
||||
s.orderExecutor = orderExecutor
|
||||
|
||||
position := orderExecutor.Position()
|
||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
||||
if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m {
|
||||
return
|
||||
}
|
||||
stdIndicatorSet, _ := session.StandardIndicatorSet(s.Symbol)
|
||||
ewma := stdIndicatorSet.EWMA(s.IntervalWindow)
|
||||
|
||||
|
||||
position := orderExecutor.Position()
|
||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||
closePrice := kline.Close
|
||||
if position.IsClosed() || position.IsDust(closePrice) {
|
||||
return
|
||||
|
@ -38,6 +47,11 @@ func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *Ge
|
|||
return
|
||||
}
|
||||
|
||||
// skip close price higher than the ewma
|
||||
if closePrice.Float64() > ewma.Last() {
|
||||
return
|
||||
}
|
||||
|
||||
if kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Ratio) > 0 {
|
||||
Notify("%s TakeProfit triggered by shadow ratio %f, price = %f",
|
||||
position.Symbol,
|
||||
|
@ -48,5 +62,5 @@ func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *Ge
|
|||
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One)
|
||||
return
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -8,24 +8,26 @@ import (
|
|||
)
|
||||
|
||||
type RoiStopLoss struct {
|
||||
Symbol string
|
||||
Percentage fixedpoint.Value `json:"percentage"`
|
||||
|
||||
session *ExchangeSession
|
||||
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) {
|
||||
s.session = session
|
||||
s.orderExecutor = orderExecutor
|
||||
|
||||
position := orderExecutor.Position()
|
||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
||||
if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m {
|
||||
return
|
||||
}
|
||||
|
||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) {
|
||||
s.checkStopPrice(kline.Close, position)
|
||||
})
|
||||
}))
|
||||
|
||||
if !IsBackTesting {
|
||||
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
|
||||
|
|
|
@ -4,5 +4,5 @@ import "testing"
|
|||
|
||||
func TestExitMethod(t *testing.T) {
|
||||
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 (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var graceful = &Graceful{}
|
||||
|
||||
//go:generate callbackgen -type Graceful
|
||||
type Graceful struct {
|
||||
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) {
|
||||
var wg sync.WaitGroup
|
||||
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()
|
||||
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 {
|
||||
return
|
||||
}
|
||||
tradeStats.Add(profit.Profit)
|
||||
|
||||
tradeStats.Add(profit)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -65,7 +66,7 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) {
|
|||
}
|
||||
|
||||
profitStats.AddProfit(*profit)
|
||||
Notify(&profitStats)
|
||||
Notify(profitStats)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -86,9 +87,9 @@ func (e *GeneralOrderExecutor) Bind() {
|
|||
e.tradeCollector.BindStream(e.session.UserDataStream)
|
||||
}
|
||||
|
||||
// CancelOrders cancels the given order objects directly
|
||||
func (e *GeneralOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error {
|
||||
err := e.session.Exchange.CancelOrders(ctx, orders...)
|
||||
return err
|
||||
return e.session.Exchange.CancelOrders(ctx, orders...)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error {
|
||||
if err := e.activeMakerOrders.GracefulCancel(ctx, e.session.Exchange); err != nil {
|
||||
// GracefulCancelActiveOrderBook cancels the orders from the active orderbook.
|
||||
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")
|
||||
return err
|
||||
}
|
||||
|
@ -118,6 +120,11 @@ func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error {
|
|||
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 {
|
||||
submitOrder := e.position.NewMarketCloseOrder(percentage)
|
||||
if submitOrder == nil {
|
||||
|
@ -125,7 +132,6 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix
|
|||
}
|
||||
|
||||
submitOrder.Tag = strings.Join(tags, ",")
|
||||
|
||||
_, err := e.SubmitOrders(ctx, *submitOrder)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/dynamic"
|
||||
"github.com/c9s/bbgo/pkg/service"
|
||||
)
|
||||
|
||||
|
@ -106,10 +107,10 @@ func Sync(obj interface{}) {
|
|||
}
|
||||
|
||||
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)
|
||||
|
||||
newValueInf := newTypeValueInterface(value.Type())
|
||||
newValueInf := dynamic.NewTypeValueInterface(value.Type())
|
||||
// inf := value.Interface()
|
||||
store := persistence.NewStore("state", id, tag)
|
||||
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 {
|
||||
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)
|
||||
|
||||
inf := fv.Interface()
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/dynamic"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/service"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
@ -23,7 +24,6 @@ func (s *TestStructWithoutInstanceID) ID() string {
|
|||
|
||||
type TestStruct struct {
|
||||
*Environment
|
||||
*Graceful
|
||||
|
||||
Position *types.Position `persistence:"position"`
|
||||
Integer int64 `persistence:"integer"`
|
||||
|
@ -83,7 +83,7 @@ func Test_loadPersistenceFields(t *testing.T) {
|
|||
t.Run(psName+"/nil", func(t *testing.T) {
|
||||
var b *TestStruct = nil
|
||||
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) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package bbgo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/dynamic"
|
||||
)
|
||||
|
||||
type InstanceIDProvider interface {
|
||||
|
@ -19,7 +19,7 @@ func callID(obj interface{}) string {
|
|||
return ret[0].String()
|
||||
}
|
||||
|
||||
if symbol, ok := isSymbolBasedStrategy(sv); ok {
|
||||
if symbol, ok := dynamic.LookupSymbolField(sv); ok {
|
||||
m := sv.MethodByName("ID")
|
||||
ret := m.Call(nil)
|
||||
return ret[0].String() + ":" + symbol
|
||||
|
@ -31,82 +31,3 @@ func callID(obj interface{}) 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
|
||||
// It also maintains and collects the data returned from the stream.
|
||||
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
|
||||
// ---------------------------
|
||||
|
@ -253,12 +249,6 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession {
|
|||
marketDataStream.SetPublicOnly()
|
||||
|
||||
session := &ExchangeSession{
|
||||
Notifiability: Notifiability{
|
||||
SymbolChannelRouter: NewPatternChannelRouter(nil),
|
||||
SessionChannelRouter: NewPatternChannelRouter(nil),
|
||||
ObjectChannelRouter: NewObjectChannelRouter(),
|
||||
},
|
||||
|
||||
Name: name,
|
||||
Exchange: exchange,
|
||||
UserDataStream: userDataStream,
|
||||
|
@ -282,8 +272,7 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession {
|
|||
|
||||
session.OrderExecutor = &ExchangeOrderExecutor{
|
||||
// copy the notification system so that we can route
|
||||
Notifiability: session.Notifiability,
|
||||
Session: session,
|
||||
Session: session,
|
||||
}
|
||||
|
||||
return session
|
||||
|
@ -805,11 +794,6 @@ func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) err
|
|||
}
|
||||
|
||||
session.Name = name
|
||||
session.Notifiability = Notifiability{
|
||||
SymbolChannelRouter: NewPatternChannelRouter(nil),
|
||||
SessionChannelRouter: NewPatternChannelRouter(nil),
|
||||
ObjectChannelRouter: NewObjectChannelRouter(),
|
||||
}
|
||||
session.Exchange = ex
|
||||
session.UserDataStream = ex.NewStream()
|
||||
session.MarketDataStream = ex.NewStream()
|
||||
|
@ -830,8 +814,7 @@ func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) err
|
|||
session.orderStores = make(map[string]*OrderStore)
|
||||
session.OrderExecutor = &ExchangeOrderExecutor{
|
||||
// copy the notification system so that we can route
|
||||
Notifiability: session.Notifiability,
|
||||
Session: session,
|
||||
Session: session,
|
||||
}
|
||||
|
||||
session.usedSymbols = make(map[string]struct{})
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/dynamic"
|
||||
"github.com/c9s/bbgo/pkg/interact"
|
||||
)
|
||||
|
||||
|
@ -72,8 +73,6 @@ type Trader struct {
|
|||
exchangeStrategies map[string][]SingleExchangeStrategy
|
||||
|
||||
logger Logger
|
||||
|
||||
Graceful Graceful
|
||||
}
|
||||
|
||||
func NewTrader(environ *Environment) *Trader {
|
||||
|
@ -197,11 +196,11 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si
|
|||
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)
|
||||
}
|
||||
|
||||
if symbol, ok := isSymbolBasedStrategy(rs); ok {
|
||||
if symbol, ok := dynamic.LookupSymbolField(rs); ok {
|
||||
log.Infof("found symbol based strategy from %s", rs.Type())
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if err := parseStructAndInject(strategy,
|
||||
if err := dynamic.ParseStructAndInject(strategy,
|
||||
market,
|
||||
indicatorSet,
|
||||
store,
|
||||
|
@ -394,7 +393,7 @@ func (trader *Trader) injectCommonServices(s interface{}) error {
|
|||
// 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
|
||||
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
|
||||
if !field.IsNil() {
|
||||
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)
|
||||
}
|
||||
|
||||
if err := injectField(elem, "Facade", PersistenceServiceFacade, true); err != nil {
|
||||
if err := dynamic.InjectField(elem, "Facade", PersistenceServiceFacade, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
if err := parseStructAndInject(field.Interface(), persistenceFacade); err != nil {
|
||||
if err := ParseStructAndInject(field.Interface(), persistenceFacade); err != nil {
|
||||
return err
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
return parseStructAndInject(s,
|
||||
&trader.Graceful,
|
||||
return dynamic.ParseStructAndInject(s,
|
||||
&trader.logger,
|
||||
Notification,
|
||||
trader.environment.TradeService,
|
||||
|
|
|
@ -267,6 +267,13 @@ var BacktestCmd = &cobra.Command{
|
|||
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)
|
||||
if verboseCnt == 0 {
|
||||
trader.DisableLogging()
|
||||
|
@ -281,7 +288,7 @@ var BacktestCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
backTestIntervals := []types.Interval{types.Interval1h, types.Interval1d}
|
||||
exchangeSources, err := toExchangeSources(environ.Sessions(), backTestIntervals...)
|
||||
exchangeSources, err := toExchangeSources(environ.Sessions(), startTime, endTime, backTestIntervals...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -443,9 +450,7 @@ var BacktestCmd = &cobra.Command{
|
|||
cmdutil.WaitForSignal(runCtx, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
log.Infof("shutting down trader...")
|
||||
shutdownCtx, cancelShutdown := context.WithDeadline(runCtx, time.Now().Add(10*time.Second))
|
||||
trader.Graceful.Shutdown(shutdownCtx)
|
||||
cancelShutdown()
|
||||
bbgo.Shutdown()
|
||||
|
||||
// put the logger back to print the pnl
|
||||
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 {
|
||||
exchange := session.Exchange.(*backtest.Exchange)
|
||||
exchange.UserDataStream = session.UserDataStream.(types.StandardStreamEmitter)
|
||||
exchange.MarketDataStream = session.MarketDataStream.(types.StandardStreamEmitter)
|
||||
exchange.InitMarketData()
|
||||
backtestEx := session.Exchange.(*backtest.Exchange)
|
||||
|
||||
c, err := exchange.SubscribeMarketData(extraIntervals...)
|
||||
c, err := backtestEx.SubscribeMarketData(startTime, endTime, extraIntervals...)
|
||||
if err != nil {
|
||||
return exchangeSources, err
|
||||
}
|
||||
|
@ -657,7 +659,7 @@ func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, extraIntervals
|
|||
sessionCopy := session
|
||||
exchangeSources = append(exchangeSources, backtest.ExchangeDataSource{
|
||||
C: c,
|
||||
Exchange: exchange,
|
||||
Exchange: backtestEx,
|
||||
Session: sessionCopy,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,12 +4,16 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/data/tsv"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/optimizer"
|
||||
)
|
||||
|
||||
|
@ -17,6 +21,7 @@ func init() {
|
|||
optimizeCmd.Flags().String("optimizer-config", "optimizer.yaml", "config file")
|
||||
optimizeCmd.Flags().String("output", "output", "backtest report output directory")
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -43,6 +48,11 @@ var optimizeCmd = &cobra.Command{
|
|||
return err
|
||||
}
|
||||
|
||||
printTsvFormat, err := cmd.Flags().GetBool("tsv")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputDirectory, err := cmd.Flags().GetString("output")
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -104,6 +114,10 @@ var optimizeCmd = &cobra.Command{
|
|||
|
||||
// print metrics JSON to stdout
|
||||
fmt.Println(string(out))
|
||||
} else if printTsvFormat {
|
||||
if err := formatMetricsTsv(metrics, os.Stdout); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
for n, values := range metrics {
|
||||
if len(values) == 0 {
|
||||
|
@ -120,3 +134,95 @@ var optimizeCmd = &cobra.Command{
|
|||
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")
|
||||
}
|
||||
|
||||
market, ok := session.Market(symbol)
|
||||
if !ok {
|
||||
return fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name())
|
||||
}
|
||||
|
||||
currentPrice := currentTick.Last
|
||||
calculator := &pnl.AverageCostCalculator{
|
||||
TradingFeeCurrency: tradingFeeCurrency,
|
||||
Market: market,
|
||||
}
|
||||
|
||||
report := calculator.Calculate(symbol, trades, currentPrice)
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"path/filepath"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
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)
|
||||
cancelTrading()
|
||||
|
||||
// graceful period = 15 second
|
||||
shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(15*time.Second))
|
||||
|
||||
log.Infof("shutting down...")
|
||||
trader.Graceful.Shutdown(shutdownCtx)
|
||||
cancelShutdown()
|
||||
bbgo.Shutdown()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -216,10 +210,7 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con
|
|||
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
cancelTrading()
|
||||
|
||||
log.Infof("shutting down...")
|
||||
shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(30*time.Second))
|
||||
trader.Graceful.Shutdown(shutdownCtx)
|
||||
cancelShutdown()
|
||||
bbgo.Shutdown()
|
||||
|
||||
if err := trader.SaveState(); err != nil {
|
||||
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 (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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)
|
||||
if !field.IsValid() {
|
||||
return nil
|
||||
|
@ -38,10 +48,10 @@ func injectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnl
|
|||
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 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)
|
||||
st := reflect.TypeOf(f)
|
||||
|
||||
|
@ -121,3 +131,96 @@ func parseStructAndInject(f interface{}, objects ...interface{}) error {
|
|||
|
||||
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
|
||||
type AD struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Values types.Float64Slice
|
||||
PrePrice float64
|
||||
|
@ -23,6 +24,9 @@ type AD struct {
|
|||
}
|
||||
|
||||
func (inc *AD) Update(high, low, cloze, volume float64) {
|
||||
if len(inc.Values) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
}
|
||||
var moneyFlowVolume float64
|
||||
if high == low {
|
||||
moneyFlowVolume = 0
|
||||
|
@ -53,7 +57,7 @@ func (inc *AD) Length() int {
|
|||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &AD{}
|
||||
var _ types.SeriesExtend = &AD{}
|
||||
|
||||
func (inc *AD) calculateAndUpdate(kLines []types.KLine) {
|
||||
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
|
||||
//go:generate callbackgen -type ALMA
|
||||
type ALMA struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow // required
|
||||
Offset float64 // required: recommend to be 5
|
||||
Sigma int // required: recommend to be 0.5
|
||||
Weight []float64
|
||||
Sum float64
|
||||
weight []float64
|
||||
sum float64
|
||||
input []float64
|
||||
Values types.Float64Slice
|
||||
UpdateCallbacks []func(value float64)
|
||||
|
@ -27,16 +28,17 @@ const MaxNumOfALMA = 5_000
|
|||
const MaxNumOfALMATruncateSize = 100
|
||||
|
||||
func (inc *ALMA) Update(value float64) {
|
||||
if inc.Weight == nil {
|
||||
inc.Weight = make([]float64, inc.Window)
|
||||
if inc.weight == nil {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.weight = make([]float64, inc.Window)
|
||||
m := inc.Offset * (float64(inc.Window) - 1.)
|
||||
s := float64(inc.Window) / float64(inc.Sigma)
|
||||
inc.Sum = 0.
|
||||
inc.sum = 0.
|
||||
for i := 0; i < inc.Window; i++ {
|
||||
diff := float64(i) - m
|
||||
wt := math.Exp(-diff * diff / 2. / s / s)
|
||||
inc.Sum += wt
|
||||
inc.Weight[i] = wt
|
||||
inc.sum += wt
|
||||
inc.weight[i] = wt
|
||||
}
|
||||
}
|
||||
inc.input = append(inc.input, value)
|
||||
|
@ -44,9 +46,9 @@ func (inc *ALMA) Update(value float64) {
|
|||
weightedSum := 0.0
|
||||
inc.input = inc.input[len(inc.input)-inc.Window:]
|
||||
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 {
|
||||
inc.Values = inc.Values[MaxNumOfALMATruncateSize-1:]
|
||||
}
|
||||
|
@ -71,6 +73,8 @@ func (inc *ALMA) Length() int {
|
|||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.SeriesExtend = &ALMA{}
|
||||
|
||||
func (inc *ALMA) calculateAndUpdate(allKLines []types.KLine) {
|
||||
if inc.input == nil {
|
||||
for _, k := range allKLines {
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
//go:generate callbackgen -type ATR
|
||||
type ATR struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
PercentageVolatility types.Float64Slice
|
||||
|
||||
|
@ -25,6 +26,7 @@ func (inc *ATR) Update(high, low, cloze float64) {
|
|||
}
|
||||
|
||||
if inc.RMA == nil {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.RMA = &RMA{
|
||||
IntervalWindow: types.IntervalWindow{Window: inc.Window},
|
||||
Adjust: true,
|
||||
|
@ -73,7 +75,7 @@ func (inc *ATR) Length() int {
|
|||
return inc.RMA.Length()
|
||||
}
|
||||
|
||||
var _ types.Series = &ATR{}
|
||||
var _ types.SeriesExtend = &ATR{}
|
||||
|
||||
func (inc *ATR) CalculateAndUpdate(kLines []types.KLine) {
|
||||
for _, k := range kLines {
|
||||
|
|
|
@ -41,20 +41,20 @@ type BOLL struct {
|
|||
|
||||
type BandType int
|
||||
|
||||
func (inc *BOLL) GetUpBand() types.Series {
|
||||
return &inc.UpBand
|
||||
func (inc *BOLL) GetUpBand() types.SeriesExtend {
|
||||
return types.NewSeries(&inc.UpBand)
|
||||
}
|
||||
|
||||
func (inc *BOLL) GetDownBand() types.Series {
|
||||
return &inc.DownBand
|
||||
func (inc *BOLL) GetDownBand() types.SeriesExtend {
|
||||
return types.NewSeries(&inc.DownBand)
|
||||
}
|
||||
|
||||
func (inc *BOLL) GetSMA() types.Series {
|
||||
return &inc.SMA
|
||||
func (inc *BOLL) GetSMA() types.SeriesExtend {
|
||||
return types.NewSeries(&inc.SMA)
|
||||
}
|
||||
|
||||
func (inc *BOLL) GetStdDev() types.Series {
|
||||
return &inc.StdDev
|
||||
func (inc *BOLL) GetStdDev() types.SeriesExtend {
|
||||
return types.NewSeries(&inc.StdDev)
|
||||
}
|
||||
|
||||
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
|
||||
//go:generate callbackgen -type CCI
|
||||
type CCI struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Input types.Float64Slice
|
||||
TypicalPrice types.Float64Slice
|
||||
|
@ -23,6 +24,7 @@ type CCI struct {
|
|||
|
||||
func (inc *CCI) Update(value float64) {
|
||||
if len(inc.TypicalPrice) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.TypicalPrice.Push(value)
|
||||
inc.Input.Push(value)
|
||||
return
|
||||
|
@ -75,7 +77,7 @@ func (inc *CCI) Length() int {
|
|||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &CCI{}
|
||||
var _ types.SeriesExtend = &CCI{}
|
||||
|
||||
var three = fixedpoint.NewFromInt(3)
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
// Refer: https://en.wikipedia.org/wiki/Moving_average
|
||||
//go:generate callbackgen -type CA
|
||||
type CA struct {
|
||||
types.SeriesBase
|
||||
Interval types.Interval
|
||||
Values types.Float64Slice
|
||||
length float64
|
||||
|
@ -15,11 +16,15 @@ type CA struct {
|
|||
}
|
||||
|
||||
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.)
|
||||
inc.length += 1
|
||||
inc.Values.Push(newVal)
|
||||
if len(inc.Values) > MaxNumOfEWMA {
|
||||
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)
|
||||
}
|
||||
|
||||
var _ types.Series = &CA{}
|
||||
var _ types.SeriesExtend = &CA{}
|
||||
|
||||
func (inc *CA) calculateAndUpdate(allKLines []types.KLine) {
|
||||
for _, k := range allKLines {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
//go:generate callbackgen -type DEMA
|
||||
type DEMA struct {
|
||||
types.IntervalWindow
|
||||
types.SeriesBase
|
||||
Values types.Float64Slice
|
||||
a1 *EWMA
|
||||
a2 *EWMA
|
||||
|
@ -19,6 +20,7 @@ type DEMA struct {
|
|||
|
||||
func (inc *DEMA) Update(value float64) {
|
||||
if len(inc.Values) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.a1 = &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)
|
||||
}
|
||||
|
||||
var _ types.Series = &DEMA{}
|
||||
var _ types.SeriesExtend = &DEMA{}
|
||||
|
||||
func (inc *DEMA) calculateAndUpdate(allKLines []types.KLine) {
|
||||
if inc.a1 == nil {
|
||||
|
|
|
@ -17,11 +17,11 @@ type DMI struct {
|
|||
types.IntervalWindow
|
||||
ADXSmoothing int
|
||||
atr *ATR
|
||||
DMP types.UpdatableSeries
|
||||
DMN types.UpdatableSeries
|
||||
DMP types.UpdatableSeriesExtend
|
||||
DMN types.UpdatableSeriesExtend
|
||||
DIPlus *types.Queue
|
||||
DIMinus *types.Queue
|
||||
ADX types.UpdatableSeries
|
||||
ADX types.UpdatableSeriesExtend
|
||||
PrevHigh, PrevLow 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
|
||||
}
|
||||
|
||||
func (inc *DMI) GetDIMinus() types.Series {
|
||||
func (inc *DMI) GetDIMinus() types.SeriesExtend {
|
||||
return inc.DIMinus
|
||||
}
|
||||
|
||||
func (inc *DMI) GetADX() types.Series {
|
||||
func (inc *DMI) GetADX() types.SeriesExtend {
|
||||
return inc.ADX
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
// could be used in Monte Carlo Simulations
|
||||
//go:generate callbackgen -type Drift
|
||||
type Drift struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
chng *types.Queue
|
||||
Values types.Float64Slice
|
||||
|
@ -22,6 +23,7 @@ type Drift struct {
|
|||
|
||||
func (inc *Drift) Update(value float64) {
|
||||
if inc.chng == nil {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.SMA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}}
|
||||
inc.chng = types.NewQueue(inc.Window)
|
||||
inc.LastValue = value
|
||||
|
@ -64,7 +66,7 @@ func (inc *Drift) Length() int {
|
|||
return inc.Values.Length()
|
||||
}
|
||||
|
||||
var _ types.Series = &Drift{}
|
||||
var _ types.SeriesExtend = &Drift{}
|
||||
|
||||
func (inc *Drift) calculateAndUpdate(allKLines []types.KLine) {
|
||||
if inc.chng == nil {
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
//go:generate callbackgen -type EMV
|
||||
type EMV struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
prevH float64
|
||||
prevL float64
|
||||
|
@ -25,6 +26,7 @@ func (inc *EMV) Update(high, low, vol float64) {
|
|||
inc.EMVScale = DefaultEMVScale
|
||||
}
|
||||
if inc.prevH == 0 || inc.Values == nil {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.prevH = high
|
||||
inc.prevL = low
|
||||
inc.Values = &SMA{IntervalWindow: inc.IntervalWindow}
|
||||
|
@ -59,7 +61,7 @@ func (inc *EMV) Length() int {
|
|||
return inc.Values.Length()
|
||||
}
|
||||
|
||||
var _ types.Series = &EMV{}
|
||||
var _ types.SeriesExtend = &EMV{}
|
||||
|
||||
func (inc *EMV) calculateAndUpdate(allKLines []types.KLine) {
|
||||
if inc.Values == nil {
|
||||
|
|
|
@ -16,6 +16,7 @@ const MaxNumOfEWMATruncateSize = 100
|
|||
//go:generate callbackgen -type EWMA
|
||||
type EWMA struct {
|
||||
types.IntervalWindow
|
||||
types.SeriesBase
|
||||
Values types.Float64Slice
|
||||
LastOpenTime time.Time
|
||||
|
||||
|
@ -26,6 +27,7 @@ func (inc *EWMA) Update(value float64) {
|
|||
var multiplier = 2.0 / float64(1+inc.Window)
|
||||
|
||||
if len(inc.Values) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.Values.Push(value)
|
||||
return
|
||||
} else if len(inc.Values) > MaxNumOfEWMA {
|
||||
|
@ -136,4 +138,4 @@ func (inc *EWMA) Bind(updater KLineWindowUpdater) {
|
|||
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
|
||||
//go:generate callbackgen -type HULL
|
||||
type HULL struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
ma1 *EWMA
|
||||
ma2 *EWMA
|
||||
|
@ -20,6 +21,7 @@ type HULL struct {
|
|||
|
||||
func (inc *HULL) Update(value float64) {
|
||||
if inc.result == nil {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.ma1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window / 2}}
|
||||
inc.ma2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, 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()
|
||||
}
|
||||
|
||||
var _ types.Series = &HULL{}
|
||||
var _ types.SeriesExtend = &HULL{}
|
||||
|
||||
// TODO: should we just ignore the possible overlapping?
|
||||
func (inc *HULL) calculateAndUpdate(allKLines []types.KLine) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
// 3. resistance
|
||||
// of the market data, defined with series interface
|
||||
type Line struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
start 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 {
|
||||
return &Line{
|
||||
line := &Line{
|
||||
start: startValue,
|
||||
end: endValue,
|
||||
startIndex: startIndex,
|
||||
|
@ -71,6 +72,8 @@ func NewLine(startIndex int, startValue float64, endIndex int, endValue float64,
|
|||
currentTime: time.Time{},
|
||||
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 {
|
||||
types.SeriesBase
|
||||
*MACD
|
||||
}
|
||||
|
||||
|
@ -109,10 +110,12 @@ func (inc *MACDValues) Length() int {
|
|||
return len(inc.Values)
|
||||
}
|
||||
|
||||
func (inc *MACD) MACD() types.Series {
|
||||
return &MACDValues{inc}
|
||||
func (inc *MACD) MACD() types.SeriesExtend {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ On-Balance Volume (OBV) Definition
|
|||
*/
|
||||
//go:generate callbackgen -type OBV
|
||||
type OBV struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Values types.Float64Slice
|
||||
PrePrice float64
|
||||
|
@ -24,6 +25,7 @@ type OBV struct {
|
|||
|
||||
func (inc *OBV) Update(price, volume float64) {
|
||||
if len(inc.Values) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.PrePrice = price
|
||||
inc.Values.Push(volume)
|
||||
return
|
||||
|
@ -43,6 +45,15 @@ func (inc *OBV) Last() float64 {
|
|||
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) {
|
||||
for _, k := range kLines {
|
||||
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
|
||||
//go:generate callbackgen -type RMA
|
||||
type RMA struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Values types.Float64Slice
|
||||
counter int
|
||||
|
@ -24,6 +25,7 @@ type RMA struct {
|
|||
func (inc *RMA) Update(x float64) {
|
||||
lambda := 1 / float64(inc.Window)
|
||||
if inc.counter == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.sum = 1
|
||||
inc.tmp = x
|
||||
} else {
|
||||
|
@ -60,7 +62,7 @@ func (inc *RMA) Length() int {
|
|||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &RMA{}
|
||||
var _ types.SeriesExtend = &RMA{}
|
||||
|
||||
func (inc *RMA) calculateAndUpdate(kLines []types.KLine) {
|
||||
for _, k := range kLines {
|
||||
|
|
|
@ -14,6 +14,7 @@ https://www.investopedia.com/terms/r/rsi.asp
|
|||
*/
|
||||
//go:generate callbackgen -type RSI
|
||||
type RSI struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Values types.Float64Slice
|
||||
Prices types.Float64Slice
|
||||
|
@ -25,6 +26,9 @@ type RSI struct {
|
|||
}
|
||||
|
||||
func (inc *RSI) Update(price float64) {
|
||||
if len(inc.Prices) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
}
|
||||
inc.Prices.Push(price)
|
||||
|
||||
if len(inc.Prices) < inc.Window+1 {
|
||||
|
@ -74,7 +78,7 @@ func (inc *RSI) Length() int {
|
|||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &RSI{}
|
||||
var _ types.SeriesExtend = &RSI{}
|
||||
|
||||
func (inc *RSI) calculateAndUpdate(kLines []types.KLine) {
|
||||
for _, k := range kLines {
|
||||
|
|
|
@ -16,6 +16,7 @@ var zeroTime time.Time
|
|||
|
||||
//go:generate callbackgen -type SMA
|
||||
type SMA struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Values types.Float64Slice
|
||||
Cache types.Float64Slice
|
||||
|
@ -44,10 +45,13 @@ func (inc *SMA) Length() int {
|
|||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &SMA{}
|
||||
var _ types.SeriesExtend = &SMA{}
|
||||
|
||||
func (inc *SMA) Update(value float64) {
|
||||
if len(inc.Cache) < inc.Window {
|
||||
if len(inc.Cache) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
}
|
||||
inc.Cache = append(inc.Cache, value)
|
||||
if len(inc.Cache) == inc.Window {
|
||||
inc.Values = append(inc.Values, types.Mean(&inc.Cache))
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
//
|
||||
//go:generate callbackgen -type SSF
|
||||
type SSF struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Poles int
|
||||
c1 float64
|
||||
|
@ -34,6 +35,7 @@ type SSF struct {
|
|||
func (inc *SSF) Update(value float64) {
|
||||
if inc.Poles == 3 {
|
||||
if inc.Values == nil {
|
||||
inc.SeriesBase.Series = inc
|
||||
x := math.Pi / float64(inc.Window)
|
||||
a0 := math.Exp(-x)
|
||||
b0 := 2. * a0 * math.Cos(math.Sqrt(3.)*x)
|
||||
|
@ -53,6 +55,7 @@ func (inc *SSF) Update(value float64) {
|
|||
inc.Values.Push(result)
|
||||
} else { // poles == 2
|
||||
if inc.Values == nil {
|
||||
inc.SeriesBase.Series = inc
|
||||
x := math.Pi * math.Sqrt(2.) / float64(inc.Window)
|
||||
a0 := math.Exp(-x)
|
||||
inc.c3 = -a0 * a0
|
||||
|
@ -88,7 +91,7 @@ func (inc *SSF) Last() float64 {
|
|||
return inc.Values.Last()
|
||||
}
|
||||
|
||||
var _ types.Series = &SSF{}
|
||||
var _ types.SeriesExtend = &SSF{}
|
||||
|
||||
func (inc *SSF) calculateAndUpdate(allKLines []types.KLine) {
|
||||
if inc.Values != nil {
|
||||
|
|
|
@ -12,6 +12,7 @@ var logst = logrus.WithField("indicator", "supertrend")
|
|||
|
||||
//go:generate callbackgen -type Supertrend
|
||||
type Supertrend struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
ATRMultiplier float64 `json:"atrMultiplier"`
|
||||
|
||||
|
@ -54,6 +55,10 @@ func (inc *Supertrend) Update(highPrice, lowPrice, closePrice float64) {
|
|||
panic("window must be greater than 0")
|
||||
}
|
||||
|
||||
if inc.AverageTrueRange == nil {
|
||||
inc.SeriesBase.Series = inc
|
||||
}
|
||||
|
||||
// Start with DirectionUp
|
||||
if inc.trend != types.DirectionUp && inc.trend != types.DirectionDown {
|
||||
inc.trend = types.DirectionUp
|
||||
|
@ -120,7 +125,7 @@ func (inc *Supertrend) GetSignal() types.Direction {
|
|||
return inc.tradeSignal
|
||||
}
|
||||
|
||||
var _ types.Series = &Supertrend{}
|
||||
var _ types.SeriesExtend = &Supertrend{}
|
||||
|
||||
func (inc *Supertrend) calculateAndUpdate(kLines []types.KLine) {
|
||||
for _, k := range kLines {
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
//go:generate callbackgen -type TEMA
|
||||
type TEMA struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Values types.Float64Slice
|
||||
A1 *EWMA
|
||||
|
@ -20,6 +21,7 @@ type TEMA struct {
|
|||
|
||||
func (inc *TEMA) Update(value float64) {
|
||||
if len(inc.Values) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.A1 = &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}}
|
||||
|
@ -51,7 +53,7 @@ func (inc *TEMA) Length() int {
|
|||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &TEMA{}
|
||||
var _ types.SeriesExtend = &TEMA{}
|
||||
|
||||
func (inc *TEMA) calculateAndUpdate(allKLines []types.KLine) {
|
||||
if inc.A1 == nil {
|
||||
|
|
|
@ -10,6 +10,7 @@ const defaultVolumeFactor = 0.7
|
|||
// Refer URL: https://tradingpedia.com/forex-trading-indicator/t3-moving-average-indicator/
|
||||
//go:generate callbackgen -type TILL
|
||||
type TILL struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
VolumeFactor float64
|
||||
e1 *EWMA
|
||||
|
@ -30,6 +31,7 @@ func (inc *TILL) Update(value float64) {
|
|||
if inc.VolumeFactor == 0 {
|
||||
inc.VolumeFactor = defaultVolumeFactor
|
||||
}
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.e1 = &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}}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
// Refer URL: https://ja.wikipedia.org/wiki/移動平均
|
||||
//go:generate callbackgen -type TMA
|
||||
type TMA struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
s1 *SMA
|
||||
s2 *SMA
|
||||
|
@ -16,6 +17,7 @@ type TMA struct {
|
|||
|
||||
func (inc *TMA) Update(value float64) {
|
||||
if inc.s1 == nil {
|
||||
inc.SeriesBase.Series = inc
|
||||
w := (inc.Window + 1) / 2
|
||||
inc.s1 = &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()
|
||||
}
|
||||
|
||||
var _ types.Series = &TMA{}
|
||||
var _ types.SeriesExtend = &TMA{}
|
||||
|
||||
func (inc *TMA) calculateAndUpdate(allKLines []types.KLine) {
|
||||
if inc.s1 == nil {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
// Refer URL: https://metatrader5.com/en/terminal/help/indicators/trend_indicators/vida
|
||||
//go:generate callbackgen -type VIDYA
|
||||
type VIDYA struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Values types.Float64Slice
|
||||
input types.Float64Slice
|
||||
|
@ -19,6 +20,7 @@ type VIDYA struct {
|
|||
|
||||
func (inc *VIDYA) Update(value float64) {
|
||||
if inc.Values.Length() == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.Values.Push(value)
|
||||
inc.input.Push(value)
|
||||
return
|
||||
|
@ -66,7 +68,7 @@ func (inc *VIDYA) Length() int {
|
|||
return inc.Values.Length()
|
||||
}
|
||||
|
||||
var _ types.Series = &VIDYA{}
|
||||
var _ types.SeriesExtend = &VIDYA{}
|
||||
|
||||
func (inc *VIDYA) calculateAndUpdate(allKLines []types.KLine) {
|
||||
if inc.input.Length() == 0 {
|
||||
|
|
|
@ -17,6 +17,7 @@ const MaxNumOfVOLTruncateSize = 100
|
|||
|
||||
//go:generate callbackgen -type VOLATILITY
|
||||
type VOLATILITY struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Values types.Float64Slice
|
||||
EndTime time.Time
|
||||
|
@ -31,6 +32,19 @@ func (inc *VOLATILITY) Last() float64 {
|
|||
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) {
|
||||
if len(klines) < inc.Window {
|
||||
return
|
||||
|
@ -42,6 +56,9 @@ func (inc *VOLATILITY) calculateAndUpdate(klines []types.KLine) {
|
|||
if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) {
|
||||
return
|
||||
}
|
||||
if len(inc.Values) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
}
|
||||
|
||||
var recentT = klines[end-(inc.Window-1) : end+1]
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ Volume-Weighted Average Price (VWAP) Explained
|
|||
*/
|
||||
//go:generate callbackgen -type VWAP
|
||||
type VWAP struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Values types.Float64Slice
|
||||
Prices types.Float64Slice
|
||||
|
@ -29,6 +30,9 @@ type VWAP struct {
|
|||
}
|
||||
|
||||
func (inc *VWAP) Update(price, volume float64) {
|
||||
if len(inc.Prices) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
}
|
||||
inc.Prices.Push(price)
|
||||
inc.Volumes.Push(volume)
|
||||
|
||||
|
@ -65,7 +69,7 @@ func (inc *VWAP) Length() int {
|
|||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &VWAP{}
|
||||
var _ types.SeriesExtend = &VWAP{}
|
||||
|
||||
func (inc *VWAP) calculateAndUpdate(kLines []types.KLine) {
|
||||
var priceF = KLineTypicalPriceMapper
|
||||
|
|
|
@ -20,6 +20,7 @@ Volume Weighted Moving Average
|
|||
*/
|
||||
//go:generate callbackgen -type VWMA
|
||||
type VWMA struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Values types.Float64Slice
|
||||
EndTime time.Time
|
||||
|
@ -46,7 +47,7 @@ func (inc *VWMA) Length() int {
|
|||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &VWMA{}
|
||||
var _ types.SeriesExtend = &VWMA{}
|
||||
|
||||
func KLinePriceVolumeMapper(k types.KLine) float64 {
|
||||
return k.Close.Mul(k.Volume).Float64()
|
||||
|
@ -81,6 +82,10 @@ func (inc *VWMA) calculateAndUpdate(kLines []types.KLine) {
|
|||
return
|
||||
}
|
||||
|
||||
if len(inc.Values) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
}
|
||||
|
||||
vwma := pv / v
|
||||
inc.Values.Push(vwma)
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ const MaxNumOfWWMATruncateSize = 100
|
|||
|
||||
//go:generate callbackgen -type WWMA
|
||||
type WWMA struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
Values types.Float64Slice
|
||||
LastOpenTime time.Time
|
||||
|
@ -23,6 +24,7 @@ type WWMA struct {
|
|||
|
||||
func (inc *WWMA) Update(value float64) {
|
||||
if len(inc.Values) == 0 {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.Values.Push(value)
|
||||
return
|
||||
} else if len(inc.Values) > MaxNumOfWWMA {
|
||||
|
@ -85,4 +87,4 @@ func (inc *WWMA) Bind(updater KLineWindowUpdater) {
|
|||
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
|
||||
}
|
||||
|
||||
var _ types.Series = &WWMA{}
|
||||
var _ types.SeriesExtend = &WWMA{}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
//go:generate callbackgen -type ZLEMA
|
||||
type ZLEMA struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
|
||||
data types.Float64Slice
|
||||
|
@ -41,6 +42,7 @@ func (inc *ZLEMA) Length() int {
|
|||
|
||||
func (inc *ZLEMA) Update(value float64) {
|
||||
if inc.lag == 0 || inc.zlema == nil {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.zlema = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}}
|
||||
inc.lag = int((float64(inc.Window)-1.)/2. + 0.5)
|
||||
}
|
||||
|
@ -55,7 +57,7 @@ func (inc *ZLEMA) Update(value float64) {
|
|||
inc.zlema.Update(emaData)
|
||||
}
|
||||
|
||||
var _ types.Series = &ZLEMA{}
|
||||
var _ types.SeriesExtend = &ZLEMA{}
|
||||
|
||||
func (inc *ZLEMA) calculateAndUpdate(allKLines []types.KLine) {
|
||||
if inc.zlema == nil {
|
||||
|
|
|
@ -112,7 +112,7 @@ func (it *Interact) handleResponse(session Session, text string, ctxObjects ...i
|
|||
}
|
||||
|
||||
ctxObjects = append(ctxObjects, session)
|
||||
_, err := parseFuncArgsAndCall(f, args, ctxObjects...)
|
||||
_, err := ParseFuncArgsAndCall(f, args, ctxObjects...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ func (it *Interact) runCommand(session Session, command string, args []string, c
|
|||
|
||||
ctxObjects = append(ctxObjects, session)
|
||||
session.SetState(cmd.initState)
|
||||
if _, err := parseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil {
|
||||
if _, err := ParseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ func Test_parseFuncArgsAndCall_NoErrorFunction(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
_, err := parseFuncArgsAndCall(noErrorFunc, []string{"BTCUSDT", "0.123", "true"})
|
||||
_, err := ParseFuncArgsAndCall(noErrorFunc, []string{"BTCUSDT", "0.123", "true"})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ func Test_parseFuncArgsAndCall_ErrorFunction(t *testing.T) {
|
|||
return errors.New("error")
|
||||
}
|
||||
|
||||
_, err := parseFuncArgsAndCall(errorFunc, []string{"BTCUSDT", "0.123"})
|
||||
_, err := ParseFuncArgsAndCall(errorFunc, []string{"BTCUSDT", "0.123"})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ func Test_parseFuncArgsAndCall_InterfaceInjection(t *testing.T) {
|
|||
}
|
||||
|
||||
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.Equal(t, "123", buf.String())
|
||||
}
|
||||
|
|
|
@ -10,21 +10,20 @@ import (
|
|||
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)
|
||||
ft := reflect.TypeOf(f)
|
||||
|
||||
argIndex := 0
|
||||
|
||||
var rArgs []reflect.Value
|
||||
for i := 0; i < ft.NumIn(); i++ {
|
||||
at := ft.In(i)
|
||||
|
||||
// get the kind of argument
|
||||
switch k := at.Kind(); k {
|
||||
|
||||
case reflect.Interface:
|
||||
found := false
|
||||
|
||||
for oi := 0; oi < len(objects); oi++ {
|
||||
obj := objects[oi]
|
||||
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
|
||||
var state State
|
||||
var err error
|
||||
var state State
|
||||
for i := 0; i < ft.NumOut(); i++ {
|
||||
outType := ft.Out(i)
|
||||
switch outType.Kind() {
|
||||
|
@ -107,7 +106,6 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{})
|
|||
err = ov
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return state, err
|
||||
|
|
|
@ -4,10 +4,11 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
"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/fixedpoint"
|
||||
|
@ -19,10 +20,28 @@ var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fix
|
|||
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 {
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
Params []interface{} `json:"params,omitempty"`
|
||||
Value fixedpoint.Value `json:"value,omitempty"`
|
||||
// Labels is the labels of the given parameters
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
|
||||
// Params is the parameters used to output the metrics result
|
||||
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"`
|
||||
}
|
||||
|
||||
func copyParams(params []interface{}) []interface{} {
|
||||
|
@ -168,6 +187,7 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][]
|
|||
|
||||
var valueFunctions = map[string]MetricValueFunc{
|
||||
"totalProfit": TotalProfitMetricValueFunc,
|
||||
"totalVolume": TotalVolume,
|
||||
}
|
||||
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
|
||||
|
||||
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)
|
||||
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()
|
||||
metrics[metricName] = append(metrics[metricName], Metric{
|
||||
|
||||
metrics[metricKey] = append(metrics[metricKey], Metric{
|
||||
Params: result.Params,
|
||||
Labels: result.Labels,
|
||||
Key: metricKey,
|
||||
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.
|
||||
*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
|
||||
// This field will be injected automatically since we defined the Symbol field.
|
||||
types.Market
|
||||
|
@ -350,7 +347,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
s.profitOrders.BindStream(session.UserDataStream)
|
||||
|
||||
// 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.
|
||||
defer wg.Done()
|
||||
log.Infof("canceling active orders...")
|
||||
|
|
|
@ -49,7 +49,6 @@ type BollingerSetting struct {
|
|||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
*bbgo.Persistence
|
||||
|
||||
Environment *bbgo.Environment
|
||||
|
@ -216,18 +215,6 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
|
|||
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) {
|
||||
if s.DynamicExposurePositionScale != nil {
|
||||
v, err := s.DynamicExposurePositionScale.Scale(bandPercentage)
|
||||
|
@ -506,17 +493,7 @@ 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 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 {
|
||||
|
@ -527,13 +504,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -616,7 +587,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
// s.book = types.NewStreamBook(s.Symbol)
|
||||
// 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()
|
||||
|
||||
_ = s.orderExecutor.GracefulCancel(ctx)
|
||||
|
|
|
@ -47,7 +47,6 @@ func (b BudgetPeriod) Duration() time.Duration {
|
|||
|
||||
// Strategy is the Dollar-Cost-Average strategy
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
|
||||
Environment *bbgo.Environment
|
||||
Symbol string `json:"symbol"`
|
||||
|
|
|
@ -25,7 +25,6 @@ func init() {
|
|||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
|
||||
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.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
log.Infof("canceling trailingstop order...")
|
||||
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.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
log.Infof("canceling trailingstop order...")
|
||||
s.clear(ctx, &orderExecutor)
|
||||
|
|
|
@ -51,7 +51,6 @@ type Strategy struct {
|
|||
KLineEndTime types.Time
|
||||
|
||||
*bbgo.Environment
|
||||
*bbgo.Graceful
|
||||
bbgo.StrategyController
|
||||
|
||||
activeMakerOrders *bbgo.ActiveOrderBook
|
||||
|
@ -63,11 +62,11 @@ type Strategy struct {
|
|||
atr *indicator.ATR
|
||||
emv *indicator.EMV
|
||||
ccis *CCISTOCH
|
||||
ma5 types.Series
|
||||
ma34 types.Series
|
||||
ewo types.Series
|
||||
ewoSignal types.Series
|
||||
ewoHistogram types.Series
|
||||
ma5 types.SeriesExtend
|
||||
ma34 types.SeriesExtend
|
||||
ewo types.SeriesExtend
|
||||
ewoSignal types.SeriesExtend
|
||||
ewoHistogram types.SeriesExtend
|
||||
ewoChangeRate float64
|
||||
heikinAshi *HeikinAshi
|
||||
peakPrice fixedpoint.Value
|
||||
|
@ -331,12 +330,12 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) {
|
|||
evwma34.UpdateVal(price, vol)
|
||||
}
|
||||
})
|
||||
s.ma5 = evwma5
|
||||
s.ma34 = evwma34
|
||||
s.ma5 = types.NewSeries(evwma5)
|
||||
s.ma34 = types.NewSeries(evwma34)
|
||||
}
|
||||
|
||||
s.ewo = types.Mul(types.Minus(types.Div(s.ma5, s.ma34), 1.0), 100.)
|
||||
s.ewoHistogram = types.Minus(s.ma5, s.ma34)
|
||||
s.ewo = s.ma5.Div(s.ma34).Minus(1.0).Mul(100.)
|
||||
s.ewoHistogram = s.ma5.Minus(s.ma34)
|
||||
windowSignal := types.IntervalWindow{Interval: s.Interval, Window: s.SignalWindow}
|
||||
if s.UseEma {
|
||||
sig := &indicator.EWMA{IntervalWindow: windowSignal}
|
||||
|
@ -365,7 +364,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) {
|
|||
|
||||
if sig.Length() == 0 {
|
||||
// lazy init
|
||||
ewoVals := types.Reverse(s.ewo)
|
||||
ewoVals := s.ewo.Reverse()
|
||||
for _, ewoValue := range ewoVals {
|
||||
sig.Update(ewoValue)
|
||||
}
|
||||
|
@ -385,7 +384,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) {
|
|||
}
|
||||
if sig.Length() == 0 {
|
||||
// lazy init
|
||||
ewoVals := types.Reverse(s.ewo)
|
||||
ewoVals := s.ewo.Reverse()
|
||||
for i, ewoValue := range ewoVals {
|
||||
vol := window.Volume().Index(i)
|
||||
sig.PV.Update(ewoValue * vol)
|
||||
|
@ -397,7 +396,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) {
|
|||
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()
|
||||
log.Infof("canceling active orders...")
|
||||
s.CancelAll(ctx)
|
||||
|
|
|
@ -49,10 +49,6 @@ type Strategy struct {
|
|||
// This field will be injected automatically since we defined the Symbol field.
|
||||
*bbgo.StandardIndicatorSet
|
||||
|
||||
// Graceful shutdown function
|
||||
*bbgo.Graceful
|
||||
// --------------------------
|
||||
|
||||
// ewma is the exponential weighted moving average indicator
|
||||
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.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()
|
||||
|
||||
log.Infof("canceling active orders...")
|
||||
|
|
|
@ -31,7 +31,6 @@ type IntervalWindowSetting struct {
|
|||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
*bbgo.Persistence
|
||||
|
||||
Environment *bbgo.Environment
|
||||
|
|
|
@ -45,8 +45,6 @@ type State struct {
|
|||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful `json:"-" yaml:"-"`
|
||||
|
||||
*bbgo.Persistence
|
||||
|
||||
// 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.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
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"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/dynamic"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/indicator"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
@ -30,47 +30,124 @@ type IntervalWindowSetting struct {
|
|||
types.IntervalWindow
|
||||
}
|
||||
|
||||
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
|
||||
type BreakLow struct {
|
||||
// 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"`
|
||||
|
||||
type SupportTakeProfit struct {
|
||||
Symbol string
|
||||
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 {
|
||||
CatBounceRatio fixedpoint.Value `json:"catBounceRatio"`
|
||||
NumLayers int `json:"numLayers"`
|
||||
TotalQuantity fixedpoint.Value `json:"totalQuantity"`
|
||||
func (s *SupportTakeProfit) Subscribe(session *bbgo.ExchangeSession) {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||
}
|
||||
|
||||
Quantity fixedpoint.Value `json:"quantity"`
|
||||
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
|
||||
func (s *SupportTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
|
||||
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 {
|
||||
*bbgo.Graceful
|
||||
|
||||
Environment *bbgo.Environment
|
||||
Symbol string `json:"symbol"`
|
||||
Market types.Market
|
||||
|
@ -83,25 +160,19 @@ type Strategy struct {
|
|||
ProfitStats *types.ProfitStats `persistence:"profit_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"`
|
||||
ExitMethods []bbgo.ExitMethod `json:"exits"`
|
||||
SupportTakeProfit []*SupportTakeProfit `json:"supportTakeProfit"`
|
||||
|
||||
ExitMethods bbgo.ExitMethodSet `json:"exits"`
|
||||
|
||||
session *bbgo.ExchangeSession
|
||||
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
|
||||
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: types.Interval1m})
|
||||
|
||||
if s.BounceShort != nil && s.BounceShort.Enabled {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.BounceShort.Interval})
|
||||
if s.ResistanceShort != nil && s.ResistanceShort.Enabled {
|
||||
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 {
|
||||
session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value {
|
||||
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,
|
||||
})
|
||||
s.ExitMethods.SetAndSubscribe(session, s)
|
||||
}
|
||||
|
||||
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 {
|
||||
bbgo.Notify("Closing position", s.Position)
|
||||
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 {
|
||||
s.TradeStats = &types.TradeStats{}
|
||||
s.TradeStats = types.NewTradeStats(s.Symbol)
|
||||
}
|
||||
|
||||
s.lastLow = fixedpoint.Zero
|
||||
|
||||
// StrategyController
|
||||
s.Status = types.StrategyStatusRunning
|
||||
|
||||
|
@ -221,260 +261,33 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
})
|
||||
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 {
|
||||
method.Bind(session, s.orderExecutor)
|
||||
}
|
||||
|
||||
if s.BounceShort != nil && s.BounceShort.Enabled {
|
||||
if s.resistancePivot != nil {
|
||||
s.preloadPivot(s.resistancePivot, store)
|
||||
}
|
||||
|
||||
session.UserDataStream.OnStart(func() {
|
||||
if lastKLine == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if s.resistancePivot != nil {
|
||||
lows := s.resistancePivot.Lows
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if s.ResistanceShort != nil && s.ResistanceShort.Enabled {
|
||||
s.ResistanceShort.Bind(session, s.orderExecutor)
|
||||
}
|
||||
|
||||
// Always check whether you can open a short position or not
|
||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
||||
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) {
|
||||
|
||||
})
|
||||
if s.BreakLow != nil {
|
||||
s.BreakLow.Bind(session, s.orderExecutor)
|
||||
}
|
||||
|
||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
for i := range s.SupportTakeProfit {
|
||||
s.SupportTakeProfit[i].Bind(session, s.orderExecutor)
|
||||
}
|
||||
|
||||
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
|
||||
wg.Done()
|
||||
_ = s.orderExecutor.GracefulCancel(ctx)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) findHigherPivotLow(price fixedpoint.Value) (fixedpoint.Value, bool) {
|
||||
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 {
|
||||
func preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataStore) *types.KLine {
|
||||
klines, ok := store.KLinesOfInterval(pivot.Interval)
|
||||
if !ok {
|
||||
return nil
|
||||
|
@ -487,31 +300,7 @@ func (s *Strategy) preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataSt
|
|||
pivot.Update((*klines)[0 : i+1])
|
||||
}
|
||||
|
||||
log.Infof("found %s %v previous lows: %v", s.Symbol, pivot.IntervalWindow, pivot.Lows)
|
||||
log.Infof("found %s %v previous highs: %v", s.Symbol, pivot.IntervalWindow, pivot.Highs)
|
||||
log.Debugf("found %v previous lows: %v", pivot.IntervalWindow, pivot.Lows)
|
||||
log.Debugf("found %v previous highs: %v", pivot.IntervalWindow, pivot.Highs)
|
||||
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 {
|
||||
*bbgo.Graceful
|
||||
*bbgo.Notifiability
|
||||
|
||||
Environment *bbgo.Environment
|
||||
StandardIndicatorSet *bbgo.StandardIndicatorSet
|
||||
Market types.Market
|
||||
|
@ -416,7 +413,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
|
|||
}
|
||||
|
||||
if s.TradeStats == nil {
|
||||
s.TradeStats = &types.TradeStats{}
|
||||
s.TradeStats = types.NewTradeStats(s.Symbol)
|
||||
}
|
||||
|
||||
// initial required information
|
||||
|
|
|
@ -33,7 +33,6 @@ func init() {
|
|||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
*bbgo.Persistence
|
||||
|
||||
Environment *bbgo.Environment
|
||||
|
@ -430,7 +429,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
})
|
||||
|
||||
// Graceful shutdown
|
||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
close(s.stopC)
|
||||
|
||||
|
|
|
@ -134,7 +134,6 @@ func (control *TrailingStopControl) GenerateStopOrder(quantity fixedpoint.Value)
|
|||
type Strategy struct {
|
||||
*bbgo.Persistence `json:"-"`
|
||||
*bbgo.Environment `json:"-"`
|
||||
*bbgo.Graceful `json:"-"`
|
||||
|
||||
session *bbgo.ExchangeSession
|
||||
|
||||
|
@ -335,7 +334,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
|
||||
// trade stats
|
||||
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)
|
||||
|
@ -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()
|
||||
|
||||
// Cancel trailing stop order
|
||||
|
|
|
@ -30,7 +30,6 @@ func init() {
|
|||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
*bbgo.Persistence
|
||||
|
||||
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()
|
||||
close(s.stopC)
|
||||
|
||||
|
|
|
@ -135,8 +135,6 @@ func (a *Address) UnmarshalJSON(body []byte) error {
|
|||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
|
||||
Interval types.Duration `json:"interval"`
|
||||
|
||||
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.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
})
|
||||
|
||||
|
|
|
@ -57,7 +57,6 @@ func (s *State) Reset() {
|
|||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
*bbgo.Persistence
|
||||
|
||||
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()
|
||||
|
||||
close(s.stopC)
|
||||
|
|
|
@ -33,7 +33,6 @@ func init() {
|
|||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
*bbgo.Persistence
|
||||
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()
|
||||
|
||||
close(s.stopC)
|
||||
|
|
|
@ -58,7 +58,6 @@ func (s *State) Reset() {
|
|||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
*bbgo.Persistence
|
||||
*bbgo.Environment
|
||||
|
||||
|
@ -180,7 +179,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
|
|||
return err
|
||||
}
|
||||
|
||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
s.SaveState()
|
||||
|
|
|
@ -74,6 +74,7 @@ func ValidExchangeName(a string) (ExchangeName, error) {
|
|||
return "", fmt.Errorf("invalid exchange name: %s", a)
|
||||
}
|
||||
|
||||
//go:generate mockgen -destination=mocks/mock_exchange.go -package=mocks . Exchange
|
||||
type Exchange interface {
|
||||
Name() ExchangeName
|
||||
|
||||
|
|
|
@ -11,15 +11,18 @@ import (
|
|||
// Super basic Series type that simply holds the float64 data
|
||||
// with size limit (the only difference compare to float64slice)
|
||||
type Queue struct {
|
||||
SeriesBase
|
||||
arr []float64
|
||||
size int
|
||||
}
|
||||
|
||||
func NewQueue(size int) *Queue {
|
||||
return &Queue{
|
||||
out := &Queue{
|
||||
arr: make([]float64, 0, size),
|
||||
size: size,
|
||||
}
|
||||
out.SeriesBase.Series = out
|
||||
return out
|
||||
}
|
||||
|
||||
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.
|
||||
type Float64Indicator interface {
|
||||
|
@ -82,24 +85,24 @@ type SeriesExtend interface {
|
|||
Array(limit ...int) (result []float64)
|
||||
Reverse(limit ...int) (result Float64Slice)
|
||||
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 {
|
||||
index IndexFuncType
|
||||
last LastFuncType
|
||||
length LengthFuncType
|
||||
Series
|
||||
}
|
||||
|
||||
func NewSeries(a Series) SeriesExtend {
|
||||
return &SeriesBase{
|
||||
index: a.Index,
|
||||
last: a.Last,
|
||||
length: a.Length,
|
||||
Series: a,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,6 +111,11 @@ type UpdatableSeries interface {
|
|||
Update(float64)
|
||||
}
|
||||
|
||||
type UpdatableSeriesExtend interface {
|
||||
SeriesExtend
|
||||
Update(float64)
|
||||
}
|
||||
|
||||
// The interface maps to pinescript basic type `series` for bool type
|
||||
// Access the internal historical data from the latest to the oldest
|
||||
// Index(0) always maps to Last()
|
||||
|
@ -595,14 +603,282 @@ func Change(a Series, offset ...int) SeriesExtend {
|
|||
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)
|
||||
s := .0
|
||||
for i := 0; i < length; i++ {
|
||||
diff := a.Index(i) - avg
|
||||
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
|
||||
|
|
|
@ -33,3 +33,54 @@ func TestFloat64Slice(t *testing.T) {
|
|||
b = append(b, 3.0)
|
||||
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