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 /config/bbgo.yaml
/localconfig
/pkg/server/assets.go /pkg/server/assets.go
bbgo.sqlite3 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) => { const fetchOrders = (basePath: string, runID: string) => {
return fetch( return fetch(
`${basePath}/${runID}/orders.tsv`, `${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) => { const parseInterval = (s: string) => {
switch (s) { switch (s) {
case "1m": case "1m":
@ -390,7 +416,7 @@ const TradingViewChart = (props: TradingViewChartProps) => {
const resizeObserver = useRef<any>(); const resizeObserver = useRef<any>();
const intervals = props.reportSummary.intervals || []; const intervals = props.reportSummary.intervals || [];
intervals.sort((a,b) => { intervals.sort((a, b) => {
const as = parseInterval(a) const as = parseInterval(a)
const bs = parseInterval(b) const bs = parseInterval(b)
if (as < bs) { if (as < bs) {
@ -403,7 +429,6 @@ const TradingViewChart = (props: TradingViewChartProps) => {
const [currentInterval, setCurrentInterval] = useState(intervals.length > 0 ? intervals[intervals.length - 1] : '1m'); const [currentInterval, setCurrentInterval] = useState(intervals.length > 0 ? intervals[intervals.length - 1] : '1m');
const [showPositionBase, setShowPositionBase] = useState(false); const [showPositionBase, setShowPositionBase] = useState(false);
const [showCanceledOrders, setShowCanceledOrders] = useState(false);
const [showPositionAverageCost, setShowPositionAverageCost] = useState(false); const [showPositionAverageCost, setShowPositionAverageCost] = useState(false);
const [orders, setOrders] = useState<Order[]>([]); const [orders, setOrders] = useState<Order[]>([]);
@ -412,7 +437,6 @@ const TradingViewChart = (props: TradingViewChartProps) => {
new Date(props.reportSummary.endTime), new Date(props.reportSummary.endTime),
] ]
const [selectedTimeRange, setSelectedTimeRange] = useState(reportTimeRange) const [selectedTimeRange, setSelectedTimeRange] = useState(reportTimeRange)
const [timeRange, setTimeRange] = useState(reportTimeRange);
useEffect(() => { useEffect(() => {
if (!chartContainerRef.current || chartContainerRef.current.children.length > 0) { if (!chartContainerRef.current || chartContainerRef.current.children.length > 0) {
@ -423,7 +447,7 @@ const TradingViewChart = (props: TradingViewChartProps) => {
const fetchers = []; const fetchers = [];
const ordersFetcher = fetchOrders(props.basePath, props.runID).then((orders: Order[] | void) => { const ordersFetcher = fetchOrders(props.basePath, props.runID).then((orders: Order[] | void) => {
if (orders) { if (orders) {
const markers = ordersToMarkers(currentInterval, orders); const markers = ordersToMarkers(currentInterval, selectOrders(orders, selectedTimeRange[0], selectedTimeRange[1]));
chartData.orders = orders; chartData.orders = orders;
chartData.markers = markers; chartData.markers = markers;
setOrders(orders); setOrders(orders);
@ -436,7 +460,8 @@ const TradingViewChart = (props: TradingViewChartProps) => {
const manifest = props.reportSummary?.manifests[0]; const manifest = props.reportSummary?.manifests[0];
if (manifest && manifest.type === "strategyProperty" && manifest.strategyProperty === "position") { if (manifest && manifest.type === "strategyProperty" && manifest.strategyProperty === "position") {
const positionHistoryFetcher = fetchPositionHistory(props.basePath, props.runID, manifest.filename).then((data) => { 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); fetchers.push(positionHistoryFetcher);
} }
@ -594,7 +619,7 @@ const TradingViewChart = (props: TradingViewChartProps) => {
<TimeRangeSlider <TimeRangeSlider
selectedInterval={selectedTimeRange} selectedInterval={selectedTimeRange}
timelineInterval={timeRange} timelineInterval={reportTimeRange}
formatTick={(ms: Date) => format(new Date(ms), 'M d HH')} formatTick={(ms: Date) => format(new Date(ms), 'M d HH')}
step={1000 * parseInterval(currentInterval)} step={1000 * parseInterval(currentInterval)}
onChange={(tr: any) => { 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"); return moment(d).format("MMM Do YY hh:mm:ss A Z");
} }
const createOHLCLegendUpdater = (legend: HTMLDivElement, prefix: string) => { const createOHLCLegendUpdater = (legend: HTMLDivElement, prefix: string) => {
return (param: any, time : any) => { return (param: any, time: any) => {
if (param) { if (param) {
const change = Math.round((param.close - param.open) * 100.0) / 100.0 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; 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 # see here for more details
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
startTime: "2022-01-01" startTime: "2022-01-01"
endTime: "2022-06-18" endTime: "2022-06-30"
symbols: symbols:
- BTCUSDT - BTCUSDT
accounts: accounts:
@ -36,7 +36,12 @@ exchangeStrategies:
symbol: BTCUSDT symbol: BTCUSDT
# interval is how long do you want to update your order price and quantity # 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 is the leverage of the orders
leverage: 1.0 leverage: 1.0
@ -45,18 +50,31 @@ exchangeStrategies:
fastDEMAWindow: 144 fastDEMAWindow: 144
slowDEMAWindow: 169 slowDEMAWindow: 169
# Supertrend indicator parameters # Use linear regression as trend confirmation
superTrend: linearRegression:
# ATR window used by Supertrend interval: 5m
averageTrueRangeWindow: 39 window: 80
# ATR Multiplier for calculating super trend prices, the higher, the stronger the trends are
averageTrueRangeMultiplier: 3
# TP according to ATR multiple, 0 to disable this # TP according to ATR multiple, 0 to disable this
takeProfitMultiplier: 3 TakeProfitAtrMultiplier: 0
# Set SL price to the low of the triggering Kline # Set SL price to the low of the triggering Kline
stopLossByTriggeringK: true stopLossByTriggeringK: false
# TP/SL by reversed signals # TP/SL by reversed supertrend signal
tpslBySignal: true 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 userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot)
* [bbgo version](bbgo_version.md) - show version name * [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 * [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 * [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 * [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 * [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 * [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 * [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 * [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 * [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 * [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 * [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 loans](bbgo_margin_loans.md) - query loans history
* [bbgo margin repays](bbgo_margin_repays.md) - query repay 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 * [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 * [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 * [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 * [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 --json print optimizer metrics in json format
--optimizer-config string config file (default "optimizer.yaml") --optimizer-config string config file (default "optimizer.yaml")
--output string backtest report output directory (default "output") --output string backtest report output directory (default "output")
--tsv print optimizer metrics in csv format
``` ```
### Options inherited from parent commands ### Options inherited from parent commands
@ -43,4 +44,4 @@ bbgo optimize [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [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 * [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 * [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 ### Options
``` ```
-h, --help help for pnl -h, --help help for pnl
--include-transfer convert transfer records into trades --include-transfer convert transfer records into trades
--limit int number of trades --limit uint number of trades
--session string target exchange --session stringArray target exchange sessions
--symbol string trading symbol --since string query trades from a time point
--symbol string trading symbol
--sync sync before loading trades
``` ```
### Options inherited from parent commands ### Options inherited from parent commands
@ -48,4 +50,4 @@ bbgo pnl [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [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 * [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 * [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 * [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 * [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 * [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 * [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 * [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 * [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. - The MA window of the ATR indicator used by Supertrend.
- `averageTrueRangeMultiplier` - `averageTrueRangeMultiplier`
- Multiplier for calculating upper and lower bond prices, the higher, the stronger the trends are, but also makes it less sensitive. - 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. - TP according to ATR multiple, 0 to disable this.
- `stopLossByTriggeringK` - `stopLossByTriggeringK`
- Set SL price to the low of the triggering Kline. - Set SL price to the low/high of the triggering Kline.
- `tpslBySignal` - `stopByReversedSupertrend`
- TP/SL by reversed signals. - 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 #### Examples

View File

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

View File

@ -20,10 +20,14 @@ type AverageCostPnlReport struct {
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
Market types.Market `json:"market"` Market types.Market `json:"market"`
NumTrades int `json:"numTrades"` NumTrades int `json:"numTrades"`
Profit fixedpoint.Value `json:"profit"` Profit fixedpoint.Value `json:"profit"`
NetProfit fixedpoint.Value `json:"netProfit"` UnrealizedProfit fixedpoint.Value `json:"unrealizedProfit"`
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"` AverageCost fixedpoint.Value `json:"averageCost"`
BuyVolume fixedpoint.Value `json:"buyVolume,omitempty"` BuyVolume fixedpoint.Value `json:"buyVolume,omitempty"`
SellVolume fixedpoint.Value `json:"sellVolume,omitempty"` SellVolume fixedpoint.Value `json:"sellVolume,omitempty"`

View File

@ -27,6 +27,7 @@ type StateRecorder struct {
outputDirectory string outputDirectory string
strategies []Instance strategies []Instance
writers map[types.CsvFormatter]*tsv.Writer writers map[types.CsvFormatter]*tsv.Writer
lastLines map[types.CsvFormatter][]string
manifests Manifests manifests Manifests
} }
@ -34,6 +35,7 @@ func NewStateRecorder(outputDir string) *StateRecorder {
return &StateRecorder{ return &StateRecorder{
outputDirectory: outputDir, outputDirectory: outputDir,
writers: make(map[types.CsvFormatter]*tsv.Writer), writers: make(map[types.CsvFormatter]*tsv.Writer),
lastLines: make(map[types.CsvFormatter][]string),
manifests: make(Manifests), manifests: make(Manifests),
} }
} }
@ -42,11 +44,18 @@ func (r *StateRecorder) Snapshot() (int, error) {
var c int var c int
for obj, writer := range r.writers { for obj, writer := range r.writers {
records := obj.CsvRecords() records := obj.CsvRecords()
lastLine, hasLastLine := r.lastLines[obj]
for _, record := range records { for _, record := range records {
if hasLastLine && equalStringSlice(lastLine, record) {
continue
}
if err := writer.Write(record); err != nil { if err := writer.Write(record); err != nil {
return c, err return c, err
} }
c++ c++
r.lastLines[obj] = record
} }
writer.Flush() writer.Flush()
@ -129,3 +138,19 @@ func (r *StateRecorder) Close() error {
return err 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"` InitialTotalBalances types.BalanceMap `json:"initialTotalBalances"`
FinalTotalBalances types.BalanceMap `json:"finalTotalBalances"` 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 is the profit aggregated from the symbol reports
TotalProfit fixedpoint.Value `json:"totalProfit,omitempty"` TotalProfit fixedpoint.Value `json:"totalProfit,omitempty"`
TotalUnrealizedProfit fixedpoint.Value `json:"totalUnrealizedProfit,omitempty"` TotalUnrealizedProfit fixedpoint.Value `json:"totalUnrealizedProfit,omitempty"`
TotalGrossProfit fixedpoint.Value `json:"totalGrossProfit,omitempty"`
TotalGrossLoss fixedpoint.Value `json:"totalGrossLoss,omitempty"`
SymbolReports []SessionSymbolReport `json:"symbolReports,omitempty"` SymbolReports []SessionSymbolReport `json:"symbolReports,omitempty"`
Manifests Manifests `json:"manifests,omitempty"` Manifests Manifests `json:"manifests,omitempty"`
@ -75,13 +81,21 @@ type SessionSymbolReport struct {
Manifests Manifests `json:"manifests,omitempty"` 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) { func (r *SessionSymbolReport) Print(wantBaseAssetBaseline bool) {
color.Green("%s %s PROFIT AND LOSS REPORT", r.Exchange, r.Symbol) color.Green("%s %s PROFIT AND LOSS REPORT", r.Exchange, r.Symbol)
color.Green("===============================================") color.Green("===============================================")
r.PnL.Print() r.PnL.Print()
initQuoteAsset := inQuoteAsset(r.InitialBalances, r.Market, r.StartPrice) initQuoteAsset := r.InitialEquityValue()
finalQuoteAsset := inQuoteAsset(r.FinalBalances, r.Market, r.LastPrice) 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("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) 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) return WriteReportIndex(outputDirectory, reportIndex)
} }
// inQuoteAsset converts all balances in quote asset // InQuoteAsset converts all balances in quote asset
func inQuoteAsset(balances types.BalanceMap, market types.Market, price fixedpoint.Value) fixedpoint.Value { func InQuoteAsset(balances types.BalanceMap, market types.Market, price fixedpoint.Value) fixedpoint.Value {
quote := balances[market.QuoteCurrency] quote := balances[market.QuoteCurrency]
base := balances[market.BaseCurrency] base := balances[market.BaseCurrency]
return base.Total().Mul(price).Add(quote.Total()) return base.Total().Mul(price).Add(quote.Total())

View File

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

View File

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

View File

@ -4,12 +4,16 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"strconv"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/optimizer" "github.com/c9s/bbgo/pkg/optimizer"
) )
@ -17,6 +21,7 @@ func init() {
optimizeCmd.Flags().String("optimizer-config", "optimizer.yaml", "config file") optimizeCmd.Flags().String("optimizer-config", "optimizer.yaml", "config file")
optimizeCmd.Flags().String("output", "output", "backtest report output directory") optimizeCmd.Flags().String("output", "output", "backtest report output directory")
optimizeCmd.Flags().Bool("json", false, "print optimizer metrics in json format") optimizeCmd.Flags().Bool("json", false, "print optimizer metrics in json format")
optimizeCmd.Flags().Bool("tsv", false, "print optimizer metrics in csv format")
RootCmd.AddCommand(optimizeCmd) RootCmd.AddCommand(optimizeCmd)
} }
@ -43,6 +48,11 @@ var optimizeCmd = &cobra.Command{
return err return err
} }
printTsvFormat, err := cmd.Flags().GetBool("tsv")
if err != nil {
return err
}
outputDirectory, err := cmd.Flags().GetString("output") outputDirectory, err := cmd.Flags().GetString("output")
if err != nil { if err != nil {
return err return err
@ -104,6 +114,10 @@ var optimizeCmd = &cobra.Command{
// print metrics JSON to stdout // print metrics JSON to stdout
fmt.Println(string(out)) fmt.Println(string(out))
} else if printTsvFormat {
if err := formatMetricsTsv(metrics, os.Stdout); err != nil {
return err
}
} else { } else {
for n, values := range metrics { for n, values := range metrics {
if len(values) == 0 { if len(values) == 0 {
@ -120,3 +134,95 @@ var optimizeCmd = &cobra.Command{
return nil return nil
}, },
} }
func transformMetricsToRows(metrics map[string][]optimizer.Metric) (headers []string, rows [][]interface{}) {
var metricsKeys []string
for k := range metrics {
metricsKeys = append(metricsKeys, k)
}
var numEntries int
var paramLabels []string
for _, ms := range metrics {
for _, m := range ms {
paramLabels = m.Labels
break
}
numEntries = len(ms)
break
}
headers = append(paramLabels, metricsKeys...)
rows = make([][]interface{}, numEntries)
var metricsRows = make([][]interface{}, numEntries)
// build params into the rows
for i, m := range metrics[metricsKeys[0]] {
rows[i] = m.Params
}
for _, metricKey := range metricsKeys {
for i, ms := range metrics[metricKey] {
if len(metricsRows[i]) == 0 {
metricsRows[i] = make([]interface{}, 0, len(metricsKeys))
}
metricsRows[i] = append(metricsRows[i], ms.Value)
}
}
// merge rows
for i := range rows {
rows[i] = append(rows[i], metricsRows[i]...)
}
return headers, rows
}
func formatMetricsTsv(metrics map[string][]optimizer.Metric, writer io.WriteCloser) error {
headers, rows := transformMetricsToRows(metrics)
w := tsv.NewWriter(writer)
if err := w.Write(headers); err != nil {
return err
}
for _, row := range rows {
var cells []string
for _, o := range row {
cell, err := castCellValue(o)
if err != nil {
return err
}
cells = append(cells, cell)
}
if err := w.Write(cells); err != nil {
return err
}
}
return w.Close()
}
func castCellValue(a interface{}) (string, error) {
switch tv := a.(type) {
case fixedpoint.Value:
return tv.String(), nil
case float64:
return strconv.FormatFloat(tv, 'f', -1, 64), nil
case int64:
return strconv.FormatInt(tv, 10), nil
case int32:
return strconv.FormatInt(int64(tv), 10), nil
case int:
return strconv.Itoa(tv), nil
case bool:
return strconv.FormatBool(tv), nil
case string:
return tv, nil
case []byte:
return string(tv), nil
default:
return "", fmt.Errorf("unsupported object type: %T value: %v", tv, tv)
}
}

View File

@ -608,7 +608,7 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap,
Available: b.Balance, Available: b.Balance,
Locked: b.Locked, Locked: b.Locked,
NetAsset: b.Balance.Add(b.Locked).Sub(b.Debt), 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, Interest: b.Interest,
} }
} }

View File

@ -22,8 +22,10 @@ type Account struct {
Locked fixedpoint.Value `json:"locked"` Locked fixedpoint.Value `json:"locked"`
// v3 fields for M wallet // v3 fields for M wallet
Debt fixedpoint.Value `json:"debt"` Debt fixedpoint.Value `json:"debt"`
Interest fixedpoint.Value `json:"interest"` Principal fixedpoint.Value `json:"principal"`
Borrowed fixedpoint.Value `json:"borrowed"`
Interest fixedpoint.Value `json:"interest"`
// v2 fields // v2 fields
FiatCurrency string `json:"fiat_currency"` 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 PostRequest requestgen -method POST
//go:generate -command DeleteRequest requestgen -method DELETE //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 { type CreateWalletOrderRequest struct {
client requestgen.AuthenticatedAPIClient 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 package v3
@ -244,7 +244,7 @@ func (c *CreateWalletOrderRequest) Do(ctx context.Context) (*max.Order, error) {
} }
query := url.Values{} query := url.Values{}
apiURL := "/api/v3/wallet/:walletType/orders" apiURL := "/api/v3/wallet/:walletType/order"
slugs, err := c.GetSlugsMap() slugs, err := c.GetSlugsMap()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -4,12 +4,12 @@ package indicator
import () import ()
func (A *ATR) OnUpdate(cb func(value float64)) { func (inc *ATR) OnUpdate(cb func(value float64)) {
A.UpdateCallbacks = append(A.UpdateCallbacks, cb) inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb)
} }
func (A *ATR) EmitUpdate(value float64) { func (inc *ATR) EmitUpdate(value float64) {
for _, cb := range A.UpdateCallbacks { for _, cb := range inc.UpdateCallbacks {
cb(value) 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) { func TestMergeMigrationsMap(t *testing.T) {
MergeMigrationsMap(map[int64]*rockhopper.Migration{ MergeMigrationsMap(map[int64]*rockhopper.Migration{
2: {}, 2: &rockhopper.Migration{},
3: {}, 3: &rockhopper.Migration{},
}) })
} }

View File

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

View File

@ -17,16 +17,31 @@ import (
type MetricValueFunc func(summaryReport *backtest.SummaryReport) fixedpoint.Value type MetricValueFunc func(summaryReport *backtest.SummaryReport) fixedpoint.Value
var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fixedpoint.Value {
if summaryReport == nil {
return fixedpoint.Zero
}
return summaryReport.TotalProfit return summaryReport.TotalProfit
} }
var TotalVolume = func(summaryReport *backtest.SummaryReport) fixedpoint.Value {
if len(summaryReport.SymbolReports) == 0 {
return fixedpoint.Zero
}
buyVolume := summaryReport.SymbolReports[0].PnL.BuyVolume
sellVolume := summaryReport.SymbolReports[0].PnL.SellVolume
return buyVolume.Add(sellVolume)
}
type Metric struct { type Metric struct {
Labels []string `json:"labels,omitempty"` // Labels is the labels of the given parameters
Params []interface{} `json:"params,omitempty"` Labels []string `json:"labels,omitempty"`
Value fixedpoint.Value `json:"value,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{} { func copyParams(params []interface{}) []interface{} {
@ -172,6 +187,7 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][]
var valueFunctions = map[string]MetricValueFunc{ var valueFunctions = map[string]MetricValueFunc{
"totalProfit": TotalProfitMetricValueFunc, "totalProfit": TotalProfitMetricValueFunc,
"totalVolume": TotalVolume,
} }
var metrics = map[string][]Metric{} var metrics = map[string][]Metric{}
@ -220,16 +236,20 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][]
close(taskC) // this will shut down the executor close(taskC) // this will shut down the executor
for result := range resultsC { for result := range resultsC {
for metricName, metricFunc := range valueFunctions { if result.Report == nil {
if result.Report == nil { log.Errorf("no summaryReport found for params: %+v", result.Params)
log.Errorf("no summaryReport found for params: %+v", result.Params) continue
} }
for metricKey, metricFunc := range valueFunctions {
var metricValue = metricFunc(result.Report) var metricValue = metricFunc(result.Report)
bar.Set("log", fmt.Sprintf("params: %+v => %s %+v", result.Params, metricName, metricValue)) bar.Set("log", fmt.Sprintf("params: %+v => %s %+v", result.Params, metricKey, metricValue))
bar.Increment() bar.Increment()
metrics[metricName] = append(metrics[metricName], Metric{
metrics[metricKey] = append(metrics[metricKey], Metric{
Params: result.Params, Params: result.Params,
Labels: result.Labels, Labels: result.Labels,
Key: metricKey,
Value: metricValue, Value: metricValue,
}) })
} }

View File

@ -123,7 +123,8 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) {
minMarginLevel := s.MinMarginLevel minMarginLevel := s.MinMarginLevel
curMarginLevel := account.MarginLevel 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.MarginLevel.String(),
account.MarginRatio.String(), account.MarginRatio.String(),
account.MarginTolerance.String(), account.MarginTolerance.String(),
@ -280,7 +281,8 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE
return return
} }
if s.ExchangeSession.GetAccount().MarginLevel.Compare(s.MinMarginLevel) > 0 { account := s.ExchangeSession.GetAccount()
if account.MarginLevel.Compare(s.MinMarginLevel) > 0 {
return return
} }
@ -291,7 +293,6 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE
return return
} }
account := s.ExchangeSession.GetAccount()
minMarginLevel := s.MinMarginLevel minMarginLevel := s.MinMarginLevel
curMarginLevel := account.MarginLevel curMarginLevel := account.MarginLevel
@ -300,7 +301,11 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE
return return
} }
toRepay := b.Available toRepay := fixedpoint.Min(b.Borrowed, b.Available)
if toRepay.IsZero() {
return
}
bbgo.Notify(&MarginAction{ bbgo.Notify(&MarginAction{
Exchange: s.ExchangeSession.ExchangeName, Exchange: s.ExchangeSession.ExchangeName,
Action: "Repay", Action: "Repay",
@ -309,6 +314,7 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE
MarginLevel: curMarginLevel, MarginLevel: curMarginLevel,
MinMarginLevel: minMarginLevel, MinMarginLevel: minMarginLevel,
}) })
if err := s.marginBorrowRepay.RepayMarginAsset(context.Background(), event.Asset, toRepay); err != nil { if err := s.marginBorrowRepay.RepayMarginAsset(context.Background(), event.Asset, toRepay); err != nil {
log.WithError(err).Errorf("margin repay error") 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 // 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 { func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
if s.MinMarginLevel.IsZero() { 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 s.ExchangeSession = session
marginBorrowRepay, ok := session.Exchange.(types.MarginBorrowRepayService) marginBorrowRepay, ok := session.Exchange.(types.MarginBorrowRepayService)
if !ok { 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 s.marginBorrowRepay = marginBorrowRepay

View File

@ -40,6 +40,7 @@ type BreakLow struct {
func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) { 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: s.Interval})
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m})
} }
func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { 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 { 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 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 // use the last kline from the history before we get the next closed kline
if lastKLine != nil { if lastKLine != nil {
s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) s.updateResistanceOrders(lastKLine.Close)
} }
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { 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 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:] 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() minDistance := s.MinDistance.Float64()
groupDistance := s.GroupDistance.Float64() groupDistance := s.GroupDistance.Float64()
resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, tail(s.resistancePivot.Lows, 6)) resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, tail(s.resistancePivot.Lows, 6))
if len(resistancePrices) == 0 { if len(resistancePrices) == 0 {
return false return false
} }
@ -88,9 +91,6 @@ func (s *ResistanceShort) updateNextResistancePrice(closePrice fixedpoint.Value)
nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0]) 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() { if s.currentResistancePrice.IsZero() {
s.currentResistancePrice = nextResistancePrice s.currentResistancePrice = nextResistancePrice
return true return true
@ -99,9 +99,8 @@ func (s *ResistanceShort) updateNextResistancePrice(closePrice fixedpoint.Value)
// if the current sell price is out-dated // if the current sell price is out-dated
// or // or
// the next resistance is lower than the current one. // the next resistance is lower than the current one.
currentSellPrice := s.currentResistancePrice.Mul(one.Add(s.Ratio)) minPriceToUpdate := s.currentResistancePrice.Mul(one.Add(s.MinDistance))
if closePrice.Compare(currentSellPrice) > 0 || if closePrice.Compare(minPriceToUpdate) > 0 || nextResistancePrice.Compare(s.currentResistancePrice) < 0 {
nextResistancePrice.Compare(currentSellPrice) < 0 {
s.currentResistancePrice = nextResistancePrice s.currentResistancePrice = nextResistancePrice
return true return true
} }
@ -109,11 +108,11 @@ func (s *ResistanceShort) updateNextResistancePrice(closePrice fixedpoint.Value)
return false return false
} }
func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { func (s *ResistanceShort) updateResistanceOrders(closePrice fixedpoint.Value) {
ctx := context.Background() ctx := context.Background()
resistanceUpdated := s.updateNextResistancePrice(closePrice) resistanceUpdated := s.updateCurrentResistancePrice(closePrice)
if resistanceUpdated { 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) s.placeResistanceOrders(ctx, s.currentResistancePrice)
} }
} }
@ -151,8 +150,7 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP
spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i)))
price := sellPriceStart.Mul(one.Add(spread)) 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()) log.Infof("placing resistance short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64())
orderForms = append(orderForms, types.SubmitOrder{ 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 ( import (
"context" "context"
"fmt" "fmt"
"os"
"sync" "sync"
"github.com/c9s/bbgo/pkg/util"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -18,10 +17,9 @@ import (
const ID = "supertrend" const ID = "supertrend"
const stateKey = "state-v1"
var log = logrus.WithField("strategy", ID) var log = logrus.WithField("strategy", ID)
// TODO: limit order for ATR TP
func init() { func init() {
// Register the pointer of the strategy struct, // Register the pointer of the strategy struct,
// so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) // 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 { type Strategy struct {
*bbgo.Persistence
Environment *bbgo.Environment Environment *bbgo.Environment
session *bbgo.ExchangeSession
Market types.Market Market types.Market
// persistence fields // persistence fields
Position *types.Position `json:"position,omitempty" persistence:"position"` Position *types.Position `persistence:"position"`
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` ProfitStats *types.ProfitStats `persistence:"profit_stats"`
TradeStats *types.TradeStats `persistence:"trade_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{}
// Symbol is the market symbol you want to trade // Symbol is the market symbol you want to trade
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
// Interval is how long do you want to update your order price and quantity types.IntervalWindow
Interval types.Interval `json:"interval"`
// Double DEMA
doubleDema *DoubleDema
// FastDEMAWindow DEMA window for checking breakout // FastDEMAWindow DEMA window for checking breakout
FastDEMAWindow int `json:"fastDEMAWindow"` FastDEMAWindow int `json:"fastDEMAWindow"`
// SlowDEMAWindow DEMA window for checking breakout // SlowDEMAWindow DEMA window for checking breakout
SlowDEMAWindow int `json:"slowDEMAWindow"` SlowDEMAWindow int `json:"slowDEMAWindow"`
fastDEMA *indicator.DEMA
slowDEMA *indicator.DEMA
// SuperTrend indicator // SuperTrend indicator
// SuperTrend SuperTrend `json:"superTrend"`
Supertrend *indicator.Supertrend Supertrend *indicator.Supertrend
// SupertrendWindow ATR window for calculation of supertrend
SupertrendWindow int `json:"supertrendWindow"`
// SupertrendMultiplier ATR multiplier for calculation of supertrend // SupertrendMultiplier ATR multiplier for calculation of supertrend
SupertrendMultiplier float64 `json:"supertrendMultiplier"` SupertrendMultiplier float64 `json:"supertrendMultiplier"`
// LinearRegression Use linear regression as trend confirmation
LinearRegression *LinGre `json:"linearRegression,omitempty"`
// Leverage // Leverage
Leverage float64 `json:"leverage"` Leverage float64 `json:"leverage"`
// TakeProfitMultiplier TP according to ATR multiple, 0 to disable this // TakeProfitAtrMultiplier TP according to ATR multiple, 0 to disable this
TakeProfitMultiplier float64 `json:"takeProfitMultiplier"` 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"` StopLossByTriggeringK bool `json:"stopLossByTriggeringK"`
// TPSLBySignal TP/SL by reversed signals // StopByReversedSupertrend TP/SL by reversed supertrend signal
TPSLBySignal bool `json:"tpslBySignal"` 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 currentTakeProfitPrice fixedpoint.Value
currentStopLossPrice fixedpoint.Value currentStopLossPrice fixedpoint.Value
@ -105,7 +103,7 @@ func (s *Strategy) Validate() error {
return errors.New("interval is required") return errors.New("interval is required")
} }
if s.Leverage == 0.0 { if s.Leverage <= 0.0 {
return errors.New("leverage is required") return errors.New("leverage is required")
} }
@ -114,6 +112,7 @@ func (s *Strategy) Validate() error {
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LinearRegression.Interval})
} }
// Position control // Position control
@ -141,8 +140,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
orderForm := s.generateOrderForm(side, quantity, types.SideEffectTypeAutoRepay) 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, orderForm)
bbgo.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage)
_, err := s.orderExecutor.SubmitOrders(ctx, orderForm) _, err := s.orderExecutor.SubmitOrders(ctx, orderForm)
if err != nil { if err != nil {
@ -153,43 +151,88 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
return err 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 // setupIndicators initializes indicators
func (s *Strategy) setupIndicators() { func (s *Strategy) setupIndicators() {
if s.FastDEMAWindow == 0 { // K-line store for indicators
s.FastDEMAWindow = 144 kLineStore, _ := s.session.MarketDataStore(s.Symbol)
}
s.fastDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.FastDEMAWindow}}
if s.SlowDEMAWindow == 0 { // Double DEMA
s.SlowDEMAWindow = 169 s.doubleDema = newDoubleDema(kLineStore, s.Interval, s.FastDEMAWindow, s.SlowDEMAWindow)
}
s.slowDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.SlowDEMAWindow}}
if s.SupertrendWindow == 0 { // Supertrend
s.SupertrendWindow = 39 if s.Window == 0 {
s.Window = 39
} }
if s.SupertrendMultiplier == 0 { if s.SupertrendMultiplier == 0 {
s.SupertrendMultiplier = 3 s.SupertrendMultiplier = 3
} }
s.Supertrend = &indicator.Supertrend{IntervalWindow: types.IntervalWindow{Window: s.SupertrendWindow, Interval: s.Interval}, ATRMultiplier: s.SupertrendMultiplier} 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.SupertrendWindow, Interval: s.Interval}} 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) shouldStop(kline types.KLine, stSignal types.Direction, demaSignal types.Direction, lgSignal types.Direction) bool {
func (s *Strategy) updateIndicators(kline types.KLine) { stopNow := false
closePrice := kline.GetClose().Float64() base := s.Position.GetBase()
baseSign := base.Sign()
// Update indicators if s.StopLossByTriggeringK && !s.currentStopLossPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentStopLossPrice) > 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentStopLossPrice) < 0)) {
if kline.Interval == s.fastDEMA.Interval { // SL by triggering Kline low/high
s.fastDEMA.Update(closePrice) 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) return stopNow
} }
if kline.Interval == s.Supertrend.Interval {
s.Supertrend.Update(kline.GetHigh().Float64(), kline.GetLow().Float64(), closePrice) 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 { 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, Type: types.OrderTypeMarket,
Quantity: quantity, Quantity: quantity,
MarginSideEffect: marginOrderSideEffect, MarginSideEffect: marginOrderSideEffect,
GroupID: s.groupID,
} }
return orderForm 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 { func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
s.session = session s.session = session
s.currentStopLossPrice = fixedpoint.Zero
s.currentTakeProfitPrice = fixedpoint.Zero
// calculate group id for orders // calculate group id for orders
instanceID := s.InstanceID() instanceID := s.InstanceID()
s.groupID = util.FNV32(instanceID)
// If position is nil, we need to allocate a new position for calculation // If position is nil, we need to allocate a new position for calculation
if s.Position == nil { if s.Position == nil {
@ -235,6 +279,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.Position.Strategy = ID s.Position.Strategy = ID
s.Position.StrategyInstanceID = s.InstanceID() 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 // Set fee rate
if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 {
s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{
@ -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 // Setup order executor
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
s.orderExecutor.BindEnvironment(s.Environment) s.orderExecutor.BindEnvironment(s.Environment)
s.orderExecutor.BindProfitStats(s.ProfitStats) s.orderExecutor.BindProfitStats(s.ProfitStats)
s.orderExecutor.BindTradeStats(s.TradeStats)
s.orderExecutor.Bind() s.orderExecutor.Bind()
// Sync position to redis on trade // Sync position to redis on trade
@ -259,16 +308,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
bbgo.Sync(s) bbgo.Sync(s)
}) })
s.stopC = make(chan struct{})
// StrategyController // StrategyController
s.Status = types.StrategyStatusRunning s.Status = types.StrategyStatusRunning
s.OnSuspend(func() { s.OnSuspend(func() {
_ = s.orderExecutor.GracefulCancel(ctx) _ = s.orderExecutor.GracefulCancel(ctx)
_ = s.Persistence.Sync(s) bbgo.Sync(s)
}) })
s.OnEmergencyStop(func() { s.OnEmergencyStop(func() {
_ = s.orderExecutor.GracefulCancel(ctx) _ = s.orderExecutor.GracefulCancel(ctx)
// Close 100% position // Close 100% position
@ -278,108 +323,77 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
// Setup indicators // Setup indicators
s.setupIndicators() s.setupIndicators()
s.currentStopLossPrice = fixedpoint.Zero // Exit methods
s.currentTakeProfitPrice = fixedpoint.Zero 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 // StrategyController
if s.Status != types.StrategyStatusRunning { if s.Status != types.StrategyStatusRunning {
return return
} }
// skip k-lines from other symbols or other intervals closePrice := kline.GetClose()
if kline.Symbol != s.Symbol || kline.Interval != s.Interval { openPrice := kline.GetOpen()
return closePrice64 := closePrice.Float64()
} openPrice64 := openPrice.Float64()
// Update indicators // Supertrend signal
s.updateIndicators(kline)
// Get signals
closePrice := kline.GetClose().Float64()
openPrice := kline.GetOpen().Float64()
stSignal := s.Supertrend.GetSignal() 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()) { // DEMA signal
demaSignal = types.DirectionUp demaSignal := s.doubleDema.getDemaSignal(openPrice64, closePrice64)
} else if closePrice < s.fastDEMA.Last() && closePrice < s.slowDEMA.Last() && !(openPrice < s.fastDEMA.Last() && openPrice < s.slowDEMA.Last()) {
demaSignal = types.DirectionDown // Linear Regression signal
} else { var lgSignal types.Direction
demaSignal = types.DirectionNone if s.LinearRegression != nil {
lgSignal = s.LinearRegression.GetSignal()
} }
base := s.Position.GetBase() // TP/SL if there's non-dust position and meets the criteria
baseSign := base.Sign() 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 // Get order side
if !s.Market.IsDustQuantity(base.Abs(), kline.GetClose()) { side := s.getSide(stSignal, demaSignal, lgSignal)
if s.StopLossByTriggeringK && !s.currentStopLossPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentStopLossPrice) > 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentStopLossPrice) < 0)) { // Set TP/SL price if needed
// SL by triggering Kline low if side == types.SideTypeBuy {
log.Infof("%s SL by triggering Kline low", s.Symbol) if s.StopLossByTriggeringK {
bbgo.Notify("%s StopLoss by triggering the kline low", s.Symbol) s.currentStopLossPrice = kline.GetLow()
if err := s.ClosePosition(ctx, fixedpoint.One); err == nil { }
s.currentStopLossPrice = fixedpoint.Zero if s.TakeProfitAtrMultiplier > 0 {
s.currentTakeProfitPrice = fixedpoint.Zero s.currentTakeProfitPrice = closePrice.Add(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier))
} }
} else if s.TakeProfitMultiplier > 0 && !s.currentTakeProfitPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) < 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) > 0)) { } else if side == types.SideTypeSell {
// TP by multiple of ATR if s.StopLossByTriggeringK {
log.Infof("%s TP by multiple of ATR", s.Symbol) s.currentStopLossPrice = kline.GetHigh()
bbgo.Notify("%s TakeProfit by multiple of ATR", s.Symbol) }
if err := s.ClosePosition(ctx, fixedpoint.One); err == nil { if s.TakeProfitAtrMultiplier > 0 {
s.currentStopLossPrice = fixedpoint.Zero s.currentTakeProfitPrice = closePrice.Sub(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier))
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
}
}
} }
} }
// Open position // 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 // 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 { 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) bbgo.Notify("open %s position for signal %v", s.Symbol, side)
// Close opposite position if any // 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()) { 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) bbgo.Notify("close existing %s position before open a new position", s.Symbol)
_ = s.ClosePosition(ctx, fixedpoint.One) _ = s.ClosePosition(ctx, fixedpoint.One)
} else { } 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) bbgo.Notify("existing %s position has the same direction with the signal", s.Symbol)
return 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) log.Infof("submit open position order %v", orderForm)
_, err := s.orderExecutor.SubmitOrders(ctx, orderForm) _, err := s.orderExecutor.SubmitOrders(ctx, orderForm)
if err != nil { 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) bbgo.Notify("can not place %s open position order", s.Symbol)
} }
} }
}) }))
// Graceful shutdown // Graceful shutdown
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
close(s.stopC)
_ = s.orderExecutor.GracefulCancel(ctx) _ = s.orderExecutor.GracefulCancel(ctx)
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
}) })
return nil return nil

View File

@ -94,6 +94,10 @@ type SeriesExtend interface {
Covariance(b Series, length int) float64 Covariance(b Series, length int) float64
Correlation(b Series, length int, method ...CorrFunc) float64 Correlation(b Series, length int, method ...CorrFunc) float64
Rank(length int) SeriesExtend Rank(length int) SeriesExtend
Sigmoid() SeriesExtend
Softmax(window int) SeriesExtend
Entropy(window int) float64
CrossEntropy(b Series, window int) float64
} }
type SeriesBase struct { 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]) // if limit is given, will only calculate the first limit numbers (a.Index[0..limit])
// otherwise will operate on all elements // otherwise will operate on all elements
func Dot(a interface{}, b interface{}, limit ...int) float64 { 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) // 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} 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 // TODO: ta.linreg

View File

@ -2,6 +2,7 @@ package types
import ( import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gonum.org/v1/gonum/stat"
"testing" "testing"
) )
@ -84,3 +85,62 @@ func TestSkew(t *testing.T) {
sk := Skew(&a, 4) sk := Skew(&a, 4)
assert.InDelta(t, sk, 1.129338, 0.001) 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 { func KLineWith(symbol string, interval Interval, callback KLineCallBack) KLineCallBack {
return func(k KLine) { return func(k KLine) {
if k.Symbol != symbol || k.Interval != interval { if k.Symbol != symbol || (k.Interval != "" && k.Interval != interval) {
return return
} }
callback(k) 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 { func (s *SeriesBase) Rank(length int) SeriesExtend {
return Rank(s, length) 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 ( import (
"reflect" "reflect"
"time"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
) )
@ -11,7 +12,7 @@ import (
func FilterSimpleArgs(args []interface{}) (simpleArgs []interface{}) { func FilterSimpleArgs(args []interface{}) (simpleArgs []interface{}) {
for _, arg := range args { for _, arg := range args {
switch arg.(type) { switch arg.(type) {
case int, int64, int32, uint64, uint32, string, []byte, float64, float32, fixedpoint.Value: case int, int64, int32, uint64, uint32, string, []byte, float64, float32, fixedpoint.Value, time.Time:
simpleArgs = append(simpleArgs, arg) simpleArgs = append(simpleArgs, arg)
default: default:
rt := reflect.TypeOf(arg) rt := reflect.TypeOf(arg)

View File

@ -1,8 +1,8 @@
//go:build !release
// +build !release // +build !release
package version 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 // +build release
package version package version
const Version = "v1.35.0-daaa3352" const Version = "v1.36.0-cc8821bb"
const VersionGitRef = "cc8821bb"
const VersionGitRef = "daaa3352"