Merge branch 'main' into strategy/pivotshort

This commit is contained in:
Yo-An Lin 2022-07-12 23:38:23 +08:00 committed by GitHub
commit 8119afbb44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 1203 additions and 272 deletions

2
.gitignore vendored
View File

@ -32,6 +32,8 @@
/config/bbgo.yaml
/localconfig
/pkg/server/assets.go
bbgo.sqlite3

View File

@ -113,6 +113,19 @@ const fetchPositionHistory = (basePath: string, runID: string, filename: string)
});
};
const selectPositionHistory = (data: PositionHistoryEntry[], since: Date, until: Date): PositionHistoryEntry[] => {
const entries: PositionHistoryEntry[] = [];
for (let i = 0; i < data.length; i++) {
const d = data[i];
if (d.time < since || d.time > until) {
continue
}
entries.push(d)
}
return entries
}
const fetchOrders = (basePath: string, runID: string) => {
return fetch(
`${basePath}/${runID}/orders.tsv`,
@ -124,6 +137,19 @@ const fetchOrders = (basePath: string, runID: string) => {
});
}
const selectOrders = (data: Order[], since: Date, until: Date): Order[] => {
const entries: Order[] = [];
for (let i = 0; i < data.length; i++) {
const d = data[i];
if (d.time && (d.time < since || d.time > until)) {
continue
}
entries.push(d);
}
return entries
}
const parseInterval = (s: string) => {
switch (s) {
case "1m":
@ -390,7 +416,7 @@ const TradingViewChart = (props: TradingViewChartProps) => {
const resizeObserver = useRef<any>();
const intervals = props.reportSummary.intervals || [];
intervals.sort((a,b) => {
intervals.sort((a, b) => {
const as = parseInterval(a)
const bs = parseInterval(b)
if (as < bs) {
@ -403,7 +429,6 @@ const TradingViewChart = (props: TradingViewChartProps) => {
const [currentInterval, setCurrentInterval] = useState(intervals.length > 0 ? intervals[intervals.length - 1] : '1m');
const [showPositionBase, setShowPositionBase] = useState(false);
const [showCanceledOrders, setShowCanceledOrders] = useState(false);
const [showPositionAverageCost, setShowPositionAverageCost] = useState(false);
const [orders, setOrders] = useState<Order[]>([]);
@ -412,7 +437,6 @@ const TradingViewChart = (props: TradingViewChartProps) => {
new Date(props.reportSummary.endTime),
]
const [selectedTimeRange, setSelectedTimeRange] = useState(reportTimeRange)
const [timeRange, setTimeRange] = useState(reportTimeRange);
useEffect(() => {
if (!chartContainerRef.current || chartContainerRef.current.children.length > 0) {
@ -423,7 +447,7 @@ const TradingViewChart = (props: TradingViewChartProps) => {
const fetchers = [];
const ordersFetcher = fetchOrders(props.basePath, props.runID).then((orders: Order[] | void) => {
if (orders) {
const markers = ordersToMarkers(currentInterval, orders);
const markers = ordersToMarkers(currentInterval, selectOrders(orders, selectedTimeRange[0], selectedTimeRange[1]));
chartData.orders = orders;
chartData.markers = markers;
setOrders(orders);
@ -436,7 +460,8 @@ const TradingViewChart = (props: TradingViewChartProps) => {
const manifest = props.reportSummary?.manifests[0];
if (manifest && manifest.type === "strategyProperty" && manifest.strategyProperty === "position") {
const positionHistoryFetcher = fetchPositionHistory(props.basePath, props.runID, manifest.filename).then((data) => {
chartData.positionHistory = data;
chartData.positionHistory = selectPositionHistory(data as PositionHistoryEntry[], selectedTimeRange[0], selectedTimeRange[1]);
// chartData.positionHistory = data;
});
fetchers.push(positionHistoryFetcher);
}
@ -594,7 +619,7 @@ const TradingViewChart = (props: TradingViewChartProps) => {
<TimeRangeSlider
selectedInterval={selectedTimeRange}
timelineInterval={timeRange}
timelineInterval={reportTimeRange}
formatTick={(ms: Date) => format(new Date(ms), 'M d HH')}
step={1000 * parseInterval(currentInterval)}
onChange={(tr: any) => {
@ -661,12 +686,12 @@ const createLegendUpdater = (legend: HTMLDivElement, prefix: string) => {
}
}
const formatDate = (d : Date) : string => {
const formatDate = (d: Date): string => {
return moment(d).format("MMM Do YY hh:mm:ss A Z");
}
const createOHLCLegendUpdater = (legend: HTMLDivElement, prefix: string) => {
return (param: any, time : any) => {
return (param: any, time: any) => {
if (param) {
const change = Math.round((param.close - param.open) * 100.0) / 100.0
const changePercentage = Math.round((param.close - param.open) / param.close * 10000.0) / 100.0;

View File

@ -19,7 +19,7 @@ backtest:
# see here for more details
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
startTime: "2022-01-01"
endTime: "2022-06-18"
endTime: "2022-06-30"
symbols:
- BTCUSDT
accounts:
@ -36,7 +36,12 @@ exchangeStrategies:
symbol: BTCUSDT
# interval is how long do you want to update your order price and quantity
interval: 1h
interval: 5m
# ATR window used by Supertrend
window: 49
# ATR Multiplier for calculating super trend prices, the higher, the stronger the trends are
supertrendMultiplier: 4
# leverage is the leverage of the orders
leverage: 1.0
@ -45,18 +50,31 @@ exchangeStrategies:
fastDEMAWindow: 144
slowDEMAWindow: 169
# Supertrend indicator parameters
superTrend:
# ATR window used by Supertrend
averageTrueRangeWindow: 39
# ATR Multiplier for calculating super trend prices, the higher, the stronger the trends are
averageTrueRangeMultiplier: 3
# Use linear regression as trend confirmation
linearRegression:
interval: 5m
window: 80
# TP according to ATR multiple, 0 to disable this
takeProfitMultiplier: 3
TakeProfitAtrMultiplier: 0
# Set SL price to the low of the triggering Kline
stopLossByTriggeringK: true
stopLossByTriggeringK: false
# TP/SL by reversed signals
tpslBySignal: true
# TP/SL by reversed supertrend signal
stopByReversedSupertrend: false
# TP/SL by reversed DEMA signal
stopByReversedDema: false
# TP/SL by reversed linear regression signal
stopByReversedLinGre: false
exits:
# roiStopLoss is the stop loss percentage of the position ROI (currently the price change)
- roiStopLoss:
percentage: 4%
- protectiveStopLoss:
activationRatio: 3%
stopLossRatio: 2%
placeStopOrder: false

View File

@ -58,4 +58,4 @@ bbgo [flags]
* [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot)
* [bbgo version](bbgo_version.md) - show version name
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -42,4 +42,4 @@ bbgo account [--session SESSION] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -51,4 +51,4 @@ bbgo backtest [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -41,4 +41,4 @@ bbgo balances [--session SESSION] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -40,4 +40,4 @@ bbgo build [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -50,4 +50,4 @@ bbgo cancel-order [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -42,4 +42,4 @@ bbgo deposits [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -49,4 +49,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -43,4 +43,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -43,4 +43,4 @@ bbgo kline [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -42,4 +42,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -39,4 +39,4 @@ margin related history
* [bbgo margin loans](bbgo_margin_loans.md) - query loans history
* [bbgo margin repays](bbgo_margin_repays.md) - query repay history
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -42,4 +42,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags]
* [bbgo margin](bbgo_margin.md) - margin related history
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -42,4 +42,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags]
* [bbgo margin](bbgo_margin.md) - margin related history
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -42,4 +42,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags]
* [bbgo margin](bbgo_margin.md) - margin related history
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -41,4 +41,4 @@ bbgo market [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -13,6 +13,7 @@ bbgo optimize [flags]
--json print optimizer metrics in json format
--optimizer-config string config file (default "optimizer.yaml")
--output string backtest report output directory (default "output")
--tsv print optimizer metrics in csv format
```
### Options inherited from parent commands
@ -43,4 +44,4 @@ bbgo optimize [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -43,4 +43,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -41,4 +41,4 @@ bbgo orderupdate [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -13,11 +13,13 @@ bbgo pnl [flags]
### Options
```
-h, --help help for pnl
--include-transfer convert transfer records into trades
--limit int number of trades
--session string target exchange
--symbol string trading symbol
-h, --help help for pnl
--include-transfer convert transfer records into trades
--limit uint number of trades
--session stringArray target exchange sessions
--since string query trades from a time point
--symbol string trading symbol
--sync sync before loading trades
```
### Options inherited from parent commands
@ -48,4 +50,4 @@ bbgo pnl [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -51,4 +51,4 @@ bbgo run [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -45,4 +45,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -43,4 +43,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -43,4 +43,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -41,4 +41,4 @@ bbgo tradeupdate --session=[exchange_name] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -43,4 +43,4 @@ bbgo transfer-history [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -41,4 +41,4 @@ bbgo userdatastream [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

View File

@ -40,4 +40,4 @@ bbgo version [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 17-Jun-2022
###### Auto generated by spf13/cobra on 10-Jul-2022

83
doc/release/v1.36.0.md Normal file
View File

@ -0,0 +1,83 @@
## Fixes
- Fixed backtest stop limit order / stop market order emulation.
- Fixed optimizer panic issue (when report is nil)
- Fixed pnl command market settings.
- Fixed MAX API endpoints.
## Improvements
- Improved supertrend strategy.
- Improved pivotshort strategy.
- Refactor reward service sync.
## Features
- Added tearsheet backend api
- Added seriesExtend for indicators.
- Added progressbar to optimizer.
## New API
- Added ExitMethodSet (trailing stop, roi stop loss, roi take profit, ... etc)
- Added new graceful shutdown API, persistence API, notification API, order executor API.
[Full Changelog](https://github.com/c9s/bbgo/compare/v1.35.0...main)
- [#799](https://github.com/c9s/bbgo/pull/799): Improve supertrend strategy
- [#801](https://github.com/c9s/bbgo/pull/801): feature: optimizer: support --tsv option and render tsv output
- [#800](https://github.com/c9s/bbgo/pull/800): fix: fix exit method for trailing stop
- [#798](https://github.com/c9s/bbgo/pull/798): fix: fix trailingstop and add long position test case
- [#797](https://github.com/c9s/bbgo/pull/797): feature: re-implement trailing stop and add mock test
- [#796](https://github.com/c9s/bbgo/pull/796): strategy/pivotshort: add supportTakeProfit method
- [#793](https://github.com/c9s/bbgo/pull/793): Fix pnl command
- [#795](https://github.com/c9s/bbgo/pull/795): optimizer/fix: prevent from crashing if missing SummaryReport
- [#794](https://github.com/c9s/bbgo/pull/794): strategy/pivotshort: fix resistance updater
- [#792](https://github.com/c9s/bbgo/pull/792): strategy/pivotshort: fix findNextResistancePriceAndPlaceOrders
- [#791](https://github.com/c9s/bbgo/pull/791): strategy: pivotshort: refactor breaklow logics
- [#790](https://github.com/c9s/bbgo/pull/790): fix: strategy: pivoshort: cancel order when shutdown
- [#789](https://github.com/c9s/bbgo/pull/789): strategy: pivotshort: add more improvements for margin
- [#787](https://github.com/c9s/bbgo/pull/787): strategy: pivotshort: use active orderbook to maintain the resistance orders
- [#786](https://github.com/c9s/bbgo/pull/786): strategy: pivotshort: resistance short
- [#731](https://github.com/c9s/bbgo/pull/731): add tearsheet backend api (Sharpe)
- [#784](https://github.com/c9s/bbgo/pull/784): strategy: pivotshort: fix stopEMA
- [#785](https://github.com/c9s/bbgo/pull/785): optimizer: add progressbar
- [#778](https://github.com/c9s/bbgo/pull/778): feature: add seriesExtend
- [#783](https://github.com/c9s/bbgo/pull/783): fix: pivotshort: fix kline history loading
- [#782](https://github.com/c9s/bbgo/pull/782): refactor: moving exit methods from pivotshort to the core
- [#781](https://github.com/c9s/bbgo/pull/781): strategy: pivotshort: optimize and update config
- [#775](https://github.com/c9s/bbgo/pull/775): test: backtest: add order cancel test case
- [#773](https://github.com/c9s/bbgo/pull/773): fix: fix backtest taker order execution
- [#772](https://github.com/c9s/bbgo/pull/772): fix: backtest: fix stop order backtest, add more test cases and assertions
- [#770](https://github.com/c9s/bbgo/pull/770): fix: fix backtest stop limit order matching and add test cases
- [#769](https://github.com/c9s/bbgo/pull/769): backtest-report: sort intervals
- [#768](https://github.com/c9s/bbgo/pull/768): feature: backtest: add ohlc legend
- [#766](https://github.com/c9s/bbgo/pull/766): backtest-report: add time range slider
- [#765](https://github.com/c9s/bbgo/pull/765): improve: backtest-report layout improvements, EMA indicators and fixed the clean up issue
- [#764](https://github.com/c9s/bbgo/pull/764): strategy/pivotshort: refactor exit methods and add protection stop exit method
- [#761](https://github.com/c9s/bbgo/pull/761): datasource: refactor glassnodeapi
- [#760](https://github.com/c9s/bbgo/pull/760): doc: fix link
- [#758](https://github.com/c9s/bbgo/pull/758): improve: add pnl cmd options and fix trade query
- [#757](https://github.com/c9s/bbgo/pull/757): totp-user: add default user 'bbgo'
- [#756](https://github.com/c9s/bbgo/pull/756): refactor: clean up rsmaker, xbalance, dca, pivotshort strategies
- [#755](https://github.com/c9s/bbgo/pull/755): improve: bbgo: call global persistence facade to sync data
- [#754](https://github.com/c9s/bbgo/pull/754): optimizer: refactor max num of process in optimizer configs
- [#750](https://github.com/c9s/bbgo/pull/750): refactor: persistence singleton and improve backtest cancel performance
- [#753](https://github.com/c9s/bbgo/pull/753): optimizer: add max num of thread in config
- [#752](https://github.com/c9s/bbgo/pull/752): Upgrade nextjs from 11 to 12
- [#751](https://github.com/c9s/bbgo/pull/751): fix: reformat go code
- [#746](https://github.com/c9s/bbgo/pull/746): pivotshort: add strategy controller
- [#747](https://github.com/c9s/bbgo/pull/747): strategy/supertrend: use new order executor api
- [#748](https://github.com/c9s/bbgo/pull/748): bollmaker: remove redundant code for adapting new order executor api
- [#749](https://github.com/c9s/bbgo/pull/749): improve: add parallel local process executor for optimizer
- [#639](https://github.com/c9s/bbgo/pull/639): strategy: rsmaker: initial idea prototype
- [#745](https://github.com/c9s/bbgo/pull/745): fix: depth: do not test depth buffer when race is on
- [#744](https://github.com/c9s/bbgo/pull/744): refactor: refactor and update the support strategy
- [#743](https://github.com/c9s/bbgo/pull/743): strategy/bollmaker: refactor and clean up
- [#742](https://github.com/c9s/bbgo/pull/742): refactor: clean up bbgo.Notifiability
- [#739](https://github.com/c9s/bbgo/pull/739): refactor: redesign order executor api
- [#738](https://github.com/c9s/bbgo/pull/738): feature: binance: add binance spot rebate history support
- [#736](https://github.com/c9s/bbgo/pull/736): fix: gosimple alert
- [#737](https://github.com/c9s/bbgo/pull/737): refactor: refactor reward service sync
- [#732](https://github.com/c9s/bbgo/pull/732): Refactor grid panel
- [#734](https://github.com/c9s/bbgo/pull/734): fix: apply gofmt on all files, add revive action

View File

@ -23,12 +23,27 @@ Supertrend strategy needs margin enabled in order to submit short orders, and yo
- The MA window of the ATR indicator used by Supertrend.
- `averageTrueRangeMultiplier`
- Multiplier for calculating upper and lower bond prices, the higher, the stronger the trends are, but also makes it less sensitive.
- `takeProfitMultiplier`
- `linearRegression`
- Use linear regression as trend confirmation
- `interval`
- Time interval of linear regression
- `window`
- Window of linear regression
- `takeProfitAtrMultiplier`
- TP according to ATR multiple, 0 to disable this.
- `stopLossByTriggeringK`
- Set SL price to the low of the triggering Kline.
- `tpslBySignal`
- TP/SL by reversed signals.
- Set SL price to the low/high of the triggering Kline.
- `stopByReversedSupertrend`
- TP/SL by reversed supertrend signal.
- `stopByReversedDema`
- TP/SL by reversed DEMA signal.
- `stopByReversedLinGre`
- TP/SL by reversed linear regression signal.
- `exits`
- Exit methods to TP/SL
- `roiStopLoss`
- The stop loss percentage of the position ROI (currently the price change)
- `percentage`
#### Examples

View File

@ -19,6 +19,8 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
var bidVolume = fixedpoint.Zero
var askVolume = fixedpoint.Zero
var feeUSD = fixedpoint.Zero
var grossProfit = fixedpoint.Zero
var grossLoss = fixedpoint.Zero
if len(trades) == 0 {
return &AverageCostPnlReport{
@ -64,6 +66,12 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
totalNetProfit = totalNetProfit.Add(netProfit)
}
if profit.Sign() > 0 {
grossProfit = grossProfit.Add(profit)
} else if profit.Sign() < 0 {
grossLoss = grossLoss.Add(profit)
}
if trade.IsBuyer {
bidVolume = bidVolume.Add(trade.Quantity)
} else {
@ -96,8 +104,12 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
Profit: totalProfit,
NetProfit: totalNetProfit,
UnrealizedProfit: unrealizedProfit,
AverageCost: position.AverageCost,
FeeInUSD: totalProfit.Sub(totalNetProfit),
CurrencyFees: currencyFees,
GrossProfit: grossProfit,
GrossLoss: grossLoss,
AverageCost: position.AverageCost,
FeeInUSD: totalProfit.Sub(totalNetProfit),
CurrencyFees: currencyFees,
}
}

View File

@ -20,10 +20,14 @@ type AverageCostPnlReport struct {
Symbol string `json:"symbol"`
Market types.Market `json:"market"`
NumTrades int `json:"numTrades"`
Profit fixedpoint.Value `json:"profit"`
NetProfit fixedpoint.Value `json:"netProfit"`
UnrealizedProfit fixedpoint.Value `json:"unrealizedProfit"`
NumTrades int `json:"numTrades"`
Profit fixedpoint.Value `json:"profit"`
UnrealizedProfit fixedpoint.Value `json:"unrealizedProfit"`
NetProfit fixedpoint.Value `json:"netProfit"`
GrossProfit fixedpoint.Value `json:"grossProfit"`
GrossLoss fixedpoint.Value `json:"grossLoss"`
AverageCost fixedpoint.Value `json:"averageCost"`
BuyVolume fixedpoint.Value `json:"buyVolume,omitempty"`
SellVolume fixedpoint.Value `json:"sellVolume,omitempty"`

View File

@ -27,6 +27,7 @@ type StateRecorder struct {
outputDirectory string
strategies []Instance
writers map[types.CsvFormatter]*tsv.Writer
lastLines map[types.CsvFormatter][]string
manifests Manifests
}
@ -34,6 +35,7 @@ func NewStateRecorder(outputDir string) *StateRecorder {
return &StateRecorder{
outputDirectory: outputDir,
writers: make(map[types.CsvFormatter]*tsv.Writer),
lastLines: make(map[types.CsvFormatter][]string),
manifests: make(Manifests),
}
}
@ -42,11 +44,18 @@ func (r *StateRecorder) Snapshot() (int, error) {
var c int
for obj, writer := range r.writers {
records := obj.CsvRecords()
lastLine, hasLastLine := r.lastLines[obj]
for _, record := range records {
if hasLastLine && equalStringSlice(lastLine, record) {
continue
}
if err := writer.Write(record); err != nil {
return c, err
}
c++
r.lastLines[obj] = record
}
writer.Flush()
@ -129,3 +138,19 @@ func (r *StateRecorder) Close() error {
return err
}
func equalStringSlice(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
ad := a[i]
bd := b[i]
if ad != bd {
return false
}
}
return true
}

View File

@ -39,10 +39,16 @@ type SummaryReport struct {
InitialTotalBalances types.BalanceMap `json:"initialTotalBalances"`
FinalTotalBalances types.BalanceMap `json:"finalTotalBalances"`
InitialEquityValue fixedpoint.Value `json:"initialEquityValue"`
FinalEquityValue fixedpoint.Value `json:"finalEquityValue"`
// TotalProfit is the profit aggregated from the symbol reports
TotalProfit fixedpoint.Value `json:"totalProfit,omitempty"`
TotalUnrealizedProfit fixedpoint.Value `json:"totalUnrealizedProfit,omitempty"`
TotalGrossProfit fixedpoint.Value `json:"totalGrossProfit,omitempty"`
TotalGrossLoss fixedpoint.Value `json:"totalGrossLoss,omitempty"`
SymbolReports []SessionSymbolReport `json:"symbolReports,omitempty"`
Manifests Manifests `json:"manifests,omitempty"`
@ -75,13 +81,21 @@ type SessionSymbolReport struct {
Manifests Manifests `json:"manifests,omitempty"`
}
func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value {
return InQuoteAsset(r.InitialBalances, r.Market, r.StartPrice)
}
func (r *SessionSymbolReport) FinalEquityValue() fixedpoint.Value {
return InQuoteAsset(r.FinalBalances, r.Market, r.StartPrice)
}
func (r *SessionSymbolReport) Print(wantBaseAssetBaseline bool) {
color.Green("%s %s PROFIT AND LOSS REPORT", r.Exchange, r.Symbol)
color.Green("===============================================")
r.PnL.Print()
initQuoteAsset := inQuoteAsset(r.InitialBalances, r.Market, r.StartPrice)
finalQuoteAsset := inQuoteAsset(r.FinalBalances, r.Market, r.LastPrice)
initQuoteAsset := r.InitialEquityValue()
finalQuoteAsset := r.FinalEquityValue()
color.Green("INITIAL ASSET IN %s ~= %s %s (1 %s = %v)", r.Market.QuoteCurrency, r.Market.FormatQuantity(initQuoteAsset), r.Market.QuoteCurrency, r.Market.BaseCurrency, r.StartPrice)
color.Green("FINAL ASSET IN %s ~= %s %s (1 %s = %v)", r.Market.QuoteCurrency, r.Market.FormatQuantity(finalQuoteAsset), r.Market.QuoteCurrency, r.Market.BaseCurrency, r.LastPrice)
@ -186,8 +200,8 @@ func AddReportIndexRun(outputDirectory string, run Run) error {
return WriteReportIndex(outputDirectory, reportIndex)
}
// inQuoteAsset converts all balances in quote asset
func inQuoteAsset(balances types.BalanceMap, market types.Market, price fixedpoint.Value) fixedpoint.Value {
// InQuoteAsset converts all balances in quote asset
func InQuoteAsset(balances types.BalanceMap, market types.Market, price fixedpoint.Value) fixedpoint.Value {
quote := balances[market.QuoteCurrency]
base := balances[market.BaseCurrency]
return base.Total().Mul(price).Add(quote.Total())

View File

@ -66,6 +66,8 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) {
}
profitStats.AddProfit(*profit)
Notify(profit)
Notify(profitStats)
})
}

View File

@ -340,9 +340,6 @@ var BacktestCmd = &cobra.Command{
})
dumper := backtest.NewKLineDumper(kLineDataDir)
defer func() {
_ = dumper.Close()
}()
defer func() {
if err := dumper.Close(); err != nil {
log.WithError(err).Errorf("kline dumper can not close files")
@ -496,7 +493,6 @@ var BacktestCmd = &cobra.Command{
}
for _, session := range environ.Sessions() {
for symbol, trades := range session.Trades {
symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Trades)
if err != nil {
@ -507,6 +503,10 @@ var BacktestCmd = &cobra.Command{
summaryReport.SymbolReports = append(summaryReport.SymbolReports, *symbolReport)
summaryReport.TotalProfit = symbolReport.PnL.Profit
summaryReport.TotalUnrealizedProfit = symbolReport.PnL.UnrealizedProfit
summaryReport.InitialEquityValue = summaryReport.InitialEquityValue.Add(symbolReport.InitialEquityValue())
summaryReport.FinalEquityValue = summaryReport.FinalEquityValue.Add(symbolReport.FinalEquityValue())
summaryReport.TotalGrossProfit.Add(symbolReport.PnL.GrossProfit)
summaryReport.TotalGrossLoss.Add(symbolReport.PnL.GrossLoss)
// write report to a file
if generatingReport {

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

@ -608,7 +608,7 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap,
Available: b.Balance,
Locked: b.Locked,
NetAsset: b.Balance.Add(b.Locked).Sub(b.Debt),
Borrowed: b.Debt, // TODO: Replace this with borrow in the newer version
Borrowed: b.Borrowed,
Interest: b.Interest,
}
}

View File

@ -22,8 +22,10 @@ type Account struct {
Locked fixedpoint.Value `json:"locked"`
// v3 fields for M wallet
Debt fixedpoint.Value `json:"debt"`
Interest fixedpoint.Value `json:"interest"`
Debt fixedpoint.Value `json:"debt"`
Principal fixedpoint.Value `json:"principal"`
Borrowed fixedpoint.Value `json:"borrowed"`
Interest fixedpoint.Value `json:"interest"`
// v2 fields
FiatCurrency string `json:"fiat_currency"`

View File

@ -6,7 +6,7 @@ import "github.com/c9s/requestgen"
//go:generate -command PostRequest requestgen -method POST
//go:generate -command DeleteRequest requestgen -method DELETE
//go:generate PostRequest -url "/api/v3/wallet/:walletType/orders" -type CreateWalletOrderRequest -responseType .Order
//go:generate PostRequest -url "/api/v3/wallet/:walletType/order" -type CreateWalletOrderRequest -responseType .Order
type CreateWalletOrderRequest struct {
client requestgen.AuthenticatedAPIClient

View File

@ -1,4 +1,4 @@
// Code generated by "requestgen -method POST -url /api/v3/wallet/:walletType/orders -type CreateWalletOrderRequest -responseType .Order"; DO NOT EDIT.
// Code generated by "requestgen -method POST -url /api/v3/wallet/:walletType/order -type CreateWalletOrderRequest -responseType .Order"; DO NOT EDIT.
package v3
@ -244,7 +244,7 @@ func (c *CreateWalletOrderRequest) Do(ctx context.Context) (*max.Order, error) {
}
query := url.Values{}
apiURL := "/api/v3/wallet/:walletType/orders"
apiURL := "/api/v3/wallet/:walletType/order"
slugs, err := c.GetSlugsMap()
if err != nil {
return nil, err

View File

@ -4,12 +4,12 @@ package indicator
import ()
func (A *ATR) OnUpdate(cb func(value float64)) {
A.UpdateCallbacks = append(A.UpdateCallbacks, cb)
func (inc *ATR) OnUpdate(cb func(value float64)) {
inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb)
}
func (A *ATR) EmitUpdate(value float64) {
for _, cb := range A.UpdateCallbacks {
func (inc *ATR) EmitUpdate(value float64) {
for _, cb := range inc.UpdateCallbacks {
cb(value)
}
}

112
pkg/indicator/atrp.go Normal file
View File

@ -0,0 +1,112 @@
package indicator
import (
"math"
"time"
"github.com/c9s/bbgo/pkg/types"
)
// ATRP is the average true range percentage
// See also https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/atrp
//
// Calculation:
//
// ATRP = (Average True Range / Close) * 100
//
//go:generate callbackgen -type ATRP
type ATRP struct {
types.SeriesBase
types.IntervalWindow
PercentageVolatility types.Float64Slice
PreviousClose float64
RMA *RMA
EndTime time.Time
UpdateCallbacks []func(value float64)
}
func (inc *ATRP) Update(high, low, cloze float64) {
if inc.Window <= 0 {
panic("window must be greater than 0")
}
if inc.RMA == nil {
inc.SeriesBase.Series = inc
inc.RMA = &RMA{
IntervalWindow: types.IntervalWindow{Window: inc.Window},
Adjust: true,
}
inc.PreviousClose = cloze
return
}
// calculate true range
trueRange := high - low
hc := math.Abs(high - inc.PreviousClose)
lc := math.Abs(low - inc.PreviousClose)
if trueRange < hc {
trueRange = hc
}
if trueRange < lc {
trueRange = lc
}
// Note: this is the difference from ATR
trueRange = trueRange / inc.PreviousClose * 100.0
inc.PreviousClose = cloze
// apply rolling moving average
inc.RMA.Update(trueRange)
atr := inc.RMA.Last()
inc.PercentageVolatility.Push(atr / cloze)
}
func (inc *ATRP) Last() float64 {
if inc.RMA == nil {
return 0
}
return inc.RMA.Last()
}
func (inc *ATRP) Index(i int) float64 {
if inc.RMA == nil {
return 0
}
return inc.RMA.Index(i)
}
func (inc *ATRP) Length() int {
if inc.RMA == nil {
return 0
}
return inc.RMA.Length()
}
var _ types.SeriesExtend = &ATRP{}
func (inc *ATRP) CalculateAndUpdate(kLines []types.KLine) {
for _, k := range kLines {
if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) {
continue
}
inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64())
}
inc.EmitUpdate(inc.Last())
inc.EndTime = kLines[len(kLines)-1].EndTime.Time()
}
func (inc *ATRP) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if inc.Interval != interval {
return
}
inc.CalculateAndUpdate(window)
}
func (inc *ATRP) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}

View File

@ -0,0 +1,15 @@
// Code generated by "callbackgen -type ATRP"; DO NOT EDIT.
package indicator
import ()
func (inc *ATRP) OnUpdate(cb func(value float64)) {
inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb)
}
func (inc *ATRP) EmitUpdate(value float64) {
for _, cb := range inc.UpdateCallbacks {
cb(value)
}
}

View File

@ -14,7 +14,7 @@ func TestGetMigrationsMap(t *testing.T) {
func TestMergeMigrationsMap(t *testing.T) {
MergeMigrationsMap(map[int64]*rockhopper.Migration{
2: {},
3: {},
2: &rockhopper.Migration{},
3: &rockhopper.Migration{},
})
}

View File

@ -14,7 +14,7 @@ func TestGetMigrationsMap(t *testing.T) {
func TestMergeMigrationsMap(t *testing.T) {
MergeMigrationsMap(map[int64]*rockhopper.Migration{
2: {},
3: {},
2: &rockhopper.Migration{},
3: &rockhopper.Migration{},
})
}

View File

@ -17,16 +17,31 @@ import (
type MetricValueFunc func(summaryReport *backtest.SummaryReport) fixedpoint.Value
var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fixedpoint.Value {
if summaryReport == nil {
return fixedpoint.Zero
}
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{} {
@ -172,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{}
@ -220,16 +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)
}
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,
})
}

View File

@ -123,7 +123,8 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) {
minMarginLevel := s.MinMarginLevel
curMarginLevel := account.MarginLevel
log.Infof("current account margin level: %s margin ratio: %s, margin tolerance: %s",
bbgo.Notify("%s: current margin level: %s, margin ratio: %s, margin tolerance: %s",
s.ExchangeSession.Name,
account.MarginLevel.String(),
account.MarginRatio.String(),
account.MarginTolerance.String(),
@ -280,7 +281,8 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE
return
}
if s.ExchangeSession.GetAccount().MarginLevel.Compare(s.MinMarginLevel) > 0 {
account := s.ExchangeSession.GetAccount()
if account.MarginLevel.Compare(s.MinMarginLevel) > 0 {
return
}
@ -291,7 +293,6 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE
return
}
account := s.ExchangeSession.GetAccount()
minMarginLevel := s.MinMarginLevel
curMarginLevel := account.MarginLevel
@ -300,7 +301,11 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE
return
}
toRepay := b.Available
toRepay := fixedpoint.Min(b.Borrowed, b.Available)
if toRepay.IsZero() {
return
}
bbgo.Notify(&MarginAction{
Exchange: s.ExchangeSession.ExchangeName,
Action: "Repay",
@ -309,6 +314,7 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE
MarginLevel: curMarginLevel,
MinMarginLevel: minMarginLevel,
})
if err := s.marginBorrowRepay.RepayMarginAsset(context.Background(), event.Asset, toRepay); err != nil {
log.WithError(err).Errorf("margin repay error")
}
@ -366,14 +372,14 @@ func (a *MarginAction) SlackAttachment() slack.Attachment {
// This strategy simply spent all available quote currency to buy the symbol whenever kline gets closed
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
if s.MinMarginLevel.IsZero() {
log.Warnf("minMarginLevel is 0, you should configure this minimal margin ratio for controlling the liquidation risk")
log.Warnf("%s: minMarginLevel is 0, you should configure this minimal margin ratio for controlling the liquidation risk", session.Name)
}
s.ExchangeSession = session
marginBorrowRepay, ok := session.Exchange.(types.MarginBorrowRepayService)
if !ok {
return fmt.Errorf("exchange %s does not implement types.MarginBorrowRepayService", session.ExchangeName)
return fmt.Errorf("exchange %s does not implement types.MarginBorrowRepayService", session.Name)
}
s.marginBorrowRepay = marginBorrowRepay

View File

@ -40,6 +40,7 @@ type BreakLow struct {
func (s *BreakLow) 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})
}
func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
@ -69,7 +70,7 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
}
if lastLow.Compare(s.lastLow) != 0 {
log.Infof("new pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time())
bbgo.Notify("%s new pivot low detected: %f %s", s.Symbol, s.pivot.LastLow(), kline.EndTime.Time().String())
}
s.lastLow = lastLow

View File

@ -54,7 +54,7 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
// use the last kline from the history before we get the next closed kline
if lastKLine != nil {
s.findNextResistancePriceAndPlaceOrders(lastKLine.Close)
s.updateResistanceOrders(lastKLine.Close)
}
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
@ -63,7 +63,7 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
return
}
s.findNextResistancePriceAndPlaceOrders(kline.Close)
s.updateResistanceOrders(kline.Close)
}))
}
@ -75,11 +75,14 @@ func tail(arr []float64, length int) []float64 {
return arr[len(arr)-1-length:]
}
func (s *ResistanceShort) updateNextResistancePrice(closePrice fixedpoint.Value) bool {
// updateCurrentResistancePrice update the current resistance price
// we should only update the resistance price when:
// 1) the close price is already above the current resistance price by (1 + minDistance)
// 2) the next resistance price is lower than the current resistance price.
func (s *ResistanceShort) updateCurrentResistancePrice(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
}
@ -88,9 +91,6 @@ func (s *ResistanceShort) updateNextResistancePrice(closePrice fixedpoint.Value)
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
@ -99,9 +99,8 @@ func (s *ResistanceShort) updateNextResistancePrice(closePrice fixedpoint.Value)
// 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 {
minPriceToUpdate := s.currentResistancePrice.Mul(one.Add(s.MinDistance))
if closePrice.Compare(minPriceToUpdate) > 0 || nextResistancePrice.Compare(s.currentResistancePrice) < 0 {
s.currentResistancePrice = nextResistancePrice
return true
}
@ -109,11 +108,11 @@ func (s *ResistanceShort) updateNextResistancePrice(closePrice fixedpoint.Value)
return false
}
func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) {
func (s *ResistanceShort) updateResistanceOrders(closePrice fixedpoint.Value) {
ctx := context.Background()
resistanceUpdated := s.updateNextResistancePrice(closePrice)
resistanceUpdated := s.updateCurrentResistancePrice(closePrice)
if resistanceUpdated {
bbgo.Notify("Found next resistance price: %f, updating resistance order...", s.currentResistancePrice.Float64())
bbgo.Notify("%s Found next resistance price at %f, updating resistance order...", s.Symbol, s.currentResistancePrice.Float64())
s.placeResistanceOrders(ctx, s.currentResistancePrice)
}
}
@ -151,8 +150,7 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP
spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i)))
price := sellPriceStart.Mul(one.Add(spread))
log.Infof("price = %f", price.Float64())
log.Infof("resistance sell 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{

View File

@ -0,0 +1,67 @@
package supertrend
import (
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
type DoubleDema struct {
Interval types.Interval `json:"interval"`
// FastDEMAWindow DEMA window for checking breakout
FastDEMAWindow int `json:"fastDEMAWindow"`
// SlowDEMAWindow DEMA window for checking breakout
SlowDEMAWindow int `json:"slowDEMAWindow"`
fastDEMA *indicator.DEMA
slowDEMA *indicator.DEMA
}
// getDemaSignal get current DEMA signal
func (dd *DoubleDema) getDemaSignal(openPrice float64, closePrice float64) types.Direction {
var demaSignal types.Direction = types.DirectionNone
if closePrice > dd.fastDEMA.Last() && closePrice > dd.slowDEMA.Last() && !(openPrice > dd.fastDEMA.Last() && openPrice > dd.slowDEMA.Last()) {
demaSignal = types.DirectionUp
} else if closePrice < dd.fastDEMA.Last() && closePrice < dd.slowDEMA.Last() && !(openPrice < dd.fastDEMA.Last() && openPrice < dd.slowDEMA.Last()) {
demaSignal = types.DirectionDown
}
return demaSignal
}
// preloadDema preloads DEMA indicators
func (dd *DoubleDema) preloadDema(kLineStore *bbgo.MarketDataStore) {
if klines, ok := kLineStore.KLinesOfInterval(dd.fastDEMA.Interval); ok {
for i := 0; i < len(*klines); i++ {
dd.fastDEMA.Update((*klines)[i].GetClose().Float64())
}
}
if klines, ok := kLineStore.KLinesOfInterval(dd.slowDEMA.Interval); ok {
for i := 0; i < len(*klines); i++ {
dd.slowDEMA.Update((*klines)[i].GetClose().Float64())
}
}
}
// newDoubleDema initializes double DEMA indicators
func newDoubleDema(kLineStore *bbgo.MarketDataStore, interval types.Interval, fastDEMAWindow int, slowDEMAWindow int) *DoubleDema {
dd := DoubleDema{Interval: interval, FastDEMAWindow: fastDEMAWindow, SlowDEMAWindow: slowDEMAWindow}
// DEMA
if dd.FastDEMAWindow == 0 {
dd.FastDEMAWindow = 144
}
dd.fastDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: dd.Interval, Window: dd.FastDEMAWindow}}
dd.fastDEMA.Bind(kLineStore)
if dd.SlowDEMAWindow == 0 {
dd.SlowDEMAWindow = 169
}
dd.slowDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: dd.Interval, Window: dd.SlowDEMAWindow}}
dd.slowDEMA.Bind(kLineStore)
dd.preloadDema(kLineStore)
return &dd
}

View File

@ -0,0 +1,73 @@
package supertrend
import (
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
// LinGre is Linear Regression baseline
type LinGre struct {
types.IntervalWindow
baseLineSlope float64
}
// Update Linear Regression baseline slope
func (lg *LinGre) Update(klines []types.KLine) {
if len(klines) < lg.Window {
lg.baseLineSlope = 0
return
}
var sumX, sumY, sumXSqr, sumXY float64 = 0, 0, 0, 0
end := len(klines) - 1 // The last kline
for i := end; i >= end-lg.Window+1; i-- {
val := klines[i].GetClose().Float64()
per := float64(end - i + 1)
sumX += per
sumY += val
sumXSqr += per * per
sumXY += val * per
}
length := float64(lg.Window)
slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX)
average := sumY / length
endPrice := average - slope*sumX/length + slope
startPrice := endPrice + slope*(length-1)
lg.baseLineSlope = (length - 1) / (endPrice - startPrice)
log.Debugf("linear regression baseline slope: %f", lg.baseLineSlope)
}
func (lg *LinGre) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if lg.Interval != interval {
return
}
lg.Update(window)
}
func (lg *LinGre) Bind(updater indicator.KLineWindowUpdater) {
updater.OnKLineWindowUpdate(lg.handleKLineWindowUpdate)
}
// GetSignal get linear regression signal
func (lg *LinGre) GetSignal() types.Direction {
var lgSignal types.Direction = types.DirectionNone
switch {
case lg.baseLineSlope > 0:
lgSignal = types.DirectionUp
case lg.baseLineSlope < 0:
lgSignal = types.DirectionDown
}
return lgSignal
}
// preloadLinGre preloads linear regression indicator
func (lg *LinGre) preload(kLineStore *bbgo.MarketDataStore) {
if klines, ok := kLineStore.KLinesOfInterval(lg.Interval); ok {
lg.Update((*klines)[0:])
}
}

View File

@ -3,10 +3,9 @@ package supertrend
import (
"context"
"fmt"
"os"
"sync"
"github.com/c9s/bbgo/pkg/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -18,10 +17,9 @@ import (
const ID = "supertrend"
const stateKey = "state-v1"
var log = logrus.WithField("strategy", ID)
// TODO: limit order for ATR TP
func init() {
// Register the pointer of the strategy struct,
// so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON)
@ -30,57 +28,57 @@ func init() {
}
type Strategy struct {
*bbgo.Persistence
Environment *bbgo.Environment
session *bbgo.ExchangeSession
Market types.Market
// persistence fields
Position *types.Position `json:"position,omitempty" persistence:"position"`
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
// Order and trade
orderExecutor *bbgo.GeneralOrderExecutor
// groupID is the group ID used for the strategy instance for canceling orders
groupID uint32
stopC chan struct{}
Position *types.Position `persistence:"position"`
ProfitStats *types.ProfitStats `persistence:"profit_stats"`
TradeStats *types.TradeStats `persistence:"trade_stats"`
// Symbol is the market symbol you want to trade
Symbol string `json:"symbol"`
// Interval is how long do you want to update your order price and quantity
Interval types.Interval `json:"interval"`
types.IntervalWindow
// Double DEMA
doubleDema *DoubleDema
// FastDEMAWindow DEMA window for checking breakout
FastDEMAWindow int `json:"fastDEMAWindow"`
// SlowDEMAWindow DEMA window for checking breakout
SlowDEMAWindow int `json:"slowDEMAWindow"`
fastDEMA *indicator.DEMA
slowDEMA *indicator.DEMA
// SuperTrend indicator
// SuperTrend SuperTrend `json:"superTrend"`
Supertrend *indicator.Supertrend
// SupertrendWindow ATR window for calculation of supertrend
SupertrendWindow int `json:"supertrendWindow"`
// SupertrendMultiplier ATR multiplier for calculation of supertrend
SupertrendMultiplier float64 `json:"supertrendMultiplier"`
// LinearRegression Use linear regression as trend confirmation
LinearRegression *LinGre `json:"linearRegression,omitempty"`
// Leverage
Leverage float64 `json:"leverage"`
// TakeProfitMultiplier TP according to ATR multiple, 0 to disable this
TakeProfitMultiplier float64 `json:"takeProfitMultiplier"`
// TakeProfitAtrMultiplier TP according to ATR multiple, 0 to disable this
TakeProfitAtrMultiplier float64 `json:"takeProfitAtrMultiplier"`
// StopLossByTriggeringK Set SL price to the low of the triggering Kline
// StopLossByTriggeringK Set SL price to the low/high of the triggering Kline
StopLossByTriggeringK bool `json:"stopLossByTriggeringK"`
// TPSLBySignal TP/SL by reversed signals
TPSLBySignal bool `json:"tpslBySignal"`
// StopByReversedSupertrend TP/SL by reversed supertrend signal
StopByReversedSupertrend bool `json:"stopByReversedSupertrend"`
// StopByReversedDema TP/SL by reversed DEMA signal
StopByReversedDema bool `json:"stopByReversedDema"`
// StopByReversedLinGre TP/SL by reversed linear regression signal
StopByReversedLinGre bool `json:"stopByReversedLinGre"`
// ExitMethods Exit methods
ExitMethods bbgo.ExitMethodSet `json:"exits"`
session *bbgo.ExchangeSession
orderExecutor *bbgo.GeneralOrderExecutor
currentTakeProfitPrice fixedpoint.Value
currentStopLossPrice fixedpoint.Value
@ -105,7 +103,7 @@ func (s *Strategy) Validate() error {
return errors.New("interval is required")
}
if s.Leverage == 0.0 {
if s.Leverage <= 0.0 {
return errors.New("leverage is required")
}
@ -114,6 +112,7 @@ func (s *Strategy) Validate() error {
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LinearRegression.Interval})
}
// Position control
@ -141,8 +140,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
orderForm := s.generateOrderForm(side, quantity, types.SideEffectTypeAutoRepay)
log.Infof("submit close position order %v", orderForm)
bbgo.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage)
bbgo.Notify("submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, orderForm)
_, err := s.orderExecutor.SubmitOrders(ctx, orderForm)
if err != nil {
@ -153,43 +151,88 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
return err
}
// preloadSupertrend preloads supertrend indicator
func preloadSupertrend(supertrend *indicator.Supertrend, kLineStore *bbgo.MarketDataStore) {
if klines, ok := kLineStore.KLinesOfInterval(supertrend.Interval); ok {
for i := 0; i < len(*klines); i++ {
supertrend.Update((*klines)[i].GetHigh().Float64(), (*klines)[i].GetLow().Float64(), (*klines)[i].GetClose().Float64())
}
}
}
// setupIndicators initializes indicators
func (s *Strategy) setupIndicators() {
if s.FastDEMAWindow == 0 {
s.FastDEMAWindow = 144
}
s.fastDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.FastDEMAWindow}}
// K-line store for indicators
kLineStore, _ := s.session.MarketDataStore(s.Symbol)
if s.SlowDEMAWindow == 0 {
s.SlowDEMAWindow = 169
}
s.slowDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.SlowDEMAWindow}}
// Double DEMA
s.doubleDema = newDoubleDema(kLineStore, s.Interval, s.FastDEMAWindow, s.SlowDEMAWindow)
if s.SupertrendWindow == 0 {
s.SupertrendWindow = 39
// Supertrend
if s.Window == 0 {
s.Window = 39
}
if s.SupertrendMultiplier == 0 {
s.SupertrendMultiplier = 3
}
s.Supertrend = &indicator.Supertrend{IntervalWindow: types.IntervalWindow{Window: s.SupertrendWindow, Interval: s.Interval}, ATRMultiplier: s.SupertrendMultiplier}
s.Supertrend.AverageTrueRange = &indicator.ATR{IntervalWindow: types.IntervalWindow{Window: s.SupertrendWindow, Interval: s.Interval}}
s.Supertrend = &indicator.Supertrend{IntervalWindow: types.IntervalWindow{Window: s.Window, Interval: s.Interval}, ATRMultiplier: s.SupertrendMultiplier}
s.Supertrend.AverageTrueRange = &indicator.ATR{IntervalWindow: types.IntervalWindow{Window: s.Window, Interval: s.Interval}}
s.Supertrend.Bind(kLineStore)
preloadSupertrend(s.Supertrend, kLineStore)
// Linear Regression
if s.LinearRegression != nil {
if s.LinearRegression.Window == 0 {
s.LinearRegression = nil
} else if s.LinearRegression.Interval == "" {
s.LinearRegression = nil
} else {
s.LinearRegression.Bind(kLineStore)
s.LinearRegression.preload(kLineStore)
}
}
}
// updateIndicators updates indicators
func (s *Strategy) updateIndicators(kline types.KLine) {
closePrice := kline.GetClose().Float64()
func (s *Strategy) shouldStop(kline types.KLine, stSignal types.Direction, demaSignal types.Direction, lgSignal types.Direction) bool {
stopNow := false
base := s.Position.GetBase()
baseSign := base.Sign()
// Update indicators
if kline.Interval == s.fastDEMA.Interval {
s.fastDEMA.Update(closePrice)
if s.StopLossByTriggeringK && !s.currentStopLossPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentStopLossPrice) > 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentStopLossPrice) < 0)) {
// SL by triggering Kline low/high
bbgo.Notify("%s stop loss by triggering the kline low/high", s.Symbol)
stopNow = true
} else if s.TakeProfitAtrMultiplier > 0 && !s.currentTakeProfitPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) < 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) > 0)) {
// TP by multiple of ATR
bbgo.Notify("%s take profit by multiple of ATR", s.Symbol)
stopNow = true
} else if s.StopByReversedSupertrend && ((baseSign < 0 && stSignal == types.DirectionUp) || (baseSign > 0 && stSignal == types.DirectionDown)) {
// Use supertrend signal to TP/SL
bbgo.Notify("%s stop by the reversed signal of Supertrend", s.Symbol)
stopNow = true
} else if s.StopByReversedDema && ((baseSign < 0 && demaSignal == types.DirectionUp) || (baseSign > 0 && demaSignal == types.DirectionDown)) {
// Use DEMA signal to TP/SL
bbgo.Notify("%s stop by the reversed signal of DEMA", s.Symbol)
stopNow = true
} else if s.StopByReversedLinGre && ((baseSign < 0 && lgSignal == types.DirectionUp) || (baseSign > 0 && lgSignal == types.DirectionDown)) {
// Use linear regression signal to TP/SL
bbgo.Notify("%s stop by the reversed signal of linear regression", s.Symbol)
stopNow = true
}
if kline.Interval == s.slowDEMA.Interval {
s.slowDEMA.Update(closePrice)
}
if kline.Interval == s.Supertrend.Interval {
s.Supertrend.Update(kline.GetHigh().Float64(), kline.GetLow().Float64(), closePrice)
return stopNow
}
func (s *Strategy) getSide(stSignal types.Direction, demaSignal types.Direction, lgSignal types.Direction) types.SideType {
var side types.SideType
if stSignal == types.DirectionUp && demaSignal == types.DirectionUp && (s.LinearRegression == nil || lgSignal == types.DirectionUp) {
side = types.SideTypeBuy
} else if stSignal == types.DirectionDown && demaSignal == types.DirectionDown && (s.LinearRegression == nil || lgSignal == types.DirectionDown) {
side = types.SideTypeSell
}
return side
}
func (s *Strategy) generateOrderForm(side types.SideType, quantity fixedpoint.Value, marginOrderSideEffect types.MarginOrderSideEffectType) types.SubmitOrder {
@ -200,7 +243,6 @@ func (s *Strategy) generateOrderForm(side types.SideType, quantity fixedpoint.Va
Type: types.OrderTypeMarket,
Quantity: quantity,
MarginSideEffect: marginOrderSideEffect,
GroupID: s.groupID,
}
return orderForm
@ -223,9 +265,11 @@ func (s *Strategy) calculateQuantity(currentPrice fixedpoint.Value) fixedpoint.V
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
s.session = session
s.currentStopLossPrice = fixedpoint.Zero
s.currentTakeProfitPrice = fixedpoint.Zero
// calculate group id for orders
instanceID := s.InstanceID()
s.groupID = util.FNV32(instanceID)
// If position is nil, we need to allocate a new position for calculation
if s.Position == nil {
@ -235,6 +279,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.Position.Strategy = ID
s.Position.StrategyInstanceID = s.InstanceID()
// Profit stats
if s.ProfitStats == nil {
s.ProfitStats = types.NewProfitStats(s.Market)
}
if s.TradeStats == nil {
s.TradeStats = types.NewTradeStats(s.Symbol)
}
// Set fee rate
if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 {
s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{
@ -243,15 +296,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
})
}
// Profit
if s.ProfitStats == nil {
s.ProfitStats = types.NewProfitStats(s.Market)
}
// Setup order executor
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
s.orderExecutor.BindEnvironment(s.Environment)
s.orderExecutor.BindProfitStats(s.ProfitStats)
s.orderExecutor.BindTradeStats(s.TradeStats)
s.orderExecutor.Bind()
// Sync position to redis on trade
@ -259,16 +308,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
bbgo.Sync(s)
})
s.stopC = make(chan struct{})
// StrategyController
s.Status = types.StrategyStatusRunning
s.OnSuspend(func() {
_ = s.orderExecutor.GracefulCancel(ctx)
_ = s.Persistence.Sync(s)
bbgo.Sync(s)
})
s.OnEmergencyStop(func() {
_ = s.orderExecutor.GracefulCancel(ctx)
// Close 100% position
@ -278,108 +323,77 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
// Setup indicators
s.setupIndicators()
s.currentStopLossPrice = fixedpoint.Zero
s.currentTakeProfitPrice = fixedpoint.Zero
// Exit methods
for _, method := range s.ExitMethods {
method.Bind(session, s.orderExecutor)
}
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
// StrategyController
if s.Status != types.StrategyStatusRunning {
return
}
// skip k-lines from other symbols or other intervals
if kline.Symbol != s.Symbol || kline.Interval != s.Interval {
return
}
closePrice := kline.GetClose()
openPrice := kline.GetOpen()
closePrice64 := closePrice.Float64()
openPrice64 := openPrice.Float64()
// Update indicators
s.updateIndicators(kline)
// Get signals
closePrice := kline.GetClose().Float64()
openPrice := kline.GetOpen().Float64()
// Supertrend signal
stSignal := s.Supertrend.GetSignal()
var demaSignal types.Direction
if closePrice > s.fastDEMA.Last() && closePrice > s.slowDEMA.Last() && !(openPrice > s.fastDEMA.Last() && openPrice > s.slowDEMA.Last()) {
demaSignal = types.DirectionUp
} else if closePrice < s.fastDEMA.Last() && closePrice < s.slowDEMA.Last() && !(openPrice < s.fastDEMA.Last() && openPrice < s.slowDEMA.Last()) {
demaSignal = types.DirectionDown
} else {
demaSignal = types.DirectionNone
// DEMA signal
demaSignal := s.doubleDema.getDemaSignal(openPrice64, closePrice64)
// Linear Regression signal
var lgSignal types.Direction
if s.LinearRegression != nil {
lgSignal = s.LinearRegression.GetSignal()
}
base := s.Position.GetBase()
baseSign := base.Sign()
// TP/SL if there's non-dust position and meets the criteria
if !s.Market.IsDustQuantity(s.Position.GetBase().Abs(), closePrice) && s.shouldStop(kline, stSignal, demaSignal, lgSignal) {
if err := s.ClosePosition(ctx, fixedpoint.One); err == nil {
s.currentStopLossPrice = fixedpoint.Zero
s.currentTakeProfitPrice = fixedpoint.Zero
}
}
// TP/SL if there's non-dust position
if !s.Market.IsDustQuantity(base.Abs(), kline.GetClose()) {
if s.StopLossByTriggeringK && !s.currentStopLossPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentStopLossPrice) > 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentStopLossPrice) < 0)) {
// SL by triggering Kline low
log.Infof("%s SL by triggering Kline low", s.Symbol)
bbgo.Notify("%s StopLoss by triggering the kline low", s.Symbol)
if err := s.ClosePosition(ctx, fixedpoint.One); err == nil {
s.currentStopLossPrice = fixedpoint.Zero
s.currentTakeProfitPrice = fixedpoint.Zero
}
} else if s.TakeProfitMultiplier > 0 && !s.currentTakeProfitPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) < 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) > 0)) {
// TP by multiple of ATR
log.Infof("%s TP by multiple of ATR", s.Symbol)
bbgo.Notify("%s TakeProfit by multiple of ATR", s.Symbol)
if err := s.ClosePosition(ctx, fixedpoint.One); err == nil {
s.currentStopLossPrice = fixedpoint.Zero
s.currentTakeProfitPrice = fixedpoint.Zero
}
} else if s.TPSLBySignal {
// Use signals to TP/SL
log.Infof("%s TP/SL by reverse of DEMA or Supertrend", s.Symbol)
bbgo.Notify("%s TP/SL by reverse of DEMA or Supertrend", s.Symbol)
if (baseSign < 0 && (stSignal == types.DirectionUp || demaSignal == types.DirectionUp)) || (baseSign > 0 && (stSignal == types.DirectionDown || demaSignal == types.DirectionDown)) {
if err := s.ClosePosition(ctx, fixedpoint.One); err == nil {
s.currentStopLossPrice = fixedpoint.Zero
s.currentTakeProfitPrice = fixedpoint.Zero
}
}
// Get order side
side := s.getSide(stSignal, demaSignal, lgSignal)
// Set TP/SL price if needed
if side == types.SideTypeBuy {
if s.StopLossByTriggeringK {
s.currentStopLossPrice = kline.GetLow()
}
if s.TakeProfitAtrMultiplier > 0 {
s.currentTakeProfitPrice = closePrice.Add(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier))
}
} else if side == types.SideTypeSell {
if s.StopLossByTriggeringK {
s.currentStopLossPrice = kline.GetHigh()
}
if s.TakeProfitAtrMultiplier > 0 {
s.currentTakeProfitPrice = closePrice.Sub(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier))
}
}
// Open position
var side types.SideType
if stSignal == types.DirectionUp && demaSignal == types.DirectionUp {
side = types.SideTypeBuy
if s.StopLossByTriggeringK {
s.currentStopLossPrice = kline.GetLow()
}
if s.TakeProfitMultiplier > 0 {
s.currentTakeProfitPrice = kline.GetClose().Add(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitMultiplier))
}
} else if stSignal == types.DirectionDown && demaSignal == types.DirectionDown {
side = types.SideTypeSell
if s.StopLossByTriggeringK {
s.currentStopLossPrice = kline.GetHigh()
}
if s.TakeProfitMultiplier > 0 {
s.currentTakeProfitPrice = kline.GetClose().Sub(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitMultiplier))
}
}
// The default value of side is an empty string. Unless side is set by the checks above, the result of the following condition is false
if side == types.SideTypeSell || side == types.SideTypeBuy {
log.Infof("open %s position for signal %v", s.Symbol, side)
bbgo.Notify("open %s position for signal %v", s.Symbol, side)
// Close opposite position if any
if !s.Position.IsDust(kline.GetClose()) {
if !s.Position.IsDust(closePrice) {
if (side == types.SideTypeSell && s.Position.IsLong()) || (side == types.SideTypeBuy && s.Position.IsShort()) {
log.Infof("close existing %s position before open a new position", s.Symbol)
bbgo.Notify("close existing %s position before open a new position", s.Symbol)
_ = s.ClosePosition(ctx, fixedpoint.One)
} else {
log.Infof("existing %s position has the same direction with the signal", s.Symbol)
bbgo.Notify("existing %s position has the same direction with the signal", s.Symbol)
return
}
}
orderForm := s.generateOrderForm(side, s.calculateQuantity(kline.GetClose()), types.SideEffectTypeMarginBuy)
orderForm := s.generateOrderForm(side, s.calculateQuantity(closePrice), types.SideEffectTypeMarginBuy)
log.Infof("submit open position order %v", orderForm)
_, err := s.orderExecutor.SubmitOrders(ctx, orderForm)
if err != nil {
@ -387,14 +401,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
bbgo.Notify("can not place %s open position order", s.Symbol)
}
}
})
}))
// Graceful shutdown
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
close(s.stopC)
_ = s.orderExecutor.GracefulCancel(ctx)
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
})
return nil

View File

@ -94,6 +94,10 @@ type SeriesExtend interface {
Covariance(b Series, length int) float64
Correlation(b Series, length int, method ...CorrFunc) float64
Rank(length int) SeriesExtend
Sigmoid() SeriesExtend
Softmax(window int) SeriesExtend
Entropy(window int) float64
CrossEntropy(b Series, window int) float64
}
type SeriesBase struct {
@ -524,7 +528,69 @@ var _ Series = &MulSeriesResult{}
// if limit is given, will only calculate the first limit numbers (a.Index[0..limit])
// otherwise will operate on all elements
func Dot(a interface{}, b interface{}, limit ...int) float64 {
return Sum(Mul(a, b), limit...)
var aaf float64
var aas Series
var bbf float64
var bbs Series
var isaf, isbf bool
switch tp := a.(type) {
case float64:
aaf = tp
isaf = true
case Series:
aas = tp
isaf = false
default:
panic("input should be either Series or float64")
}
switch tp := b.(type) {
case float64:
bbf = tp
isbf = true
case Series:
bbs = tp
isbf = false
default:
panic("input should be either Series or float64")
}
l := 1
if len(limit) > 0 {
l = limit[0]
} else if isaf && isbf {
l = 1
} else {
if !isaf {
l = aas.Length()
}
if !isbf {
if l > bbs.Length() {
l = bbs.Length()
}
}
}
if isaf && isbf {
return aaf * bbf * float64(l)
} else if isaf && !isbf {
sum := 0.
for i := 0; i < l; i++ {
sum += aaf * bbs.Index(i)
}
return sum
} else if !isaf && isbf {
sum := 0.
for i := 0; i < l; i++ {
sum += aas.Index(i) * bbf
}
return sum
} else {
sum := 0.
for i := 0; i < l; i++ {
sum += aas.Index(i) * bbs.Index(i)
}
return sum
}
}
// Extract elements from the Series to a float64 array, following the order of Index(0..limit)
@ -881,4 +947,175 @@ func Rolling(a Series, window int) *RollingResult {
return &RollingResult{a, window}
}
type SigmoidResult struct {
a Series
}
func (s *SigmoidResult) Last() float64 {
return 1. / (1. + math.Exp(-s.a.Last()))
}
func (s *SigmoidResult) Index(i int) float64 {
return 1. / (1. + math.Exp(-s.a.Index(i)))
}
func (s *SigmoidResult) Length() int {
return s.a.Length()
}
// Sigmoid returns the input values in range of -1 to 1
// along the sigmoid or s-shaped curve.
// Commonly used in machine learning while training neural networks
// as an activation function.
func Sigmoid(a Series) SeriesExtend {
return NewSeries(&SigmoidResult{a})
}
// SoftMax returns the input value in the range of 0 to 1
// with sum of all the probabilities being equal to one.
// It is commonly used in machine learning neural networks.
// Will return Softmax SeriesExtend result based in latest [window] numbers from [a] Series
func Softmax(a Series, window int) SeriesExtend {
s := 0.0
max := Highest(a, window)
for i := 0; i < window; i++ {
s += math.Exp(a.Index(i) - max)
}
out := NewQueue(window)
for i := window - 1; i >= 0; i-- {
out.Update(math.Exp(a.Index(i)-max) / s)
}
return out
}
// Entropy computes the Shannon entropy of a distribution or the distance between
// two distributions. The natural logarithm is used.
// - sum(v * ln(v))
func Entropy(a Series, window int) (e float64) {
for i := 0; i < window; i++ {
v := a.Index(i)
if v != 0 {
e -= v * math.Log(v)
}
}
return e
}
// CrossEntropy computes the cross-entropy between the two distributions
func CrossEntropy(a, b Series, window int) (e float64) {
for i := 0; i < window; i++ {
v := a.Index(i)
if v != 0 {
e -= v * math.Log(b.Index(i))
}
}
return e
}
func sigmoid(z float64) float64 {
return 1. / (1. + math.Exp(-z))
}
func propagate(w []float64, gradient float64, x [][]float64, y []float64) (float64, []float64, float64) {
logloss_epoch := 0.0
var activations []float64
var dw []float64
m := len(y)
db := 0.0
for i, xx := range x {
result := 0.0
for j, ww := range w {
result += ww * xx[j]
}
a := sigmoid(result + gradient)
activations = append(activations, a)
logloss := a*math.Log1p(y[i]) + (1.-a)*math.Log1p(1-y[i])
logloss_epoch += logloss
db += a - y[i]
}
for j := range w {
err := 0.0
for i, xx := range x {
err_i := activations[i] - y[i]
err += err_i * xx[j]
}
err /= float64(m)
dw = append(dw, err)
}
cost := -(logloss_epoch / float64(len(x)))
db /= float64(m)
return cost, dw, db
}
func LogisticRegression(x []Series, y Series, lookback, iterations int, learningRate float64) *LogisticRegressionModel {
features := len(x)
if features == 0 {
panic("no feature to train")
}
w := make([]float64, features)
if lookback > x[0].Length() {
lookback = x[0].Length()
}
xx := make([][]float64, lookback)
for i := 0; i < lookback; i++ {
for j := 0; j < features; j++ {
xx[i] = append(xx[i], x[j].Index(lookback-i-1))
}
}
yy := Reverse(y, lookback)
b := 0.
for i := 0; i < iterations; i++ {
_, dw, db := propagate(w, b, xx, yy)
for j := range w {
w[j] = w[j] - (learningRate * dw[j])
}
b -= learningRate * db
}
return &LogisticRegressionModel{
Weight: w,
Gradient: b,
LearningRate: learningRate,
}
}
type LogisticRegressionModel struct {
Weight []float64
Gradient float64
LearningRate float64
}
/*
// Might not be correct.
// Please double check before uncomment this
func (l *LogisticRegressionModel) Update(x []float64, y float64) {
z := 0.0
for i, w := l.Weight {
z += w * x[i]
}
a := sigmoid(z + l.Gradient)
//logloss := a * math.Log1p(y) + (1.-a)*math.Log1p(1-y)
db = a - y
var dw []float64
for j, ww := range l.Weight {
err := db * x[j]
dw = append(dw, err)
}
for i := range l.Weight {
l.Weight[i] -= l.LearningRate * dw[i]
}
l.Gradient -= l.LearningRate * db
}
*/
func (l *LogisticRegressionModel) Predict(x []float64) float64 {
z := 0.0
for i, w := range l.Weight {
z += w * x[i]
}
return sigmoid(z + l.Gradient)
}
// TODO: ta.linreg

View File

@ -2,6 +2,7 @@ package types
import (
"github.com/stretchr/testify/assert"
"gonum.org/v1/gonum/stat"
"testing"
)
@ -84,3 +85,62 @@ func TestSkew(t *testing.T) {
sk := Skew(&a, 4)
assert.InDelta(t, sk, 1.129338, 0.001)
}
func TestEntropy(t *testing.T) {
var a = Float64Slice{.2, .0, .6, .2}
e := stat.Entropy(a)
assert.InDelta(t, e, Entropy(&a, a.Length()), 0.0001)
}
func TestCrossEntropy(t *testing.T) {
var a = Float64Slice{.2, .0, .6, .2}
var b = Float64Slice{.3, .6, .0, .1}
e := stat.CrossEntropy(a, b)
assert.InDelta(t, e, CrossEntropy(&a, &b, a.Length()), 0.0001)
}
func TestSoftmax(t *testing.T) {
var a = Float64Slice{3.0, 1.0, 0.2}
out := Softmax(&a, a.Length())
r := Float64Slice{0.8360188027814407, 0.11314284146556013, 0.05083835575299916}
for i := 0; i < out.Length(); i++ {
assert.InDelta(t, r.Index(i), out.Index(i), 0.001)
}
}
func TestSigmoid(t *testing.T) {
a := Float64Slice{3.0, 1.0, 2.1}
out := Sigmoid(&a)
r := Float64Slice{0.9525741268224334, 0.7310585786300049, 0.8909031788043871}
for i := 0; i < out.Length(); i++ {
assert.InDelta(t, r.Index(i), out.Index(i), 0.001)
}
}
// from https://en.wikipedia.org/wiki/Logistic_regression
func TestLogisticRegression(t *testing.T) {
a := []Float64Slice{{0.5, 0.75, 1., 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3., 3.25, 3.5, 4., 4.25, 4.5, 4.75, 5., 5.5}}
b := Float64Slice{0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1}
var x []Series
x = append(x, &a[0])
model := LogisticRegression(x, &b, a[0].Length(), 8000, 0.0009)
inputs := []float64{1., 2., 2.7, 3., 4., 5.}
results := []bool{false, false, true, true, true, true}
for i, x := range inputs {
input := []float64{x}
pred := model.Predict(input)
assert.Equal(t, pred > 0.5, results[i])
}
}
func TestDot(t *testing.T) {
a := Float64Slice{7, 6, 5, 4, 3, 2, 1, 0}
b := Float64Slice{200., 201., 203., 204., 203., 199.}
out1 := Dot(&a, &b, 3)
assert.InDelta(t, out1, 611., 0.001)
out2 := Dot(&a, 3., 2)
assert.InDelta(t, out2, 3., 0.001)
out3 := Dot(3., &a, 2)
assert.InDelta(t, out2, out3, 0.001)
}

View File

@ -609,7 +609,7 @@ type KLineCallBack func(k KLine)
func KLineWith(symbol string, interval Interval, callback KLineCallBack) KLineCallBack {
return func(k KLine) {
if k.Symbol != symbol || k.Interval != interval {
if k.Symbol != symbol || (k.Interval != "" && k.Interval != interval) {
return
}
callback(k)

View File

@ -124,3 +124,19 @@ func (s *SeriesBase) Correlation(b Series, length int, method ...CorrFunc) float
func (s *SeriesBase) Rank(length int) SeriesExtend {
return Rank(s, length)
}
func (s *SeriesBase) Sigmoid() SeriesExtend {
return Sigmoid(s)
}
func (s *SeriesBase) Softmax(window int) SeriesExtend {
return Softmax(s, window)
}
func (s *SeriesBase) Entropy(window int) float64 {
return Entropy(s, window)
}
func (s *SeriesBase) CrossEntropy(b Series, window int) float64 {
return CrossEntropy(s, b, window)
}

View File

@ -2,6 +2,7 @@ package util
import (
"reflect"
"time"
"github.com/c9s/bbgo/pkg/fixedpoint"
)
@ -11,7 +12,7 @@ import (
func FilterSimpleArgs(args []interface{}) (simpleArgs []interface{}) {
for _, arg := range args {
switch arg.(type) {
case int, int64, int32, uint64, uint32, string, []byte, float64, float32, fixedpoint.Value:
case int, int64, int32, uint64, uint32, string, []byte, float64, float32, fixedpoint.Value, time.Time:
simpleArgs = append(simpleArgs, arg)
default:
rt := reflect.TypeOf(arg)

View File

@ -1,8 +1,8 @@
//go:build !release
// +build !release
package version
const Version = "v1.35.0-daaa3352-dev"
const Version = "v1.36.0-cc8821bb-dev"
const VersionGitRef = "cc8821bb"
const VersionGitRef = "daaa3352"

View File

@ -1,8 +1,8 @@
//go:build release
// +build release
package version
const Version = "v1.35.0-daaa3352"
const Version = "v1.36.0-cc8821bb"
const VersionGitRef = "cc8821bb"
const VersionGitRef = "daaa3352"