Merge branch 'main' into improve/supertrend-strategy

This commit is contained in:
Andy Cheng 2022-07-07 10:33:30 +08:00 committed by GitHub
commit f8777752a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
107 changed files with 3443 additions and 1043 deletions

158
README.md
View File

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

View File

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

View File

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

View File

@ -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
View File

@ -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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,5 +4,5 @@ import "testing"
func TestExitMethod(t *testing.T) {
em := &ExitMethod{}
em.Subscribe()
em.Subscribe(&ExchangeSession{})
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

@ -0,0 +1,2 @@
package bbgo

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

@ -0,0 +1 @@
package statistics

34
pkg/statistics/sharp.go Normal file
View 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
}

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

View File

@ -0,0 +1 @@
package statistics

View File

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

View File

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

View File

@ -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"`

View File

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

View File

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

View File

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

View File

@ -31,7 +31,6 @@ type IntervalWindowSetting struct {
}
type Strategy struct {
*bbgo.Graceful
*bbgo.Persistence
Environment *bbgo.Environment

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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