first commit
This commit is contained in:
parent
ba6efd2e4c
commit
ee09262bf2
7
cmd/qbtrade/main.go
Normal file
7
cmd/qbtrade/main.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "git.qtrade.icu/lychiyu/qbtrade/pkg/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
40
config/atrpin.yaml
Normal file
40
config/atrpin.yaml
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: &exchange max
|
||||||
|
envVarPrefix: max
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: *exchange
|
||||||
|
atrpin:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
interval: 5m
|
||||||
|
window: 14
|
||||||
|
multiplier: 100.0
|
||||||
|
minPriceRange: 20%
|
||||||
|
amount: 100
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
startTime: "2018-10-01"
|
||||||
|
endTime: "2018-11-01"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions:
|
||||||
|
- *exchange
|
||||||
|
# syncSecKLines: true
|
||||||
|
accounts:
|
||||||
|
max:
|
||||||
|
makerFeeRate: 0.0%
|
||||||
|
takerFeeRate: 0.075%
|
||||||
|
balances:
|
||||||
|
BTC: 1.0
|
||||||
|
USDT: 10_000.0
|
||||||
|
|
||||||
|
|
21
config/audacitymaker.yaml
Normal file
21
config/audacitymaker.yaml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
# futures: true
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
audacitymaker:
|
||||||
|
symbol: ETHBUSD
|
||||||
|
orderFlow:
|
||||||
|
interval: 1m
|
||||||
|
quantity: 0.01
|
38
config/autoborrow.yaml
Normal file
38
config/autoborrow.yaml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
autoborrow:
|
||||||
|
interval: 30m
|
||||||
|
autoRepayWhenDeposit: true
|
||||||
|
|
||||||
|
# minMarginRatio for triggering auto borrow
|
||||||
|
# we trigger auto borrow only when the margin ratio is above the number
|
||||||
|
minMarginLevel: 3.0
|
||||||
|
|
||||||
|
# maxMarginRatio for stop auto-repay
|
||||||
|
# if the margin ratio is high enough, we don't have the urge to repay
|
||||||
|
maxMarginLevel: 20.0
|
||||||
|
|
||||||
|
marginRepayAlert:
|
||||||
|
slackMentions:
|
||||||
|
- '<@USER_ID>'
|
||||||
|
- '<!subteam^TEAM_ID>'
|
||||||
|
|
||||||
|
marginLevelAlert:
|
||||||
|
interval: 5m
|
||||||
|
minMargin: 2.0
|
||||||
|
slackMentions:
|
||||||
|
- '<@USER_ID>'
|
||||||
|
- '<!subteam^TEAM_ID>'
|
||||||
|
|
||||||
|
assets:
|
||||||
|
- asset: ETH
|
||||||
|
low: 3.0
|
||||||
|
maxQuantityPerBorrow: 1.0
|
||||||
|
maxTotalBorrow: 10.0
|
||||||
|
|
||||||
|
- asset: USDT
|
||||||
|
low: 1000.0
|
||||||
|
maxQuantityPerBorrow: 100.0
|
||||||
|
maxTotalBorrow: 10.0
|
||||||
|
|
22
config/autobuy.yaml
Normal file
22
config/autobuy.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
# automaticaly buy coins when the balance is lower than the threshold
|
||||||
|
autobuy:
|
||||||
|
symbol: MAXTWD
|
||||||
|
schedule: "@every 1s"
|
||||||
|
threshold: 200
|
||||||
|
# price type: LAST, BUY, SELL, MID, TAKER, MAKER
|
||||||
|
priceType: BUY
|
||||||
|
|
||||||
|
# order quantity or amount
|
||||||
|
# quantity: 100
|
||||||
|
amount: 800
|
||||||
|
|
||||||
|
# skip if the price is higher than the upper band
|
||||||
|
bollinger:
|
||||||
|
interval: 1m
|
||||||
|
window: 21
|
||||||
|
bandWidth: 2.0
|
||||||
|
|
||||||
|
dryRun: true
|
23
config/backtest.yaml
Normal file
23
config/backtest.yaml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
backtest:
|
||||||
|
startTime: "2022-01-01"
|
||||||
|
endTime: "2022-01-02"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
- ftx
|
||||||
|
- max
|
||||||
|
- kucoin
|
||||||
|
- okex
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
grid:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
quantity: 0.001
|
||||||
|
gridNumber: 100
|
||||||
|
profitSpread: 1000.0 # The profit price spread that you want to add to your sell order when your buy order is executed
|
||||||
|
upperPrice: 40_000.0
|
||||||
|
lowerPrice: 20_000.0
|
||||||
|
|
27
config/binance-margin.yaml
Normal file
27
config/binance-margin.yaml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
# cross margin
|
||||||
|
binance_margin:
|
||||||
|
exchange: binance
|
||||||
|
margin: true
|
||||||
|
|
||||||
|
# isolated margin
|
||||||
|
binance_margin_linkusdt:
|
||||||
|
exchange: binance
|
||||||
|
margin: true
|
||||||
|
isolatedMargin: true
|
||||||
|
isolatedMarginSymbol: LINKUSDT
|
||||||
|
|
||||||
|
binance_margin_dotusdt:
|
||||||
|
exchange: binance
|
||||||
|
margin: true
|
||||||
|
isolatedMargin: true
|
||||||
|
isolatedMarginSymbol: DOTUSDT
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance_margin_linkusdt
|
||||||
|
dummy:
|
||||||
|
symbol: LINKUSDT
|
||||||
|
interval: 1m
|
||||||
|
|
60
config/bollgrid.yaml
Normal file
60
config/bollgrid.yaml
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
# binance:
|
||||||
|
# exchange: binance
|
||||||
|
# envVarPrefix: binance
|
||||||
|
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: MAX
|
||||||
|
|
||||||
|
riskControls:
|
||||||
|
# This is the session-based risk controller, which let you configure different risk controller by session.
|
||||||
|
sessionBased:
|
||||||
|
# "max" is the session name that you want to configure the risk control
|
||||||
|
max:
|
||||||
|
# orderExecutor is one of the risk control
|
||||||
|
orderExecutor:
|
||||||
|
# symbol-routed order executor
|
||||||
|
bySymbol:
|
||||||
|
BTCUSDT:
|
||||||
|
# basic risk control order executor
|
||||||
|
basic:
|
||||||
|
minQuoteBalance: 1000.0
|
||||||
|
maxBaseAssetBalance: 0
|
||||||
|
minBaseAssetBalance: 1.0
|
||||||
|
maxOrderAmount: 3000.0
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
# for testing max draw down (MDD) at 03-12
|
||||||
|
# see here for more details
|
||||||
|
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
|
||||||
|
startTime: "2020-09-04"
|
||||||
|
endTime: "2020-09-14"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
account:
|
||||||
|
max:
|
||||||
|
makerFeeRate: 0.075%
|
||||||
|
takerFeeRate: 0.075%
|
||||||
|
balances:
|
||||||
|
BTC: 0.0
|
||||||
|
USDT: 10000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
bollgrid:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
interval: 5m
|
||||||
|
gridNumber: 2
|
||||||
|
quantity: 0.001
|
||||||
|
profitSpread: 100.0
|
232
config/bollmaker.yaml
Normal file
232
config/bollmaker.yaml
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
---
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: BINANCE
|
||||||
|
|
||||||
|
# example command:
|
||||||
|
# godotenv -f .env.local -- go run ./cmd/qbtrade backtest --sync-from 2020-11-01 --config config/grid.yaml --base-asset-baseline
|
||||||
|
backtest:
|
||||||
|
# for testing max draw down (MDD) at 03-12
|
||||||
|
# see here for more details
|
||||||
|
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
|
||||||
|
startTime: "2022-05-01"
|
||||||
|
endTime: "2023-11-01"
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
symbols:
|
||||||
|
- ETHUSDT
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
ETH: 0.0
|
||||||
|
USDT: 10_000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
bollmaker:
|
||||||
|
symbol: ETHUSDT
|
||||||
|
|
||||||
|
# interval is how long do you want to update your order price and quantity
|
||||||
|
interval: 1m
|
||||||
|
|
||||||
|
# quantity is the base order quantity for your buy/sell order.
|
||||||
|
quantity: 0.05
|
||||||
|
|
||||||
|
# amount is used for fixed-amount order, for example, use fixed 20 USDT order for BTCUSDT market
|
||||||
|
# amount: 20
|
||||||
|
|
||||||
|
# useTickerPrice use the ticker api to get the mid price instead of the closed kline price.
|
||||||
|
# The back-test engine is kline-based, so the ticker price api is not supported.
|
||||||
|
# Turn this on if you want to do real trading.
|
||||||
|
useTickerPrice: true
|
||||||
|
|
||||||
|
# spread is the price spread from the middle price.
|
||||||
|
# For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread))
|
||||||
|
# For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread))
|
||||||
|
# Spread can be set by percentage or floating number. e.g., 0.1% or 0.001
|
||||||
|
spread: 0.1%
|
||||||
|
|
||||||
|
# minProfitSpread is the minimal order price spread from the current average cost.
|
||||||
|
# For long position, you will only place sell order above the price (= average cost * (1 + minProfitSpread))
|
||||||
|
# For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread))
|
||||||
|
minProfitSpread: 0.1%
|
||||||
|
|
||||||
|
# minProfitActivationRate activates MinProfitSpread when position RoI higher than the specified percentage
|
||||||
|
minProfitActivationRate: -10%
|
||||||
|
|
||||||
|
# trendEMA detects the trend by a given EMA
|
||||||
|
# when EMA goes up (the last > the previous), allow buy and sell
|
||||||
|
# when EMA goes down (the last < the previous), disable buy, allow sell
|
||||||
|
# uncomment this to enable it:
|
||||||
|
trendEMA:
|
||||||
|
interval: 1d
|
||||||
|
window: 7
|
||||||
|
maxGradient: 1.5
|
||||||
|
minGradient: 1.01
|
||||||
|
|
||||||
|
# ==================================================================
|
||||||
|
# Dynamic spread is an experimental feature. it will override the fixed spread settings above.
|
||||||
|
#
|
||||||
|
# dynamicSpread enables the automatic adjustment to bid and ask spread.
|
||||||
|
# Choose one of the scaling strategy to enable dynamicSpread:
|
||||||
|
# - amplitude: scales by K-line amplitude
|
||||||
|
# - weightedBollWidth: scales by weighted Bollinger band width (explained below)
|
||||||
|
# ==================================================================
|
||||||
|
#
|
||||||
|
# =========================================
|
||||||
|
# dynamicSpread with amplitude
|
||||||
|
# =========================================
|
||||||
|
# dynamicSpread:
|
||||||
|
# amplitude: # delete other scaling strategy if this is defined
|
||||||
|
# # window is the window of the SMAs of spreads
|
||||||
|
# window: 1
|
||||||
|
# askSpreadScale:
|
||||||
|
# byPercentage:
|
||||||
|
# # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
|
||||||
|
# exp:
|
||||||
|
# # from down to up
|
||||||
|
# domain: [ 0.0001, 0.005 ]
|
||||||
|
# # when in down band, holds 1.0 by maximum
|
||||||
|
# # when in up band, holds 0.05 by maximum
|
||||||
|
# range: [ 0.001, 0.002 ]
|
||||||
|
# bidSpreadScale:
|
||||||
|
# byPercentage:
|
||||||
|
# # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
|
||||||
|
# exp:
|
||||||
|
# # from down to up
|
||||||
|
# domain: [ 0.0001, 0.005 ]
|
||||||
|
# # when in down band, holds 1.0 by maximum
|
||||||
|
# # when in up band, holds 0.05 by maximum
|
||||||
|
# range: [ 0.001, 0.002 ]
|
||||||
|
#
|
||||||
|
# =========================================
|
||||||
|
# dynamicSpread with weightedBollWidth
|
||||||
|
# =========================================
|
||||||
|
# dynamicSpread:
|
||||||
|
# # weightedBollWidth scales spread base on weighted Bollinger bandwidth ratio between default and neutral bands.
|
||||||
|
# #
|
||||||
|
# # Given the default band: moving average bd_mid, band from bd_lower to bd_upper.
|
||||||
|
# # And the neutral band: from bn_lower to bn_upper
|
||||||
|
# # Set the sigmoid weighting function:
|
||||||
|
# # - to ask spread, the weighting density function d_weight(x) is sigmoid((x - bd_mid) / (bd_upper - bd_lower))
|
||||||
|
# # - to bid spread, the weighting density function d_weight(x) is sigmoid((bd_mid - x) / (bd_upper - bd_lower))
|
||||||
|
# # Then calculate the weighted band width ratio by taking integral of d_weight(x) from bx_lower to bx_upper:
|
||||||
|
# # - weighted_ratio = integral(d_weight from bn_lower to bn_upper) / integral(d_weight from bd_lower to bd_upper)
|
||||||
|
# # - The wider neutral band get greater ratio
|
||||||
|
# # - To ask spread, the higher neutral band get greater ratio
|
||||||
|
# # - To bid spread, the lower neutral band get greater ratio
|
||||||
|
# # The weighted ratio always positive, and may be greater than 1 if neutral band is wider than default band.
|
||||||
|
#
|
||||||
|
# weightedBollWidth: # delete other scaling strategy if this is defined
|
||||||
|
# # sensitivity is a factor of the weighting function: 1 / (1 + exp(-(x - bd_mid) * sensitivity / (bd_upper - bd_lower)))
|
||||||
|
# # A positive number. The greater factor, the sharper weighting function. Default set to 1.0 .
|
||||||
|
# sensitivity: 1.0
|
||||||
|
#
|
||||||
|
# askSpreadScale:
|
||||||
|
# byPercentage:
|
||||||
|
# # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
|
||||||
|
# linear:
|
||||||
|
# # from down to up
|
||||||
|
# domain: [ 0.1, 0.5 ]
|
||||||
|
# range: [ 0.001, 0.002 ]
|
||||||
|
# bidSpreadScale:
|
||||||
|
# byPercentage:
|
||||||
|
# # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
|
||||||
|
# linear:
|
||||||
|
# # from down to up
|
||||||
|
# domain: [ 0.1, 0.5 ]
|
||||||
|
# range: [ 0.001, 0.002 ]
|
||||||
|
|
||||||
|
# maxExposurePosition is the maximum position you can hold
|
||||||
|
# +10 means you can hold 10 ETH long position by maximum
|
||||||
|
# -10 means you can hold -10 ETH short position by maximum
|
||||||
|
# uncomment this if you want a fixed position exposure.
|
||||||
|
# maxExposurePosition: 3.0
|
||||||
|
maxExposurePosition: 10
|
||||||
|
|
||||||
|
# dynamicExposurePositionScale overrides maxExposurePosition
|
||||||
|
# for domain,
|
||||||
|
# -1 means -100%, the price is on the lower band price.
|
||||||
|
# if the price breaks the lower band, a number less than -1 will be given.
|
||||||
|
# 1 means 100%, the price is on the upper band price.
|
||||||
|
# if the price breaks the upper band, a number greater than 1 will be given, for example, 1.2 for 120%, and 1.3 for 130%.
|
||||||
|
dynamicExposurePositionScale:
|
||||||
|
byPercentage:
|
||||||
|
# exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
|
||||||
|
exp:
|
||||||
|
# from lower band -100% (-1) to upper band 100% (+1)
|
||||||
|
domain: [ -1, 1 ]
|
||||||
|
# when in down band, holds 1.0 by maximum
|
||||||
|
# when in up band, holds 0.05 by maximum
|
||||||
|
range: [ 10.0, 1.0 ]
|
||||||
|
|
||||||
|
# DisableShort means you can don't want short position during the market making
|
||||||
|
# THe short here means you might sell some of your existing inventory.
|
||||||
|
disableShort: true
|
||||||
|
|
||||||
|
# uptrendSkew, like the strongUptrendSkew, but the price is still in the default band.
|
||||||
|
uptrendSkew: 0.8
|
||||||
|
|
||||||
|
# downtrendSkew, like the strongDowntrendSkew, but the price is still in the default band.
|
||||||
|
downtrendSkew: 1.2
|
||||||
|
|
||||||
|
# defaultBollinger is a long-term time frame bollinger
|
||||||
|
# this bollinger band is used for controlling your position (how much you can hold)
|
||||||
|
# when price is near the upper band, it holds less.
|
||||||
|
# when price is near the lower band, it holds more.
|
||||||
|
defaultBollinger:
|
||||||
|
interval: "1h"
|
||||||
|
window: 21
|
||||||
|
bandWidth: 2.0
|
||||||
|
|
||||||
|
# neutralBollinger is the smaller range of the bollinger band
|
||||||
|
# If price is in this band, it usually means the price is oscillating.
|
||||||
|
neutralBollinger:
|
||||||
|
interval: "5m"
|
||||||
|
window: 21
|
||||||
|
bandWidth: 2.0
|
||||||
|
|
||||||
|
# tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band.
|
||||||
|
tradeInBand: true
|
||||||
|
|
||||||
|
# buyBelowNeutralSMA: when this set, it will only place buy order when the current price is below the SMA line.
|
||||||
|
buyBelowNeutralSMA: true
|
||||||
|
|
||||||
|
# emaCross is used for turning buy on/off
|
||||||
|
# when short term EMA cross fast term EMA, turn on buy,
|
||||||
|
# otherwise, turn off buy
|
||||||
|
emaCross:
|
||||||
|
enabled: false
|
||||||
|
interval: 1h
|
||||||
|
fastWindow: 3
|
||||||
|
slowWindow: 12
|
||||||
|
|
||||||
|
exits:
|
||||||
|
|
||||||
|
# roiTakeProfit is used to force taking profit by percentage of the position ROI (currently the price change)
|
||||||
|
# force to take the profit ROI exceeded the percentage.
|
||||||
|
- roiTakeProfit:
|
||||||
|
percentage: 3%
|
||||||
|
|
||||||
|
- protectiveStopLoss:
|
||||||
|
activationRatio: 1%
|
||||||
|
stopLossRatio: 0.2%
|
||||||
|
placeStopOrder: false
|
||||||
|
|
||||||
|
- protectiveStopLoss:
|
||||||
|
activationRatio: 2%
|
||||||
|
stopLossRatio: 1%
|
||||||
|
placeStopOrder: false
|
||||||
|
|
||||||
|
- protectiveStopLoss:
|
||||||
|
activationRatio: 5%
|
||||||
|
stopLossRatio: 3%
|
||||||
|
placeStopOrder: false
|
29
config/bollmaker_optimizer.yaml
Normal file
29
config/bollmaker_optimizer.yaml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# usage:
|
||||||
|
#
|
||||||
|
# go run ./cmd/qbtrade optimize --config bollmaker_ethusdt.yaml --optimizer-config optimizer.yaml --debug
|
||||||
|
#
|
||||||
|
---
|
||||||
|
executor:
|
||||||
|
type: local
|
||||||
|
local:
|
||||||
|
maxNumberOfProcesses: 10
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
- type: iterate
|
||||||
|
label: interval
|
||||||
|
path: '/exchangeStrategies/0/bollmaker/interval'
|
||||||
|
values: [ "1m", "5m", "15m", "30m" ]
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/bollmaker/amount'
|
||||||
|
label: amount
|
||||||
|
min: 20.0
|
||||||
|
max: 100.0
|
||||||
|
step: 20.0
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
label: spread
|
||||||
|
path: '/exchangeStrategies/0/bollmaker/spread'
|
||||||
|
min: 0.1%
|
||||||
|
max: 0.2%
|
||||||
|
step: 0.01%
|
26
config/convert.yaml
Normal file
26
config/convert.yaml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
convert:
|
||||||
|
## the initial asset you want to convert to
|
||||||
|
from: BNB
|
||||||
|
|
||||||
|
## the final asset you want to convert to
|
||||||
|
to: BTC
|
||||||
|
|
||||||
|
## interval is the period of trigger
|
||||||
|
interval: 1m
|
||||||
|
|
||||||
|
## minBalance is the minimal balance line you want to keep
|
||||||
|
## in this example, it means 1 BNB
|
||||||
|
minBalance: 1.0
|
||||||
|
|
||||||
|
## maxQuantity is the max quantity per order for converting asset
|
||||||
|
## in this example, it means 2 BNB
|
||||||
|
maxQuantity: 2.0
|
||||||
|
|
||||||
|
## useTakerOrder uses the taker price for the order, so the order will be a taker
|
||||||
|
## which will be filled immediately
|
||||||
|
useTakerOrder: true
|
||||||
|
|
||||||
|
|
23
config/dca.yaml
Normal file
23
config/dca.yaml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
backtest:
|
||||||
|
startTime: "2022-04-01"
|
||||||
|
endTime: "2022-05-01"
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
USDT: 20_000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
dca:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
budgetPeriod: day
|
||||||
|
investmentInterval: 4h
|
||||||
|
budget: 1000
|
||||||
|
|
||||||
|
|
31
config/dca2.yaml
Normal file
31
config/dca2.yaml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
backtest:
|
||||||
|
startTime: "2023-06-01"
|
||||||
|
endTime: "2023-07-01"
|
||||||
|
sessions:
|
||||||
|
- max
|
||||||
|
symbols:
|
||||||
|
- ETHUSDT
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
USDT: 20_000.0
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: max
|
||||||
|
dca2:
|
||||||
|
symbol: ETHUSDT
|
||||||
|
quoteInvestment: "200"
|
||||||
|
maxOrderCount: 5
|
||||||
|
priceDeviation: "0.01"
|
||||||
|
takeProfitRatio: "0.002"
|
||||||
|
coolDownInterval: 180
|
||||||
|
recoverWhenStart: true
|
||||||
|
keepOrdersWhenShutdown: true
|
12
config/deposit2transfer.yaml
Normal file
12
config/deposit2transfer.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
## deposit2transfer scans the deposit history and then transfer the deposited assets into your margin account
|
||||||
|
## currently only cross-margin is supported.
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
deposit2transfer:
|
||||||
|
## interval is the deposit history scanning interval
|
||||||
|
interval: 1m
|
||||||
|
|
||||||
|
## assets are the assets you want to transfer into the margin account
|
||||||
|
assets:
|
||||||
|
- USDT
|
141
config/driftBTC.yaml
Normal file
141
config/driftBTC.yaml
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
---
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
#futures: true
|
||||||
|
#margin: true
|
||||||
|
#isolatedMargin: true
|
||||||
|
#isolatedMarginSymbol: BTCUSDT
|
||||||
|
envVarPrefix: binance
|
||||||
|
heikinAshi: false
|
||||||
|
|
||||||
|
# Drift strategy intends to place buy/sell orders as much value mas it could be. To exchanges that requires to
|
||||||
|
# calculate fees before placing limit orders (e.g. FTX Pro), make sure the fee rate is configured correctly and
|
||||||
|
# enable modifyOrderAmountForFee to prevent order rejection.
|
||||||
|
makerFeeRate: 0.02%
|
||||||
|
takerFeeRate: 0.07%
|
||||||
|
modifyOrderAmountForFee: false
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
drift:
|
||||||
|
debug: false
|
||||||
|
minInterval: 1s
|
||||||
|
limitOrder: true
|
||||||
|
#quantity: 0.0012
|
||||||
|
canvasPath: "./output.png"
|
||||||
|
symbol: BTCUSDT
|
||||||
|
# kline interval for indicators
|
||||||
|
interval: 1s
|
||||||
|
window: 2
|
||||||
|
useAtr: true
|
||||||
|
useStopLoss: true
|
||||||
|
stoploss: 0.01%
|
||||||
|
source: hl2
|
||||||
|
predictOffset: 2
|
||||||
|
noTrailingStopLoss: true
|
||||||
|
# stddev on high/low-source
|
||||||
|
hlVarianceMultiplier: 0.7
|
||||||
|
hlRangeWindow: 6
|
||||||
|
smootherWindow: 10
|
||||||
|
fisherTransformWindow: 45
|
||||||
|
atrWindow: 24
|
||||||
|
# orders not been traded will be canceled after `pendingMinutes` minutes
|
||||||
|
pendingMinInterval: 6
|
||||||
|
noRebalance: true
|
||||||
|
trendWindow: 4
|
||||||
|
rebalanceFilter: 2
|
||||||
|
|
||||||
|
# ActivationRatio should be increasing order
|
||||||
|
# when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop
|
||||||
|
#trailingActivationRatio: [0.01, 0.016, 0.05]
|
||||||
|
#trailingActivationRatio: [0.001, 0.0081, 0.022]
|
||||||
|
trailingActivationRatio: [0.0008, 0.002, 0.01]
|
||||||
|
#trailingActivationRatio: []
|
||||||
|
#trailingCallbackRate: []
|
||||||
|
#trailingCallbackRate: [0.002, 0.01, 0.1]
|
||||||
|
#trailingCallbackRate: [0.0004, 0.0009, 0.018]
|
||||||
|
trailingCallbackRate: [0.00014, 0.0003, 0.0016]
|
||||||
|
|
||||||
|
generateGraph: true
|
||||||
|
graphPNLDeductFee: false
|
||||||
|
graphPNLPath: "./pnl.png"
|
||||||
|
graphCumPNLPath: "./cumpnl.png"
|
||||||
|
graphElapsedPath: "./elapsed.png"
|
||||||
|
#exits:
|
||||||
|
# - roiStopLoss:
|
||||||
|
# percentage: 0.35%
|
||||||
|
#- roiTakeProfit:
|
||||||
|
# percentage: 0.7%
|
||||||
|
#- protectiveStopLoss:
|
||||||
|
# activationRatio: 0.5%
|
||||||
|
# stopLossRatio: 0.2%
|
||||||
|
# placeStopOrder: false
|
||||||
|
#- trailingStop:
|
||||||
|
# callbackRate: 0.3%
|
||||||
|
# activationRatio is relative to the average cost,
|
||||||
|
# when side is buy, 1% means lower 1% than the average cost.
|
||||||
|
# when side is sell, 1% means higher 1% than the average cost.
|
||||||
|
# activationRatio: 0.7%
|
||||||
|
# minProfit uses the position ROI to calculate the profit ratio
|
||||||
|
|
||||||
|
# minProfit: 1.5%
|
||||||
|
# interval: 1m
|
||||||
|
# side: sell
|
||||||
|
# closePosition: 100%
|
||||||
|
|
||||||
|
#- trailingStop:
|
||||||
|
# callbackRate: 0.3%
|
||||||
|
# activationRatio is relative to the average cost,
|
||||||
|
# when side is buy, 1% means lower 1% than the average cost.
|
||||||
|
# when side is sell, 1% means higher 1% than the average cost.
|
||||||
|
# activationRatio: 0.7%
|
||||||
|
# minProfit uses the position ROI to calculate the profit ratio
|
||||||
|
|
||||||
|
# minProfit: 1.5%
|
||||||
|
# interval: 1m
|
||||||
|
# side: buy
|
||||||
|
# closePosition: 100%
|
||||||
|
#- protectiveStopLoss:
|
||||||
|
# activationRatio: 5%
|
||||||
|
# stopLossRatio: 1%
|
||||||
|
# placeStopOrder: false
|
||||||
|
#- cumulatedVolumeTakeProfit:
|
||||||
|
# interval: 5m
|
||||||
|
# window: 2
|
||||||
|
# minQuoteVolume: 200_000_000
|
||||||
|
#- protectiveStopLoss:
|
||||||
|
# activationRatio: 2%
|
||||||
|
# stopLossRatio: 1%
|
||||||
|
# placeStopOrder: false
|
||||||
|
|
||||||
|
sync:
|
||||||
|
userDataStream:
|
||||||
|
trades: true
|
||||||
|
filledOrders: true
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
startTime: "2022-10-19"
|
||||||
|
endTime: "2022-10-20"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions: [binance]
|
||||||
|
syncSecKLines: true
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
makerFeeRate: 0.000
|
||||||
|
takerFeeRate: 0.000
|
||||||
|
balances:
|
||||||
|
BTC: 0
|
||||||
|
USDT: 49
|
126
config/elliottwave.yaml
Normal file
126
config/elliottwave.yaml
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
---
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
futures: false
|
||||||
|
envVarPrefix: binance
|
||||||
|
heikinAshi: false
|
||||||
|
|
||||||
|
# Drift strategy intends to place buy/sell orders as much value mas it could be. To exchanges that requires to
|
||||||
|
# calculate fees before placing limit orders (e.g. FTX Pro), make sure the fee rate is configured correctly and
|
||||||
|
# enable modifyOrderAmountForFee to prevent order rejection.
|
||||||
|
makerFeeRate: 0.0002
|
||||||
|
takerFeeRate: 0.0007
|
||||||
|
modifyOrderAmountForFee: false
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
elliottwave:
|
||||||
|
minInterval: 1s
|
||||||
|
symbol: BTCUSDT
|
||||||
|
limitOrder: true
|
||||||
|
#quantity: 0.16
|
||||||
|
# kline interval for indicators
|
||||||
|
interval: 1s
|
||||||
|
stoploss: 0.01%
|
||||||
|
windowATR: 14
|
||||||
|
windowQuick: 4
|
||||||
|
windowSlow: 155
|
||||||
|
source: hl2
|
||||||
|
pendingMinInterval: 5
|
||||||
|
useHeikinAshi: true
|
||||||
|
|
||||||
|
drawGraph: true
|
||||||
|
graphIndicatorPath: "./indicator.png"
|
||||||
|
graphPNLPath: "./pnl.png"
|
||||||
|
graphCumPNLPath: "./cumpnl.png"
|
||||||
|
|
||||||
|
|
||||||
|
# ActivationRatio should be increasing order
|
||||||
|
# when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop
|
||||||
|
#trailingActivationRatio: [0.01, 0.016, 0.05]
|
||||||
|
#trailingActivationRatio: [0.001, 0.0081, 0.022]
|
||||||
|
#trailingActivationRatio: [0.0017, 0.01, 0.015]
|
||||||
|
trailingActivationRatio: []
|
||||||
|
trailingCallbackRate: []
|
||||||
|
#trailingCallbackRate: [0.002, 0.01, 0.1]
|
||||||
|
#trailingCallbackRate: [0.0004, 0.0009, 0.018]
|
||||||
|
#trailingCallbackRate: [0.0006, 0.0019, 0.006]
|
||||||
|
|
||||||
|
#exits:
|
||||||
|
# - roiStopLoss:
|
||||||
|
# percentage: 0.35%
|
||||||
|
#- roiTakeProfit:
|
||||||
|
# percentage: 0.7%
|
||||||
|
#- protectiveStopLoss:
|
||||||
|
# activationRatio: 0.5%
|
||||||
|
# stopLossRatio: 0.2%
|
||||||
|
# placeStopOrder: false
|
||||||
|
#- trailingStop:
|
||||||
|
# callbackRate: 0.3%
|
||||||
|
# activationRatio is relative to the average cost,
|
||||||
|
# when side is buy, 1% means lower 1% than the average cost.
|
||||||
|
# when side is sell, 1% means higher 1% than the average cost.
|
||||||
|
# activationRatio: 0.7%
|
||||||
|
# minProfit uses the position ROI to calculate the profit ratio
|
||||||
|
|
||||||
|
# minProfit: 1.5%
|
||||||
|
# interval: 1m
|
||||||
|
# side: sell
|
||||||
|
# closePosition: 100%
|
||||||
|
|
||||||
|
#- trailingStop:
|
||||||
|
# callbackRate: 0.3%
|
||||||
|
# activationRatio is relative to the average cost,
|
||||||
|
# when side is buy, 1% means lower 1% than the average cost.
|
||||||
|
# when side is sell, 1% means higher 1% than the average cost.
|
||||||
|
# activationRatio: 0.7%
|
||||||
|
# minProfit uses the position ROI to calculate the profit ratio
|
||||||
|
|
||||||
|
# minProfit: 1.5%
|
||||||
|
# interval: 1m
|
||||||
|
# side: buy
|
||||||
|
# closePosition: 100%
|
||||||
|
#- protectiveStopLoss:
|
||||||
|
# activationRatio: 5%
|
||||||
|
# stopLossRatio: 1%
|
||||||
|
# placeStopOrder: false
|
||||||
|
#- cumulatedVolumeTakeProfit:
|
||||||
|
# interval: 5m
|
||||||
|
# window: 2
|
||||||
|
# minQuoteVolume: 200_000_000
|
||||||
|
#- protectiveStopLoss:
|
||||||
|
# activationRatio: 2%
|
||||||
|
# stopLossRatio: 1%
|
||||||
|
# placeStopOrder: false
|
||||||
|
|
||||||
|
sync:
|
||||||
|
userDataStream:
|
||||||
|
trades: true
|
||||||
|
filledOrders: true
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
startTime: "2022-10-15"
|
||||||
|
endTime: "2022-10-19"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions: [binance]
|
||||||
|
syncSecKLines: true
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
makerFeeRate: 0.000
|
||||||
|
takerFeeRate: 0.000
|
||||||
|
balances:
|
||||||
|
BTC: 0
|
||||||
|
USDT: 100
|
37
config/emacross.yaml
Normal file
37
config/emacross.yaml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
emacross:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
# interval: 5m
|
||||||
|
# fastWindow: 6
|
||||||
|
# slowWindow: 18
|
||||||
|
# quantity: 0.01
|
||||||
|
leverage: 2
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
startTime: "2022-01-01"
|
||||||
|
endTime: "2022-03-01"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions: [binance]
|
||||||
|
# syncSecKLines: true
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
makerFeeRate: 0.0%
|
||||||
|
takerFeeRate: 0.075%
|
||||||
|
balances:
|
||||||
|
BTC: 0.0
|
||||||
|
USDT: 10_000.0
|
31
config/emastop.yaml
Normal file
31
config/emastop.yaml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
crossExchangeStrategies:
|
||||||
|
- emastop:
|
||||||
|
sourceExchange: "binance"
|
||||||
|
targetExchange: "max"
|
||||||
|
symbol: ETHUSDT
|
||||||
|
|
||||||
|
# interval is the kline interval we want to update our stop limit order
|
||||||
|
interval: 1m
|
||||||
|
|
||||||
|
# movingAverage* is used for calculating the stop price
|
||||||
|
movingAverageType: EWMA
|
||||||
|
movingAverageInterval: 1h
|
||||||
|
movingAverageWindow: 99
|
||||||
|
|
||||||
|
# stop price adjusts the stop price by a ratio. If defined, stop price = moving average price * ratio
|
||||||
|
stopPriceRatio: 0.975
|
||||||
|
|
||||||
|
# orderType is the stop order type, could be "market" or "limit"
|
||||||
|
orderType: market
|
||||||
|
# orderType: limit
|
||||||
|
|
||||||
|
# priceRatio greater than 1 means we will place the sell order as a maker order
|
||||||
|
# priceRatio less than 1 means we will place the sell order as a taker order (trade with the existing bid orders)
|
||||||
|
# priceRatio: 1.1
|
||||||
|
|
||||||
|
# you can specify "quantity" or "balancePercentage" for the sell order quantity
|
||||||
|
quantity: 0.02
|
||||||
|
|
||||||
|
# balancePercentage 0.25 means 25% of the current base asset
|
||||||
|
# balancePercentage: 0.25
|
7
config/environment.yaml
Normal file
7
config/environment.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
environment:
|
||||||
|
disableDefaultKLineSubscription: true
|
||||||
|
disableHistoryKLinePreload: true
|
||||||
|
disableStartupBalanceQuery: true
|
||||||
|
disableSessionTradeBuffer: true
|
||||||
|
disableMarketDataStore: true
|
||||||
|
maxSessionTradeBufferSize: true
|
11
config/etf.yaml
Normal file
11
config/etf.yaml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
etf:
|
||||||
|
duration: 24h
|
||||||
|
totalAmount: 200.0
|
||||||
|
index:
|
||||||
|
BTCUSDT: 5%
|
||||||
|
LTCUSDT: 15%
|
||||||
|
ETHUSDT: 30%
|
||||||
|
LINKUSDT: 20%
|
||||||
|
DOTUSDT: 30%
|
65
config/ewo_dgtrd.yaml
Normal file
65
config/ewo_dgtrd.yaml
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
futures: true
|
||||||
|
envVarPrefix: binance
|
||||||
|
heikinAshi: false
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
ewo_dgtrd:
|
||||||
|
symbol: MATICUSDT
|
||||||
|
# kline interval for indicators
|
||||||
|
interval: 15m
|
||||||
|
# use ema as MA
|
||||||
|
useEma: false
|
||||||
|
# use sma as MA, used when ema is false
|
||||||
|
# if both sma and ema are false, use EVMA
|
||||||
|
useSma: false
|
||||||
|
# ewo signal line window size
|
||||||
|
sigWin: 5
|
||||||
|
# SL percentage from entry price
|
||||||
|
stoploss: 2%
|
||||||
|
# use HeikinAshi klines instead of normal OHLC
|
||||||
|
useHeikinAshi: true
|
||||||
|
# disable SL when short
|
||||||
|
disableShortStop: false
|
||||||
|
# disable SL when long
|
||||||
|
disableLongStop: false
|
||||||
|
# CCI Stochastic Indicator high filter
|
||||||
|
cciStochFilterHigh: 80
|
||||||
|
# CCI Stochastic Indicator low filter
|
||||||
|
cciStochFilterLow: 20
|
||||||
|
# ewo change rate histogram's upperbound filter
|
||||||
|
# set to 1 would intend to let all ewo pass
|
||||||
|
ewoChangeFilterHigh: 1.
|
||||||
|
# ewo change rate histogram's lowerbound filter
|
||||||
|
# set to 0 would intend to let all ewo pass
|
||||||
|
ewoChangeFilterLow: 0.0
|
||||||
|
# print record exit point in log messages
|
||||||
|
record: false
|
||||||
|
|
||||||
|
sync:
|
||||||
|
userDataStream:
|
||||||
|
trades: true
|
||||||
|
filledOrders: true
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
symbols:
|
||||||
|
- MATICUSDT
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
startTime: "2022-05-01"
|
||||||
|
endTime: "2022-05-27"
|
||||||
|
symbols:
|
||||||
|
- MATICUSDT
|
||||||
|
sessions: [binance]
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
#makerFeeRate: 0
|
||||||
|
#takerFeeRate: 15
|
||||||
|
balances:
|
||||||
|
MATIC: 000.0
|
||||||
|
USDT: 15000.0
|
44
config/factorzoo.yaml
Normal file
44
config/factorzoo.yaml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
factorzoo:
|
||||||
|
symbol: BTCBUSD
|
||||||
|
linear:
|
||||||
|
enabled: true
|
||||||
|
interval: 1d
|
||||||
|
quantity: 1.0
|
||||||
|
window: 5
|
||||||
|
|
||||||
|
exits:
|
||||||
|
- trailingStop:
|
||||||
|
callbackRate: 1%
|
||||||
|
activationRatio: 1%
|
||||||
|
closePosition: 100%
|
||||||
|
minProfit: 15%
|
||||||
|
interval: 1m
|
||||||
|
side: buy
|
||||||
|
- trailingStop:
|
||||||
|
callbackRate: 1%
|
||||||
|
activationRatio: 1%
|
||||||
|
closePosition: 100%
|
||||||
|
minProfit: 15%
|
||||||
|
interval: 1m
|
||||||
|
side: sell
|
||||||
|
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
startTime: "2021-01-01"
|
||||||
|
endTime: "2022-08-31"
|
||||||
|
symbols:
|
||||||
|
- BTCBUSD
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
BTC: 1.0
|
||||||
|
BUSD: 40_000.0
|
34
config/fixedmaker.yaml
Normal file
34
config/fixedmaker.yaml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
backtest:
|
||||||
|
startTime: "2023-01-01"
|
||||||
|
endTime: "2023-05-31"
|
||||||
|
symbols:
|
||||||
|
- USDCUSDT
|
||||||
|
sessions:
|
||||||
|
- max
|
||||||
|
accounts:
|
||||||
|
max:
|
||||||
|
balances:
|
||||||
|
USDC: 500.0
|
||||||
|
USDT: 500.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
fixedmaker:
|
||||||
|
symbol: USDCUSDT
|
||||||
|
interval: 1m
|
||||||
|
halfSpread: 0.05%
|
||||||
|
quantity: 15
|
||||||
|
orderType: LIMIT_MAKER
|
||||||
|
dryRun: true
|
||||||
|
|
||||||
|
positionHardLimit: 1500
|
||||||
|
maxPositionQuantity: 1500
|
||||||
|
circuitBreakLossThreshold: -0.15
|
||||||
|
circuitBreakEMA:
|
||||||
|
interval: 1m
|
||||||
|
window: 14
|
||||||
|
|
||||||
|
inventorySkew:
|
||||||
|
inventoryRangeMultiplier: 1.0
|
||||||
|
targetBaseRatio: 0.5
|
14
config/flashcrash.yaml
Normal file
14
config/flashcrash.yaml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
flashcrash:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
interval: 1m
|
||||||
|
baseQuantity: 0.01
|
||||||
|
percentage: 0.6 # 60% of the current price from the moving average
|
||||||
|
gridNumber: 3
|
26
config/fmaker.yaml
Normal file
26
config/fmaker.yaml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
fmaker:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
interval: 1m
|
||||||
|
spread: 0.15%
|
||||||
|
amount: 300 # 11
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
startTime: "2022-01-01"
|
||||||
|
endTime: "2022-05-31"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
account:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
BTC: 1 # 1
|
||||||
|
USDT: 45_000 # 30_000
|
53
config/grid-usdttwd.yaml
Normal file
53
config/grid-usdttwd.yaml
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
|
||||||
|
riskControls:
|
||||||
|
# This is the session-based risk controller, which let you configure different risk controller by session.
|
||||||
|
sessionBased:
|
||||||
|
# "max" is the session name that you want to configure the risk control
|
||||||
|
max:
|
||||||
|
# orderExecutor is one of the risk control
|
||||||
|
orderExecutor:
|
||||||
|
# symbol-routed order executor
|
||||||
|
bySymbol:
|
||||||
|
USDTTWD:
|
||||||
|
# basic risk control order executor
|
||||||
|
basic:
|
||||||
|
minQuoteBalance: 100.0 # keep 100 twd
|
||||||
|
maxBaseAssetBalance: 100_000.0
|
||||||
|
minBaseAssetBalance: 1_000.0
|
||||||
|
maxOrderAmount: 2000.0 # 1000 twd
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
# for testing max draw down (MDD) at 03-12
|
||||||
|
# see here for more details
|
||||||
|
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
|
||||||
|
startTime: "2021-01-01"
|
||||||
|
endTime: "2021-01-30"
|
||||||
|
sessions:
|
||||||
|
- max
|
||||||
|
symbols:
|
||||||
|
- USDTTWD
|
||||||
|
feeMode: quote
|
||||||
|
accounts:
|
||||||
|
max:
|
||||||
|
makerFeeRate: 0.0125%
|
||||||
|
takerFeeRate: 0.075%
|
||||||
|
balances:
|
||||||
|
BTC: 0.0
|
||||||
|
USDT: 10_000.0
|
||||||
|
TWD: 100_000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
grid:
|
||||||
|
symbol: USDTTWD
|
||||||
|
quantity: 10.0 # 10 USDT per grid
|
||||||
|
gridNumber: 100 # 50 GRID, Total Amount will be 10 USDT * 50 GRID = 500 USDT
|
||||||
|
profitSpread: 0.1 # When buying USDT at 28.1, we will sell it at 28.1 + 0.1 = 28.2, When selling USDT at 28.1, we will buy it back at 28.1 - 0.1 = 28.0
|
||||||
|
upperPrice: 28.90
|
||||||
|
lowerPrice: 27.90
|
||||||
|
long: true # long mode means we don't keep cash when we sell usdt, we will use the same amount of twd to buy more usdt back
|
61
config/grid.yaml
Normal file
61
config/grid.yaml
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
#max:
|
||||||
|
# exchange: max
|
||||||
|
# envVarPrefix: max
|
||||||
|
|
||||||
|
riskControls:
|
||||||
|
# This is the session-based risk controller, which let you configure different risk controller by session.
|
||||||
|
sessionBased:
|
||||||
|
# "max" is the session name that you want to configure the risk control
|
||||||
|
max:
|
||||||
|
# orderExecutor is one of the risk control
|
||||||
|
orderExecutor:
|
||||||
|
# symbol-routed order executor
|
||||||
|
bySymbol:
|
||||||
|
BTCUSDT:
|
||||||
|
# basic risk control order executor
|
||||||
|
basic:
|
||||||
|
minQuoteBalance: 100.0
|
||||||
|
maxBaseAssetBalance: 3.0
|
||||||
|
minBaseAssetBalance: 0.0
|
||||||
|
maxOrderAmount: 1000.0
|
||||||
|
|
||||||
|
# example command:
|
||||||
|
# godotenv -f .env.local -- go run ./cmd/qbtrade backtest --sync-from 2020-11-01 --config config/grid.yaml --base-asset-baseline
|
||||||
|
backtest:
|
||||||
|
# for testing max draw down (MDD) at 03-12
|
||||||
|
# see here for more details
|
||||||
|
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
|
||||||
|
startTime: "2022-05-09"
|
||||||
|
endTime: "2022-05-20"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions: [binance]
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
BTC: 0.0
|
||||||
|
USDT: 10000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
grid:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
quantity: 0.001
|
||||||
|
# scaleQuantity:
|
||||||
|
# byPrice:
|
||||||
|
# exp:
|
||||||
|
# domain: [20_000, 30_000]
|
||||||
|
# range: [0.2, 0.001]
|
||||||
|
gridNumber: 20
|
||||||
|
profitSpread: 1000.0 # The profit price spread that you want to add to your sell order when your buy order is executed
|
||||||
|
upperPrice: 30_000.0
|
||||||
|
lowerPrice: 28_000.0
|
||||||
|
# long: true # The sell order is submitted in the same order amount as the filled corresponding buy order, rather than the same quantity.
|
||||||
|
|
94
config/grid2-max.yaml
Normal file
94
config/grid2-max.yaml
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
switches:
|
||||||
|
trade: false
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: MAX
|
||||||
|
|
||||||
|
|
||||||
|
# example command:
|
||||||
|
# godotenv -f .env.local -- go run ./cmd/qbtrade backtest --config config/grid2-max.yaml --base-asset-baseline
|
||||||
|
backtest:
|
||||||
|
startTime: "2022-01-01"
|
||||||
|
endTime: "2022-11-25"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions: [ max ]
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
BTC: 0.0
|
||||||
|
USDT: 10000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
## You can run the following command to cancel all grid orders if the orders are not successfully canceled:
|
||||||
|
## go run ./cmd/qbtrade --dotenv .env.local.max-staging cancel-order --all --symbol BTCUSDT --config config/grid2-max.yaml
|
||||||
|
- on: max
|
||||||
|
grid2:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
upperPrice: 18_000.0
|
||||||
|
lowerPrice: 16_000.0
|
||||||
|
gridNumber: 20
|
||||||
|
|
||||||
|
## compound is used for buying more inventory when the profit is made by the filled SELL order.
|
||||||
|
## when compound is disabled, fixed quantity is used for each grid order.
|
||||||
|
## default: false
|
||||||
|
compound: true
|
||||||
|
|
||||||
|
## earnBase is used to profit base quantity instead of quote quantity.
|
||||||
|
## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair.
|
||||||
|
# earnBase: true
|
||||||
|
|
||||||
|
## triggerPrice is used for opening your grid only when the last price touches your pre-set price.
|
||||||
|
## this is useful when you don't want to create a grid from a higher price.
|
||||||
|
## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000
|
||||||
|
# triggerPrice: 16_900.0
|
||||||
|
|
||||||
|
## stopLossPrice is used for closing the grid and sell all the inventory to stop loss.
|
||||||
|
## (optional)
|
||||||
|
stopLossPrice: 16_000.0
|
||||||
|
|
||||||
|
## takeProfitPrice is used for closing the grid and sell all the inventory at higher price to take profit
|
||||||
|
## (optional)
|
||||||
|
takeProfitPrice: 20_000.0
|
||||||
|
|
||||||
|
## profitSpread is the profit spread of the arbitrage order (sell order)
|
||||||
|
## greater the profitSpread, greater the profit you make when the sell order is filled.
|
||||||
|
## you can set this instead of the default grid profit spread.
|
||||||
|
## by default, profitSpread = (upperPrice - lowerPrice) / gridNumber
|
||||||
|
## that is, greater the gridNumber, lesser the profit of each grid.
|
||||||
|
# profitSpread: 1000.0
|
||||||
|
|
||||||
|
## There are 3 kinds of setup
|
||||||
|
## NOTICE: you can only choose one, uncomment the config to enable it
|
||||||
|
##
|
||||||
|
## 1) fixed amount: amount is the quote unit (e.g. USDT in BTCUSDT)
|
||||||
|
# amount: 10.0
|
||||||
|
|
||||||
|
## 2) fixed quantity: it will use your balance to place orders with the fixed quantity. e.g. 0.001 BTC
|
||||||
|
quantity: 0.001
|
||||||
|
|
||||||
|
## 3) quoteInvestment and baseInvestment: when using quoteInvestment, the strategy will automatically calculate your best quantity for the whole grid.
|
||||||
|
## quoteInvestment is required, and baseInvestment is optional (could be zero)
|
||||||
|
## if you have existing BTC position and want to reuse it you can set the baseInvestment.
|
||||||
|
# quoteInvestment: 10_000
|
||||||
|
# baseInvestment: 1.0
|
||||||
|
|
||||||
|
feeRate: 0.075%
|
||||||
|
closeWhenCancelOrder: true
|
||||||
|
resetPositionWhenStart: true
|
||||||
|
clearOpenOrdersWhenStart: false
|
||||||
|
keepOrdersWhenShutdown: false
|
||||||
|
recoverOrdersWhenStart: false
|
||||||
|
|
||||||
|
## skipSpreadCheck skips the minimal spread check for the grid profit
|
||||||
|
skipSpreadCheck: true
|
128
config/grid2.yaml
Normal file
128
config/grid2.yaml
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
switches:
|
||||||
|
trade: false
|
||||||
|
orderUpdate: false
|
||||||
|
submitOrder: false
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
sync:
|
||||||
|
# userDataStream is used to sync the trading data in real-time
|
||||||
|
# it uses the websocket connection to insert the trades
|
||||||
|
userDataStream:
|
||||||
|
trades: false
|
||||||
|
filledOrders: false
|
||||||
|
|
||||||
|
# since is the start date of your trading data
|
||||||
|
since: 2019-01-01
|
||||||
|
|
||||||
|
# sessions is the list of session names you want to sync
|
||||||
|
# by default, qbtrade sync all your available sessions.
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
|
||||||
|
# symbols is the list of symbols you want to sync
|
||||||
|
# by default, qbtrade try to guess your symbols by your existing account balances.
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
|
||||||
|
# example command:
|
||||||
|
# go run ./cmd/qbtrade backtest --config config/grid2.yaml --base-asset-baseline
|
||||||
|
backtest:
|
||||||
|
startTime: "2021-06-01"
|
||||||
|
endTime: "2021-06-30"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions: [binance]
|
||||||
|
feeMode: token
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
makerFeeRate: 0.075%
|
||||||
|
takerFeeRate: 0.075%
|
||||||
|
balances:
|
||||||
|
BTC: 1.0
|
||||||
|
USDT: 21_000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
grid2:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
|
||||||
|
## autoRange can be used to detect a price range from a specific time frame
|
||||||
|
## the pivot low / pivot high of the given range will be used for lowerPrice and upperPrice.
|
||||||
|
## when autoRange is set, it will override the upperPrice/lowerPrice settings.
|
||||||
|
##
|
||||||
|
## the valid format is [1-9][hdw]
|
||||||
|
## example: "14d" means it will find the highest/lowest price that is higher/lower than left 14d and right 14d.
|
||||||
|
# autoRange: 14d
|
||||||
|
|
||||||
|
lowerPrice: 28_000.0
|
||||||
|
upperPrice: 50_000.0
|
||||||
|
|
||||||
|
## gridNumber is the total orders between the upper price and the lower price
|
||||||
|
## gridSpread = (upperPrice - lowerPrice) / gridNumber
|
||||||
|
## Make sure your gridNumber satisfy this: MIN(gridSpread/lowerPrice, gridSpread/upperPrice) > (makerFeeRate * 2)
|
||||||
|
gridNumber: 150
|
||||||
|
|
||||||
|
## compound is used for buying more inventory when the profit is made by the filled SELL order.
|
||||||
|
## when compound is disabled, fixed quantity is used for each grid order.
|
||||||
|
## default: false
|
||||||
|
compound: true
|
||||||
|
|
||||||
|
## earnBase is used to profit base quantity instead of quote quantity.
|
||||||
|
## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair.
|
||||||
|
# earnBase: true
|
||||||
|
|
||||||
|
## triggerPrice (optional) is used for opening your grid only when the last price touches your trigger price.
|
||||||
|
## this is useful when you don't want to create a grid from a higher price.
|
||||||
|
## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000
|
||||||
|
# triggerPrice: 17_000.0
|
||||||
|
|
||||||
|
## triggerPrice (optional) is used for closing your grid only when the last price touches your stop loss price.
|
||||||
|
## for example, when the price drops to 17_000.0 then close the grid and sell all base inventory.
|
||||||
|
# stopLossPrice: 10_000.0
|
||||||
|
|
||||||
|
## profitSpread is the profit spread of the arbitrage order (sell order)
|
||||||
|
## greater the profitSpread, greater the profit you make when the sell order is filled.
|
||||||
|
## you can set this instead of the default grid profit spread.
|
||||||
|
## by default, profitSpread = (upperPrice - lowerPrice) / gridNumber
|
||||||
|
## that is, greater the gridNumber, lesser the profit of each grid.
|
||||||
|
# profitSpread: 1000.0
|
||||||
|
|
||||||
|
## There are 3 kinds of setup
|
||||||
|
## NOTICE: you can only choose one, uncomment the config to enable it
|
||||||
|
##
|
||||||
|
## 1) fixed amount: amount is the quote unit (e.g. 10 USDT in BTCUSDT)
|
||||||
|
# amount: 10.0
|
||||||
|
|
||||||
|
## 2) fixed quantity: it will use your balance to place orders with the fixed quantity. e.g. 0.001 BTC
|
||||||
|
# quantity: 0.001
|
||||||
|
|
||||||
|
## 3) quoteInvestment and baseInvestment: when using quoteInvestment, the strategy will automatically calculate your best quantity for the whole grid.
|
||||||
|
## quoteInvestment is required, and baseInvestment is optional (could be zero)
|
||||||
|
## if you have existing BTC position and want to reuse it you can set the baseInvestment.
|
||||||
|
quoteInvestment: 20_000
|
||||||
|
|
||||||
|
## baseInvestment (optional) can be useful when you have existing inventory, maybe bought at much lower price
|
||||||
|
baseInvestment: 1.0
|
||||||
|
|
||||||
|
## closeWhenCancelOrder (optional)
|
||||||
|
## default to false
|
||||||
|
closeWhenCancelOrder: true
|
||||||
|
|
||||||
|
## resetPositionWhenStart (optional)
|
||||||
|
## default to false
|
||||||
|
resetPositionWhenStart: false
|
||||||
|
|
||||||
|
## clearOpenOrdersWhenStart (optional)
|
||||||
|
## default to false
|
||||||
|
clearOpenOrdersWhenStart: false
|
||||||
|
keepOrdersWhenShutdown: false
|
37
config/harmonic.yaml
Normal file
37
config/harmonic.yaml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
harmonic:
|
||||||
|
symbol: BTCBUSD
|
||||||
|
interval: 1s
|
||||||
|
window: 60
|
||||||
|
quantity: 0.005
|
||||||
|
# Draw pnl
|
||||||
|
drawGraph: true
|
||||||
|
graphPNLPath: "./pnl.png"
|
||||||
|
graphCumPNLPath: "./cumpnl.png"
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
startTime: "2022-10-01"
|
||||||
|
endTime: "2022-10-07"
|
||||||
|
symbols:
|
||||||
|
- BTCBUSD
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
BTC: 1.0
|
||||||
|
BUSD: 60_000.0
|
40
config/irr.yaml
Normal file
40
config/irr.yaml
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
irr:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
interval: 1m
|
||||||
|
window: 10
|
||||||
|
amount: 5000
|
||||||
|
# Draw pnl
|
||||||
|
drawGraph: true
|
||||||
|
graphPNLPath: "./pnl.png"
|
||||||
|
graphCumPNLPath: "./cumpnl.png"
|
||||||
|
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
startTime: "2022-01-01"
|
||||||
|
endTime: "2022-11-01"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions: [binance]
|
||||||
|
# syncSecKLines: true
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
makerFeeRate: 0.0000
|
||||||
|
takerFeeRate: 0.0000
|
||||||
|
balances:
|
||||||
|
BTC: 0.0
|
||||||
|
USDT: 5000
|
186
config/linregmaker.yaml
Normal file
186
config/linregmaker.yaml
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
---
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
margin: true
|
||||||
|
isolatedMargin: true
|
||||||
|
isolatedMarginSymbol: BTCUSDT
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions: [binance]
|
||||||
|
# for testing max draw down (MDD) at 03-12
|
||||||
|
# see here for more details
|
||||||
|
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
|
||||||
|
startTime: "2022-05-01"
|
||||||
|
endTime: "2022-10-31"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
makerCommission: 10 # 0.15%
|
||||||
|
takerCommission: 15 # 0.15%
|
||||||
|
balances:
|
||||||
|
BTC: 2.0
|
||||||
|
USDT: 10000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
linregmaker:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
|
||||||
|
# interval is how long do you want to update your order price and quantity
|
||||||
|
interval: 1m
|
||||||
|
|
||||||
|
# leverage uses the account net value to calculate the allowed margin
|
||||||
|
leverage: 1
|
||||||
|
|
||||||
|
# reverseEMA is used to determine the long-term trend.
|
||||||
|
# Above the ReverseEMA is the long trend and vise versa.
|
||||||
|
# All the opposite trend position will be closed upon the trend change
|
||||||
|
reverseEMA:
|
||||||
|
interval: 1d
|
||||||
|
window: 60
|
||||||
|
|
||||||
|
# reverseInterval is the interval to check trend reverse against ReverseEMA. Close price of this interval crossing
|
||||||
|
# the ReverseEMA triggers main trend change.
|
||||||
|
reverseInterval: 4h
|
||||||
|
|
||||||
|
# fastLinReg is to determine the short-term trend.
|
||||||
|
# Buy/sell orders are placed if the FastLinReg and the ReverseEMA trend are in the same direction, and only orders
|
||||||
|
# that reduce position are placed if the FastLinReg and the ReverseEMA trend are in different directions.
|
||||||
|
fastLinReg:
|
||||||
|
interval: 1m
|
||||||
|
window: 30
|
||||||
|
|
||||||
|
# slowLinReg is to determine the midterm trend.
|
||||||
|
# When the SlowLinReg and the ReverseEMA trend are in different directions, creation of opposite position is
|
||||||
|
# allowed.
|
||||||
|
slowLinReg:
|
||||||
|
interval: 1m
|
||||||
|
window: 120
|
||||||
|
|
||||||
|
# allowOppositePosition if true, the creation of opposite position is allowed when both fast and slow LinReg are in
|
||||||
|
# the opposite direction to main trend
|
||||||
|
allowOppositePosition: true
|
||||||
|
|
||||||
|
# fasterDecreaseRatio the quantity of decreasing position orders are multiplied by this ratio when both fast and
|
||||||
|
# slow LinReg are in the opposite direction to main trend
|
||||||
|
fasterDecreaseRatio: 2
|
||||||
|
|
||||||
|
# neutralBollinger is the smaller range of the bollinger band
|
||||||
|
# If price is in this band, it usually means the price is oscillating.
|
||||||
|
# If price goes out of this band, we tend to not place sell orders or buy orders
|
||||||
|
neutralBollinger:
|
||||||
|
interval: "15m"
|
||||||
|
window: 21
|
||||||
|
bandWidth: 2.0
|
||||||
|
|
||||||
|
# tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band.
|
||||||
|
tradeInBand: true
|
||||||
|
|
||||||
|
# spread is the price spread from the middle price.
|
||||||
|
# For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread))
|
||||||
|
# For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread))
|
||||||
|
# Spread can be set by percentage or floating number. e.g., 0.1% or 0.001
|
||||||
|
spread: 0.1%
|
||||||
|
# dynamicSpread enables the automatic adjustment to bid and ask spread.
|
||||||
|
# Overrides Spread, BidSpread, and AskSpread
|
||||||
|
dynamicSpread:
|
||||||
|
amplitude: # delete other scaling strategy if this is defined
|
||||||
|
# window is the window of the SMAs of spreads
|
||||||
|
window: 1
|
||||||
|
interval: "1m"
|
||||||
|
askSpreadScale:
|
||||||
|
byPercentage:
|
||||||
|
# exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
|
||||||
|
exp:
|
||||||
|
# from down to up
|
||||||
|
domain: [ 0.0001, 0.005 ]
|
||||||
|
# the spread range
|
||||||
|
range: [ 0.001, 0.002 ]
|
||||||
|
bidSpreadScale:
|
||||||
|
byPercentage:
|
||||||
|
# exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
|
||||||
|
exp:
|
||||||
|
# from down to up
|
||||||
|
domain: [ 0.0001, 0.005 ]
|
||||||
|
# the spread range
|
||||||
|
range: [ 0.001, 0.002 ]
|
||||||
|
|
||||||
|
# maxExposurePosition is the maximum position you can hold
|
||||||
|
# 10 means you can hold 10 ETH long/short position by maximum
|
||||||
|
#maxExposurePosition: 10
|
||||||
|
# dynamicExposure is used to define the exposure position range with the given percentage.
|
||||||
|
# When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically
|
||||||
|
dynamicExposure:
|
||||||
|
bollBandExposure:
|
||||||
|
interval: "1h"
|
||||||
|
window: 21
|
||||||
|
bandWidth: 2.0
|
||||||
|
dynamicExposurePositionScale:
|
||||||
|
byPercentage:
|
||||||
|
# exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
|
||||||
|
exp:
|
||||||
|
# from lower band -100% (-1) to upper band 100% (+1)
|
||||||
|
domain: [ -1, 1 ]
|
||||||
|
# when in down band, holds 0.1 by maximum
|
||||||
|
# when in up band, holds 1 by maximum
|
||||||
|
range: [ 0.1, 1 ]
|
||||||
|
|
||||||
|
# quantity is the base order quantity for your buy/sell order.
|
||||||
|
quantity: 0.001
|
||||||
|
# amount: fixed amount instead of qty
|
||||||
|
#amount: 10
|
||||||
|
# useDynamicQuantityAsAmount calculates amount instead of quantity
|
||||||
|
useDynamicQuantityAsAmount: false
|
||||||
|
# dynamicQuantityIncrease calculates the increase position order quantity dynamically
|
||||||
|
dynamicQuantityIncrease:
|
||||||
|
- linRegDynamicQuantity:
|
||||||
|
quantityLinReg:
|
||||||
|
interval: 1m
|
||||||
|
window: 20
|
||||||
|
dynamicQuantityLinRegScale:
|
||||||
|
byPercentage:
|
||||||
|
linear:
|
||||||
|
domain: [ -0.0001, 0.00005 ]
|
||||||
|
range: [ 0, 0.02 ]
|
||||||
|
# dynamicQuantityDecrease calculates the decrease position order quantity dynamically
|
||||||
|
dynamicQuantityDecrease:
|
||||||
|
- linRegDynamicQuantity:
|
||||||
|
quantityLinReg:
|
||||||
|
interval: 1m
|
||||||
|
window: 20
|
||||||
|
dynamicQuantityLinRegScale:
|
||||||
|
byPercentage:
|
||||||
|
linear:
|
||||||
|
domain: [ -0.00005, 0.0001 ]
|
||||||
|
range: [ 0.02, 0 ]
|
||||||
|
|
||||||
|
# minProfitSpread is the minimal order price spread from the current average cost.
|
||||||
|
# For long position, you will only place sell order above the price (= average cost * (1 + minProfitSpread))
|
||||||
|
# For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread))
|
||||||
|
minProfitSpread: 0.1%
|
||||||
|
|
||||||
|
# minProfitActivationRate activates MinProfitSpread when position RoI higher than the specified percentage
|
||||||
|
minProfitActivationRate: -10%
|
||||||
|
|
||||||
|
exits:
|
||||||
|
# roiStopLoss is the stop loss percentage of the position ROI (currently the price change)
|
||||||
|
- roiStopLoss:
|
||||||
|
percentage: 30%
|
||||||
|
|
||||||
|
profitStatsTracker:
|
||||||
|
interval: 1d
|
||||||
|
window: 30
|
||||||
|
accumulatedProfitReport:
|
||||||
|
profitMAWindow: 60
|
||||||
|
shortTermProfitWindow: 14
|
||||||
|
tsvReportPath: res.tsv
|
||||||
|
trackParameters: false
|
54
config/liquiditymaker.yaml
Normal file
54
config/liquiditymaker.yaml
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
makerFeeRate: 0%
|
||||||
|
takerFeeRate: 0.025%
|
||||||
|
|
||||||
|
#services:
|
||||||
|
# googleSpreadSheet:
|
||||||
|
# jsonTokenFile: ".credentials/google-cloud/service-account-json-token.json"
|
||||||
|
# spreadSheetId: "YOUR_SPREADSHEET_ID"
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
liquiditymaker:
|
||||||
|
symbol: &symbol USDTTWD
|
||||||
|
|
||||||
|
## adjustmentUpdateInterval is the interval for adjusting position
|
||||||
|
adjustmentUpdateInterval: 1m
|
||||||
|
|
||||||
|
## liquidityUpdateInterval is the interval for updating liquidity orders
|
||||||
|
liquidityUpdateInterval: 1h
|
||||||
|
|
||||||
|
numOfLiquidityLayers: 30
|
||||||
|
askLiquidityAmount: 20_000.0
|
||||||
|
bidLiquidityAmount: 20_000.0
|
||||||
|
liquidityPriceRange: 2%
|
||||||
|
useLastTradePrice: true
|
||||||
|
spread: 1.1%
|
||||||
|
|
||||||
|
liquidityScale:
|
||||||
|
exp:
|
||||||
|
domain: [1, 30]
|
||||||
|
range: [1, 4]
|
||||||
|
|
||||||
|
## maxExposure controls how much balance should be used for placing the maker orders
|
||||||
|
maxExposure: 200_000
|
||||||
|
minProfit: 0.01%
|
||||||
|
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- max
|
||||||
|
startTime: "2023-05-20"
|
||||||
|
endTime: "2023-06-01"
|
||||||
|
symbols:
|
||||||
|
- *symbol
|
||||||
|
account:
|
||||||
|
max:
|
||||||
|
makerFeeRate: 0.0%
|
||||||
|
takerFeeRate: 0.025%
|
||||||
|
balances:
|
||||||
|
USDT: 5000
|
||||||
|
TWD: 150_000
|
27
config/marketcap.yaml
Normal file
27
config/marketcap.yaml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
marketcap:
|
||||||
|
interval: 1m
|
||||||
|
quoteCurrency: TWD
|
||||||
|
quoteCurrencyWeight: 0%
|
||||||
|
baseCurrencies:
|
||||||
|
- BTC
|
||||||
|
- ETH
|
||||||
|
- MATIC
|
||||||
|
threshold: 1%
|
||||||
|
# max amount to buy or sell per order
|
||||||
|
maxAmount: 1_000
|
||||||
|
queryInterval: 1h
|
||||||
|
orderType: LIMIT_MAKER # LIMIT_MAKER, LIMIT, MARKET
|
||||||
|
dryRun: true
|
35
config/max-margin.yaml
Normal file
35
config/max-margin.yaml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
max_margin:
|
||||||
|
exchange: max
|
||||||
|
margin: true
|
||||||
|
|
||||||
|
sync:
|
||||||
|
# userDataStream is used to sync the trading data in real-time
|
||||||
|
# it uses the websocket connection to insert the trades
|
||||||
|
userDataStream:
|
||||||
|
trades: false
|
||||||
|
filledOrders: false
|
||||||
|
|
||||||
|
# since is the start date of your trading data
|
||||||
|
since: 2019-11-01
|
||||||
|
|
||||||
|
# sessions is the list of session names you want to sync
|
||||||
|
# by default, qbtrade sync all your available sessions.
|
||||||
|
sessions:
|
||||||
|
- max_margin
|
||||||
|
|
||||||
|
# symbols is the list of symbols you want to sync
|
||||||
|
# by default, qbtrade try to guess your symbols by your existing account balances.
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
- ETHUSDT
|
||||||
|
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: max_margin
|
||||||
|
pricealert:
|
||||||
|
symbol: LINKUSDT
|
||||||
|
interval: 1m
|
||||||
|
|
10
config/minimal.yaml
Normal file
10
config/minimal.yaml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
xpuremaker:
|
||||||
|
symbol: MAXUSDT
|
||||||
|
numOrders: 2
|
||||||
|
side: both
|
||||||
|
behindVolume: 1000.0
|
||||||
|
priceTick: 0.01
|
||||||
|
baseQuantity: 100.0
|
50
config/multi-session.yaml
Normal file
50
config/multi-session.yaml
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
binance_margin_linkusdt:
|
||||||
|
exchange: binance
|
||||||
|
margin: true
|
||||||
|
isolatedMargin: true
|
||||||
|
isolatedMarginSymbol: LINKUSDT
|
||||||
|
|
||||||
|
binance_cross_margin:
|
||||||
|
exchange: binance
|
||||||
|
margin: true
|
||||||
|
#
|
||||||
|
# The following API key/secret will be used:
|
||||||
|
# BINANCE_CROSS_MARGIN_API_KEY=____YOUR_MARGIN_API_KEY____
|
||||||
|
# BINANCE_CROSS_MARGIN_API_SECRET=____YOUR_MARGIN_API_SECRET____
|
||||||
|
envVarPrefix: BINANCE_CROSS_MARGIN
|
||||||
|
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
#
|
||||||
|
# The following API key/secret will be used:
|
||||||
|
# BINANCE_SPOT_API_KEY=____YOUR_SPOT_API_KEY____
|
||||||
|
# BINANCE_SPOT_API_SECRET=____YOUR_SPOT_API_SECRET____
|
||||||
|
envVarPrefix: BINANCE_SPOT
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance_margin_linkusdt
|
||||||
|
support:
|
||||||
|
symbol: LINKUSDT
|
||||||
|
interval: 1m
|
||||||
|
minVolume: 2_000
|
||||||
|
marginOrderSideEffect: borrow
|
||||||
|
|
||||||
|
scaleQuantity:
|
||||||
|
byVolume:
|
||||||
|
exp:
|
||||||
|
domain: [ 1_000, 200_000 ]
|
||||||
|
range: [ 3.0, 5.0 ]
|
||||||
|
|
||||||
|
maxBaseAssetBalance: 1000.0
|
||||||
|
minQuoteAssetBalance: 2000.0
|
||||||
|
|
||||||
|
targets:
|
||||||
|
- profitPercentage: 0.02
|
||||||
|
quantityPercentage: 0.5
|
||||||
|
marginOrderSideEffect: repay
|
||||||
|
|
52
config/optimizer-hyperparam-search.yaml
Normal file
52
config/optimizer-hyperparam-search.yaml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# usage:
|
||||||
|
#
|
||||||
|
# go run ./cmd/qbtrade hoptimize --config bollmaker_ethusdt.yaml --optimizer-config optimizer-hyperparam-search.yaml
|
||||||
|
#
|
||||||
|
---
|
||||||
|
# The search algorithm. Supports the following algorithms:
|
||||||
|
# - tpe: (default) Tree-structured Parzen Estimators
|
||||||
|
# - cmaes: Covariance Matrix Adaptation Evolution Strategy
|
||||||
|
# - sobol: Quasi-monte carlo sampling based on Sobol sequence
|
||||||
|
# - random: random search
|
||||||
|
# Reference: https://c-bata.medium.com/practical-bayesian-optimization-in-go-using-goptuna-edf97195fcb5
|
||||||
|
algorithm: tpe
|
||||||
|
|
||||||
|
# The objective function to be maximized. Possible options are:
|
||||||
|
# - profit: by trading profit
|
||||||
|
# - volume: by trading volume
|
||||||
|
# - equity: by equity difference
|
||||||
|
objectiveBy: equity
|
||||||
|
|
||||||
|
# Maximum number of search evaluations.
|
||||||
|
maxEvaluation: 1000
|
||||||
|
|
||||||
|
executor:
|
||||||
|
type: local
|
||||||
|
local:
|
||||||
|
maxNumberOfProcesses: 10
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
- type: string # alias: iterate
|
||||||
|
path: '/exchangeStrategies/0/bollmaker/interval'
|
||||||
|
values: ["1m", "5m"]
|
||||||
|
|
||||||
|
- type: rangeInt
|
||||||
|
label: window
|
||||||
|
path: '/exchangeStrategies/0/bollmaker/defaultBollinger/window'
|
||||||
|
min: 12
|
||||||
|
max: 240
|
||||||
|
|
||||||
|
- type: rangeFloat # alias: range
|
||||||
|
path: '/exchangeStrategies/0/bollmaker/spread'
|
||||||
|
min: 0.001
|
||||||
|
max: 0.002
|
||||||
|
|
||||||
|
- type: rangeFloat
|
||||||
|
path: '/exchangeStrategies/0/bollmaker/quantity'
|
||||||
|
min: 0.001
|
||||||
|
max: 0.070
|
||||||
|
# Most markets defines the minimum order amount. "step" is useful in such case.
|
||||||
|
step: 0.001
|
||||||
|
|
||||||
|
- type: bool
|
||||||
|
path: '/exchangeStrategies/0/bollmaker/buyBelowNeutralSMA'
|
26
config/optimizer.yaml
Normal file
26
config/optimizer.yaml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# usage:
|
||||||
|
#
|
||||||
|
# go run ./cmd/qbtrade optimize --config bollmaker_ethusdt.yaml --optimizer-config optimizer.yaml --debug
|
||||||
|
#
|
||||||
|
---
|
||||||
|
executor:
|
||||||
|
type: local
|
||||||
|
local:
|
||||||
|
maxNumberOfProcesses: 10
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
- type: iterate
|
||||||
|
path: '/exchangeStrategies/0/bollmaker/interval'
|
||||||
|
values: ["1m", "5m"]
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/bollmaker/amount'
|
||||||
|
min: 20.0
|
||||||
|
max: 40.0
|
||||||
|
step: 20.0
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/bollmaker/spread'
|
||||||
|
min: 0.1%
|
||||||
|
max: 0.2%
|
||||||
|
step: 0.02%
|
63
config/pivotshort-GMTBUSD.yaml
Normal file
63
config/pivotshort-GMTBUSD.yaml
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
margin: true
|
||||||
|
isolatedMargin: true
|
||||||
|
isolatedMarginSymbol: GMTBUSD
|
||||||
|
# futures: true
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
pivotshort:
|
||||||
|
symbol: GMTBUSD
|
||||||
|
interval: 5m
|
||||||
|
window: 120
|
||||||
|
|
||||||
|
entry:
|
||||||
|
immediate: true
|
||||||
|
catBounceRatio: 1%
|
||||||
|
quantity: 20
|
||||||
|
numLayers: 3
|
||||||
|
marginOrderSideEffect: borrow
|
||||||
|
|
||||||
|
exits:
|
||||||
|
# roiStopLoss is the stop loss percentage of the position ROI (currently the price change)
|
||||||
|
- roiStopLoss:
|
||||||
|
percentage: 2%
|
||||||
|
|
||||||
|
# roiTakeProfit is used to force taking profit by percentage of the position ROI (currently the price change)
|
||||||
|
# force to take the profit ROI exceeded the percentage.
|
||||||
|
- roiTakeProfit:
|
||||||
|
percentage: 30%
|
||||||
|
|
||||||
|
- protectiveStopLoss:
|
||||||
|
activationRatio: 1%
|
||||||
|
stopLossRatio: 0.2%
|
||||||
|
placeStopOrder: true
|
||||||
|
|
||||||
|
# lowerShadowTakeProfit is used to taking profit when the (lower shadow height / low price) > lowerShadowRatio
|
||||||
|
# you can grab a simple stats by the following SQL:
|
||||||
|
# SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20;
|
||||||
|
- lowerShadowTakeProfit:
|
||||||
|
ratio: 3%
|
||||||
|
|
||||||
|
# cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold
|
||||||
|
- cumulatedVolumeTakeProfit:
|
||||||
|
minQuoteVolume: 90_000_000
|
||||||
|
window: 5
|
||||||
|
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
startTime: "2022-05-25"
|
||||||
|
endTime: "2022-06-03"
|
||||||
|
symbols:
|
||||||
|
- GMTBUSD
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
GMT: 3_000.0
|
||||||
|
USDT: 3_000.0
|
153
config/pivotshort.yaml
Normal file
153
config/pivotshort.yaml
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
# uncomment this to enable cross margin
|
||||||
|
margin: true
|
||||||
|
|
||||||
|
# uncomment this to enable isolated margin
|
||||||
|
# isolatedMargin: true
|
||||||
|
# isolatedMarginSymbol: ETHUSDT
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
pivotshort:
|
||||||
|
symbol: ETHUSDT
|
||||||
|
|
||||||
|
# interval is the main pivot interval
|
||||||
|
interval: 5m
|
||||||
|
|
||||||
|
# window is the main pivot window
|
||||||
|
window: 200
|
||||||
|
|
||||||
|
quantity: 10.0
|
||||||
|
|
||||||
|
# when quantity is not given, leverage will be used.
|
||||||
|
# leverage: 10.0
|
||||||
|
|
||||||
|
# breakLow settings are used for shorting when the current price break the previous low
|
||||||
|
breakLow:
|
||||||
|
# ratio is how much the price breaks the previous low to trigger the short.
|
||||||
|
ratio: 0%
|
||||||
|
|
||||||
|
# quantity is used for submitting the sell order
|
||||||
|
# if quantity is not set, all base balance will be used for selling the short.
|
||||||
|
quantity: 10.0
|
||||||
|
|
||||||
|
# marketOrder submits the market sell order when the closed price is lower than the previous pivot low.
|
||||||
|
# by default we will use market order
|
||||||
|
marketOrder: true
|
||||||
|
|
||||||
|
# limitOrder place limit order to open the short position instead of using market order
|
||||||
|
# this is useful when your quantity or leverage is quiet large.
|
||||||
|
limitOrder: false
|
||||||
|
|
||||||
|
# limitOrderTakerRatio is the price ratio to adjust your limit order as a taker order. e.g., 0.1%
|
||||||
|
# for sell order, 0.1% ratio means your final price = price * (1 - 0.1%)
|
||||||
|
# for buy order, 0.1% ratio means your final price = price * (1 + 0.1%)
|
||||||
|
# this is only enabled when the limitOrder option set to true
|
||||||
|
limitOrderTakerRatio: 0
|
||||||
|
|
||||||
|
# bounceRatio is used for calculating the price of the limit sell order.
|
||||||
|
# it's ratio of pivot low bounce when a new pivot low is detected.
|
||||||
|
# Sometimes when the price breaks the previous low, the price might be pulled back to a higher price.
|
||||||
|
# The bounceRatio is useful for such case, however, you might also miss the chance to short at the price if there is no pull back.
|
||||||
|
# Notice: When marketOrder is set, bounceRatio will not be used.
|
||||||
|
# bounceRatio: 0.1%
|
||||||
|
|
||||||
|
# stopEMA is the price range we allow short.
|
||||||
|
# Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange])
|
||||||
|
# Higher the stopEMARange than higher the chance to open a short
|
||||||
|
stopEMA:
|
||||||
|
interval: 1h
|
||||||
|
window: 99
|
||||||
|
range: 2%
|
||||||
|
|
||||||
|
trendEMA:
|
||||||
|
interval: 1d
|
||||||
|
window: 7
|
||||||
|
|
||||||
|
resistanceShort:
|
||||||
|
enabled: true
|
||||||
|
interval: 5m
|
||||||
|
window: 80
|
||||||
|
|
||||||
|
quantity: 10.0
|
||||||
|
|
||||||
|
# minDistance is used to ignore the place that is too near to the current price
|
||||||
|
minDistance: 5%
|
||||||
|
groupDistance: 1%
|
||||||
|
|
||||||
|
# ratio is the ratio of the resistance price,
|
||||||
|
# higher the ratio, higher the sell price
|
||||||
|
# first_layer_price = resistance_price * (1 + ratio)
|
||||||
|
# second_layer_price = (resistance_price * (1 + ratio)) * (2 * layerSpread)
|
||||||
|
ratio: 1.5%
|
||||||
|
numOfLayers: 3
|
||||||
|
layerSpread: 0.4%
|
||||||
|
|
||||||
|
exits:
|
||||||
|
# (0) roiStopLoss is the stop loss percentage of the position ROI (currently the price change)
|
||||||
|
- roiStopLoss:
|
||||||
|
percentage: 0.8%
|
||||||
|
|
||||||
|
# (1) roiTakeProfit is used to force taking profit by percentage of the position ROI (currently the price change)
|
||||||
|
# force to take the profit ROI exceeded the percentage.
|
||||||
|
- roiTakeProfit:
|
||||||
|
percentage: 35%
|
||||||
|
|
||||||
|
# (2) protective stop loss -- short term
|
||||||
|
- protectiveStopLoss:
|
||||||
|
activationRatio: 0.6%
|
||||||
|
stopLossRatio: 0.1%
|
||||||
|
placeStopOrder: false
|
||||||
|
|
||||||
|
# (3) protective stop loss -- long term
|
||||||
|
- protectiveStopLoss:
|
||||||
|
activationRatio: 5%
|
||||||
|
stopLossRatio: 1%
|
||||||
|
placeStopOrder: false
|
||||||
|
|
||||||
|
# (4) lowerShadowTakeProfit is used to taking profit when the (lower shadow height / low price) > lowerShadowRatio
|
||||||
|
# you can grab a simple stats by the following SQL:
|
||||||
|
# SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20;
|
||||||
|
- lowerShadowTakeProfit:
|
||||||
|
interval: 30m
|
||||||
|
window: 99
|
||||||
|
ratio: 3%
|
||||||
|
|
||||||
|
# (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold
|
||||||
|
- cumulatedVolumeTakeProfit:
|
||||||
|
interval: 5m
|
||||||
|
window: 2
|
||||||
|
minQuoteVolume: 200_000_000
|
||||||
|
|
||||||
|
- trailingStop:
|
||||||
|
callbackRate: 3%
|
||||||
|
|
||||||
|
# activationRatio is relative to the average cost,
|
||||||
|
# when side is buy, 1% means lower 1% than the average cost.
|
||||||
|
# when side is sell, 1% means higher 1% than the average cost.
|
||||||
|
activationRatio: 40%
|
||||||
|
|
||||||
|
# minProfit uses the position ROI to calculate the profit ratio
|
||||||
|
# minProfit: 1%
|
||||||
|
|
||||||
|
interval: 1m
|
||||||
|
side: buy
|
||||||
|
closePosition: 100%
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
startTime: "2022-01-01"
|
||||||
|
endTime: "2022-06-18"
|
||||||
|
symbols:
|
||||||
|
- ETHUSDT
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
ETH: 10.0
|
||||||
|
USDT: 5000.0
|
65
config/pivotshort_optimizer.yaml
Normal file
65
config/pivotshort_optimizer.yaml
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# usage:
|
||||||
|
#
|
||||||
|
# go run ./cmd/qbtrade optimize --config config/pivotshort.yaml --optimizer-config config/pivotshort_optimizer.yaml --debug
|
||||||
|
#
|
||||||
|
---
|
||||||
|
executor:
|
||||||
|
type: local
|
||||||
|
local:
|
||||||
|
maxNumberOfProcesses: 10
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
|
||||||
|
- type: iterate
|
||||||
|
label: interval
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/interval'
|
||||||
|
values: [ "1m", "5m", "30m" ]
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/window'
|
||||||
|
label: window
|
||||||
|
min: 100.0
|
||||||
|
max: 200.0
|
||||||
|
step: 20.0
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/breakLow/stopEMARange'
|
||||||
|
label: stopEMARange
|
||||||
|
min: 0%
|
||||||
|
max: 10%
|
||||||
|
step: 1%
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/exits/0/roiStopLoss/percentage'
|
||||||
|
label: roiStopLossPercentage
|
||||||
|
min: 0.5%
|
||||||
|
max: 2%
|
||||||
|
step: 0.1%
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/exits/1/roiTakeProfit/percentage'
|
||||||
|
label: roiTakeProfit
|
||||||
|
min: 10%
|
||||||
|
max: 40%
|
||||||
|
step: 5%
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/exits/2/protectiveStopLoss/activationRatio'
|
||||||
|
label: protectiveStopLoss_activationRatio
|
||||||
|
min: 0.5%
|
||||||
|
max: 3%
|
||||||
|
step: 0.1%
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/exits/4/lowerShadowTakeProfit/ratio'
|
||||||
|
label: lowerShadowTakeProfit_ratio
|
||||||
|
min: 1%
|
||||||
|
max: 10%
|
||||||
|
step: 1%
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/exits/5/cumulatedVolumeTakeProfit/minQuoteVolume'
|
||||||
|
label: cumulatedVolumeTakeProfit_minQuoteVolume
|
||||||
|
min: 3_000_000
|
||||||
|
max: 20_000_000
|
||||||
|
step: 100_000
|
12
config/pricealert-tg.yaml
Normal file
12
config/pricealert-tg.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
pricealert:
|
||||||
|
symbol: "BTCUSDT"
|
||||||
|
interval: "1m"
|
||||||
|
minChange: 300
|
22
config/pricealert.yaml
Normal file
22
config/pricealert.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
pricealert:
|
||||||
|
symbol: "BTCUSDT"
|
||||||
|
interval: "1m"
|
||||||
|
minChange: 0.01
|
55
config/pricedrop.yaml
Normal file
55
config/pricedrop.yaml
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
riskControls:
|
||||||
|
# This is the session-based risk controller, which let you configure different risk controller by session.
|
||||||
|
sessionBased:
|
||||||
|
# "max" is the session name that you want to configure the risk control
|
||||||
|
max:
|
||||||
|
# orderExecutors is one of the risk control
|
||||||
|
orderExecutor:
|
||||||
|
# symbol-routed order executor
|
||||||
|
bySymbol:
|
||||||
|
BTCUSDT:
|
||||||
|
# basic risk control order executor
|
||||||
|
basic:
|
||||||
|
minQuoteBalance: 1000.0
|
||||||
|
maxBaseAssetBalance: 2.0
|
||||||
|
minBaseAssetBalance: 0.1
|
||||||
|
maxOrderAmount: 100.0
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
# for testing max draw down (MDD) at 03-12
|
||||||
|
# see here for more details
|
||||||
|
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
|
||||||
|
startTime: "2022-01-01"
|
||||||
|
endTime: "2022-01-15"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
account:
|
||||||
|
binance:
|
||||||
|
makerFeeRate: 0.075%
|
||||||
|
takerFeeRate: 0.075%
|
||||||
|
balances:
|
||||||
|
BTC: 0.1
|
||||||
|
USDT: 10000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
pricedrop:
|
||||||
|
symbol: "BTCUSDT"
|
||||||
|
interval: "1m"
|
||||||
|
baseQuantity: 0.001
|
||||||
|
minDropPercentage: -0.01
|
10
config/random.yaml
Normal file
10
config/random.yaml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
random:
|
||||||
|
symbol: USDCUSDT
|
||||||
|
# https://pkg.go.dev/github.com/robfig/cron#hdr-Predefined_schedules
|
||||||
|
schedule: "@every 8h"
|
||||||
|
quantity: 8
|
||||||
|
onStart: true
|
||||||
|
dryRun: true
|
17
config/rebalance.yaml
Normal file
17
config/rebalance.yaml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
rebalance:
|
||||||
|
schedule: "@every 1s"
|
||||||
|
quoteCurrency: TWD
|
||||||
|
targetWeights:
|
||||||
|
BTC: 60%
|
||||||
|
ETH: 30%
|
||||||
|
TWD: 10%
|
||||||
|
threshold: 1%
|
||||||
|
maxAmount: 1_000 # max amount to buy or sell per order
|
||||||
|
orderType: LIMIT_MAKER # LIMIT, LIMIT_MAKER or MARKET
|
||||||
|
priceType: MAKER # LAST, MID, TAKER or MAKER
|
||||||
|
balanceType: TOTAL
|
||||||
|
dryRun: true
|
||||||
|
onStart: true
|
54
config/rsicross.yaml
Normal file
54
config/rsicross.yaml
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
rsicross:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
interval: 5m
|
||||||
|
fastWindow: 6
|
||||||
|
slowWindow: 18
|
||||||
|
openBelow: 30.0
|
||||||
|
closeAbove: 70.0
|
||||||
|
quantity: 0.1
|
||||||
|
|
||||||
|
### RISK CONTROLS
|
||||||
|
## circuitBreakEMA is used for calculating the price for circuitBreak
|
||||||
|
# circuitBreakEMA:
|
||||||
|
# interval: 1m
|
||||||
|
# window: 14
|
||||||
|
|
||||||
|
## circuitBreakLossThreshold is the maximum loss threshold for realized+unrealized PnL
|
||||||
|
# circuitBreakLossThreshold: -10.0
|
||||||
|
|
||||||
|
## positionHardLimit is the maximum position limit
|
||||||
|
# positionHardLimit: 500.0
|
||||||
|
|
||||||
|
## maxPositionQuantity is the maximum quantity per order that could be controlled in positionHardLimit,
|
||||||
|
## this parameter is used with positionHardLimit togerther
|
||||||
|
# maxPositionQuantity: 10.0
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
startTime: "2022-01-01"
|
||||||
|
endTime: "2022-03-01"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions: [binance]
|
||||||
|
# syncSecKLines: true
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
makerFeeRate: 0.0%
|
||||||
|
takerFeeRate: 0.075%
|
||||||
|
balances:
|
||||||
|
BTC: 0.0
|
||||||
|
USDT: 10_000.0
|
101
config/rsmaker.yaml
Normal file
101
config/rsmaker.yaml
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
---
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sync:
|
||||||
|
# userDataStream is used to sync the trading data in real-time
|
||||||
|
# it uses the websocket connection to insert the trades
|
||||||
|
userDataStream:
|
||||||
|
trades: true
|
||||||
|
filledOrders: true
|
||||||
|
|
||||||
|
# since is the start date of your trading data
|
||||||
|
since: 2021-08-01
|
||||||
|
|
||||||
|
# sessions is the list of session names you want to sync
|
||||||
|
# by default, qbtrade sync all your available sessions.
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
|
||||||
|
# symbols is the list of symbols you want to sync
|
||||||
|
# by default, qbtrade try to guess your symbols by your existing account balances.
|
||||||
|
symbols:
|
||||||
|
- NEARBUSD
|
||||||
|
- BTCUSDT
|
||||||
|
- ETHUSDT
|
||||||
|
- LINKUSDT
|
||||||
|
- BNBUSDT
|
||||||
|
- DOTUSDT
|
||||||
|
- DOTBUSD
|
||||||
|
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
# futures: true
|
||||||
|
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
rsmaker:
|
||||||
|
symbol: BTCBUSD
|
||||||
|
interval: 1m
|
||||||
|
# quantity: 40
|
||||||
|
amount: 20
|
||||||
|
minProfitSpread: 0.1%
|
||||||
|
|
||||||
|
# uptrendSkew: 0.7
|
||||||
|
|
||||||
|
# downtrendSkew, like the strongDowntrendSkew, but the price is still in the default band.
|
||||||
|
# downtrendSkew: 1.3
|
||||||
|
|
||||||
|
# tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band.
|
||||||
|
# tradeInBand: true
|
||||||
|
|
||||||
|
# buyBelowNeutralSMA: when this set, it will only place buy order when the current price is below the SMA line.
|
||||||
|
# buyBelowNeutralSMA: true
|
||||||
|
|
||||||
|
defaultBollinger:
|
||||||
|
interval: "1h"
|
||||||
|
window: 21
|
||||||
|
bandWidth: 2.0
|
||||||
|
|
||||||
|
# neutralBollinger is the smaller range of the bollinger band
|
||||||
|
# If price is in this band, it usually means the price is oscillating.
|
||||||
|
neutralBollinger:
|
||||||
|
interval: "5m"
|
||||||
|
window: 21
|
||||||
|
bandWidth: 2.0
|
||||||
|
|
||||||
|
dynamicExposurePositionScale:
|
||||||
|
byPercentage:
|
||||||
|
# exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
|
||||||
|
exp:
|
||||||
|
# from lower band -100% (-1) to upper band 100% (+1)
|
||||||
|
domain: [ -2, 2 ]
|
||||||
|
# when in down band, holds 1.0 by maximum
|
||||||
|
# when in up band, holds 0.05 by maximum
|
||||||
|
range: [ 1, 0.01 ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
# for testing max draw down (MDD) at 03-12
|
||||||
|
# see here for more details
|
||||||
|
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
|
||||||
|
startTime: "2022-03-26"
|
||||||
|
endTime: "2022-04-12"
|
||||||
|
symbols:
|
||||||
|
- BTCBUSD
|
||||||
|
account:
|
||||||
|
binance:
|
||||||
|
makerFeeRate: 0.0
|
||||||
|
balances:
|
||||||
|
BTC: 1
|
||||||
|
BUSD: 45_000.0
|
33
config/schedule-USDTTWD.yaml
Normal file
33
config/schedule-USDTTWD.yaml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- max
|
||||||
|
startTime: "2022-01-01"
|
||||||
|
endTime: "2022-06-18"
|
||||||
|
symbols:
|
||||||
|
- USDTTWD
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
TWD: 280_000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
schedule:
|
||||||
|
interval: 1m
|
||||||
|
symbol: USDTTWD
|
||||||
|
side: buy
|
||||||
|
amount: 500
|
||||||
|
|
||||||
|
aboveMovingAverage:
|
||||||
|
type: EWMA
|
||||||
|
interval: 1h
|
||||||
|
window: 99
|
||||||
|
side: sell
|
||||||
|
|
||||||
|
belowMovingAverage:
|
||||||
|
type: EWMA
|
||||||
|
interval: 1h
|
||||||
|
window: 99
|
||||||
|
side: buy
|
||||||
|
|
30
config/schedule-btcusdt.yaml
Normal file
30
config/schedule-btcusdt.yaml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
riskControls:
|
||||||
|
# This is the session-based risk controller, which let you configure different risk controller by session.
|
||||||
|
sessionBased:
|
||||||
|
# "max" is the session name that you want to configure the risk control
|
||||||
|
max:
|
||||||
|
# orderExecutor is one of the risk control
|
||||||
|
orderExecutor:
|
||||||
|
# symbol-routed order executor
|
||||||
|
bySymbol:
|
||||||
|
BTCUSDT:
|
||||||
|
# basic risk control order executor
|
||||||
|
basic:
|
||||||
|
minQuoteBalance: 1000.0
|
||||||
|
maxBaseAssetBalance: 500.0
|
||||||
|
minBaseAssetBalance: 300.0
|
||||||
|
maxOrderAmount: 1000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: max
|
||||||
|
schedule:
|
||||||
|
interval: 1h
|
||||||
|
symbol: BTCUSDT
|
||||||
|
side: buy
|
||||||
|
quantity: 0.001
|
||||||
|
belowMovingAverage:
|
||||||
|
type: EWMA
|
||||||
|
interval: 1h
|
||||||
|
window: 99
|
83
config/schedule-ethusdt.yaml
Normal file
83
config/schedule-ethusdt.yaml
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
# time godotenv -f .env.local -- go run ./cmd/qbtrade backtest --base-asset-baseline --config config/schedule-ethusdt.yaml -v
|
||||||
|
backtest:
|
||||||
|
startTime: "2021-08-01"
|
||||||
|
endTime: "2021-08-07"
|
||||||
|
symbols:
|
||||||
|
- ETHUSDT
|
||||||
|
account:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
ETH: 1.0
|
||||||
|
USDT: 20_000.0
|
||||||
|
|
||||||
|
riskControls:
|
||||||
|
# This is the session-based risk controller, which let you configure different risk controller by session.
|
||||||
|
sessionBased:
|
||||||
|
# "max" is the session name that you want to configure the risk control
|
||||||
|
max:
|
||||||
|
# orderExecutor is one of the risk control
|
||||||
|
orderExecutor:
|
||||||
|
# symbol-routed order executor
|
||||||
|
bySymbol:
|
||||||
|
ETHUSDT:
|
||||||
|
# basic risk control order executor
|
||||||
|
basic:
|
||||||
|
minQuoteBalance: 1000.0
|
||||||
|
maxBaseAssetBalance: 500.0
|
||||||
|
minBaseAssetBalance: 300.0
|
||||||
|
maxOrderAmount: 1000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
schedule:
|
||||||
|
# trigger schedule per hour
|
||||||
|
# valid intervals are: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
|
||||||
|
interval: 1h
|
||||||
|
|
||||||
|
symbol: ETHUSDT
|
||||||
|
side: buy
|
||||||
|
|
||||||
|
# quantity is the quantity of the crypto (in base currency) you want to buy/sell
|
||||||
|
# quantity: 0.01
|
||||||
|
|
||||||
|
# amount is the quote quantity of the crypto (in quote currency), here is USDT.
|
||||||
|
# 11.0 means you want to buy ETH with 11 USDT.
|
||||||
|
# the quantity will be calculated automatically, according to the latest price
|
||||||
|
amount: 11.0
|
||||||
|
|
||||||
|
# belowMovingAverage is a special override (optional)
|
||||||
|
# execute order only when the closed price is below the moving average line.
|
||||||
|
# you can open the app to adjust your parameters here.
|
||||||
|
# the interval here could be different from the triggering interval.
|
||||||
|
belowMovingAverage:
|
||||||
|
type: EWMA
|
||||||
|
interval: 1h
|
||||||
|
window: 99
|
||||||
|
|
||||||
|
# you can override the default side
|
||||||
|
side: buy
|
||||||
|
|
||||||
|
# you can choose one of quantity or amount
|
||||||
|
# quantity: 0.05
|
||||||
|
# amount is how much quote balance you want to buy
|
||||||
|
# here 11.0 means you want to buy ETH with 11.0 USDT
|
||||||
|
# please note that crypto exchange requires you to submit an order above the min notional limit $10 usdt
|
||||||
|
amount: 11.0
|
||||||
|
|
||||||
|
# aboveMovingAverage is a special override (optional)
|
||||||
|
# aboveMovingAverage:
|
||||||
|
# type: EWMA
|
||||||
|
# interval: 1h
|
||||||
|
# window: 99
|
||||||
|
# side: sell
|
||||||
|
# # quantity: 0.05
|
||||||
|
# amount: 11.0
|
||||||
|
|
33
config/schedule.yaml
Normal file
33
config/schedule.yaml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
startTime: "2022-01-01"
|
||||||
|
endTime: "2022-06-18"
|
||||||
|
symbols:
|
||||||
|
- ETHUSDT
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
USDT: 20_000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
schedule:
|
||||||
|
interval: 1h
|
||||||
|
symbol: ETHUSDT
|
||||||
|
side: buy
|
||||||
|
amount: 20
|
||||||
|
|
||||||
|
aboveMovingAverage:
|
||||||
|
type: EWMA
|
||||||
|
interval: 1h
|
||||||
|
window: 99
|
||||||
|
side: sell
|
||||||
|
|
||||||
|
belowMovingAverage:
|
||||||
|
type: EWMA
|
||||||
|
interval: 1h
|
||||||
|
window: 99
|
||||||
|
side: buy
|
||||||
|
|
80
config/scmaker.yaml
Normal file
80
config/scmaker.yaml
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
makerFeeRate: 0%
|
||||||
|
takerFeeRate: 0.025%
|
||||||
|
|
||||||
|
#services:
|
||||||
|
# googleSpreadSheet:
|
||||||
|
# jsonTokenFile: ".credentials/google-cloud/service-account-json-token.json"
|
||||||
|
# spreadSheetId: "YOUR_SPREADSHEET_ID"
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
scmaker:
|
||||||
|
symbol: &symbol USDCUSDT
|
||||||
|
|
||||||
|
## adjustmentUpdateInterval is the interval for adjusting position
|
||||||
|
adjustmentUpdateInterval: 1m
|
||||||
|
|
||||||
|
## liquidityUpdateInterval is the interval for updating liquidity orders
|
||||||
|
liquidityUpdateInterval: 1h
|
||||||
|
liquiditySkew: 1.5
|
||||||
|
numOfLiquidityLayers: 10
|
||||||
|
liquidityLayerTickSize: 0.0001
|
||||||
|
liquidityScale:
|
||||||
|
exp:
|
||||||
|
domain: [0, 9]
|
||||||
|
range: [1, 4]
|
||||||
|
|
||||||
|
|
||||||
|
## maxExposure controls how much balance should be used for placing the maker orders
|
||||||
|
maxExposure: 10_000
|
||||||
|
|
||||||
|
## circuitBreakEMA is used for calculating the price for circuitBreak
|
||||||
|
circuitBreakEMA:
|
||||||
|
interval: 1m
|
||||||
|
window: 14
|
||||||
|
|
||||||
|
## circuitBreakLossThreshold is the maximum loss threshold for realized+unrealized PnL
|
||||||
|
circuitBreakLossThreshold: -10.0
|
||||||
|
|
||||||
|
## positionHardLimit is the maximum position limit
|
||||||
|
positionHardLimit: 500.0
|
||||||
|
|
||||||
|
## maxPositionQuantity is the maximum quantity per order that could be controlled in positionHardLimit,
|
||||||
|
## this parameter is used with positionHardLimit togerther
|
||||||
|
maxPositionQuantity: 10.0
|
||||||
|
|
||||||
|
|
||||||
|
midPriceEMA:
|
||||||
|
interval: 1h
|
||||||
|
window: 99
|
||||||
|
|
||||||
|
## priceRangeBollinger is used for the liquidity price range
|
||||||
|
priceRangeBollinger:
|
||||||
|
interval: 1h
|
||||||
|
window: 10
|
||||||
|
k: 1.0
|
||||||
|
|
||||||
|
|
||||||
|
strengthInterval: 1m
|
||||||
|
|
||||||
|
minProfit: 0.01%
|
||||||
|
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- max
|
||||||
|
startTime: "2023-05-20"
|
||||||
|
endTime: "2023-06-01"
|
||||||
|
symbols:
|
||||||
|
- *symbol
|
||||||
|
account:
|
||||||
|
max:
|
||||||
|
makerFeeRate: 0.0%
|
||||||
|
takerFeeRate: 0.025%
|
||||||
|
balances:
|
||||||
|
USDC: 5000
|
||||||
|
USDT: 5000
|
24
config/skeleton.yaml
Normal file
24
config/skeleton.yaml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
heikinAshi: true
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
skeleton:
|
||||||
|
symbol: BNBBUSD
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
startTime: "2022-06-14"
|
||||||
|
endTime: "2022-06-15"
|
||||||
|
symbols:
|
||||||
|
- BNBBUSD
|
||||||
|
sessions: [binance]
|
||||||
|
account:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
BNB: 0
|
||||||
|
BUSD: 10000
|
124
config/supertrend.yaml
Normal file
124
config/supertrend.yaml
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
---
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
margin: true
|
||||||
|
isolatedMargin: true
|
||||||
|
isolatedMarginSymbol: BTCUSDT
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions: [binance]
|
||||||
|
# for testing max draw down (MDD) at 03-12
|
||||||
|
# see here for more details
|
||||||
|
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
|
||||||
|
startTime: "2022-05-01"
|
||||||
|
endTime: "2022-10-31"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
makerCommission: 10 # 0.15%
|
||||||
|
takerCommission: 15 # 0.15%
|
||||||
|
balances:
|
||||||
|
BTC: 50.0
|
||||||
|
USDT: 10000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
supertrend:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
|
||||||
|
# interval is how long do you want to update your order price and quantity
|
||||||
|
interval: 1m
|
||||||
|
|
||||||
|
# ATR window used by Supertrend
|
||||||
|
window: 220
|
||||||
|
# ATR Multiplier for calculating super trend prices, the higher, the stronger the trends are
|
||||||
|
supertrendMultiplier: 10
|
||||||
|
|
||||||
|
# leverage uses the account net value to calculate the order qty
|
||||||
|
leverage: 1.0
|
||||||
|
# quantity sets the fixed order qty, takes precedence over Leverage
|
||||||
|
#quantity: 0.5
|
||||||
|
|
||||||
|
# fastDEMAWindow and slowDEMAWindow are for filtering super trend noise
|
||||||
|
fastDEMAWindow: 28
|
||||||
|
slowDEMAWindow: 170
|
||||||
|
|
||||||
|
# Use linear regression as trend confirmation
|
||||||
|
linearRegression:
|
||||||
|
interval: 1m
|
||||||
|
window: 18
|
||||||
|
|
||||||
|
# TP according to ATR multiple, 0 to disable this
|
||||||
|
TakeProfitAtrMultiplier: 0
|
||||||
|
|
||||||
|
# Set SL price to the low of the triggering Kline
|
||||||
|
stopLossByTriggeringK: false
|
||||||
|
|
||||||
|
# TP/SL by reversed supertrend signal
|
||||||
|
stopByReversedSupertrend: false
|
||||||
|
|
||||||
|
# TP/SL by reversed DEMA signal
|
||||||
|
stopByReversedDema: false
|
||||||
|
|
||||||
|
# TP/SL by reversed linear regression signal
|
||||||
|
stopByReversedLinGre: false
|
||||||
|
|
||||||
|
# Draw pnl
|
||||||
|
drawGraph: true
|
||||||
|
graphPNLPath: "./pnl.png"
|
||||||
|
graphCumPNLPath: "./cumpnl.png"
|
||||||
|
|
||||||
|
exits:
|
||||||
|
# roiStopLoss is the stop loss percentage of the position ROI (currently the price change)
|
||||||
|
- roiStopLoss:
|
||||||
|
percentage: 2%
|
||||||
|
- trailingStop:
|
||||||
|
callbackRate: 2%
|
||||||
|
#activationRatio: 20%
|
||||||
|
minProfit: 10%
|
||||||
|
interval: 1m
|
||||||
|
side: both
|
||||||
|
closePosition: 100%
|
||||||
|
- higherHighLowerLowStopLoss:
|
||||||
|
# interval is the kline interval used by this exit
|
||||||
|
interval: 15m
|
||||||
|
# window is used as the range to determining higher highs and lower lows
|
||||||
|
window: 5
|
||||||
|
# highLowWindow is the range to calculate the number of higher highs and lower lows
|
||||||
|
highLowWindow: 12
|
||||||
|
# If the number of higher highs or lower lows with in HighLowWindow is less than MinHighLow, the exit is
|
||||||
|
# triggered. 0 disables this parameter. Either one of MaxHighLow and MinHighLow must be larger than 0
|
||||||
|
minHighLow: 2
|
||||||
|
# If the number of higher highs or lower lows with in HighLowWindow is more than MaxHighLow, the exit is
|
||||||
|
# triggered. 0 disables this parameter. Either one of MaxHighLow and MinHighLow must be larger than 0
|
||||||
|
maxHighLow: 0
|
||||||
|
# ActivationRatio is the trigger condition
|
||||||
|
# When the price goes higher (lower for short position) than this ratio, the stop will be activated.
|
||||||
|
# You can use this to combine several exits
|
||||||
|
activationRatio: 0.5%
|
||||||
|
# DeactivationRatio is the kill condition
|
||||||
|
# When the price goes higher (lower for short position) than this ratio, the stop will be deactivated.
|
||||||
|
# You can use this to combine several exits
|
||||||
|
deactivationRatio: 10%
|
||||||
|
# If true, looking for lower lows in long position and higher highs in short position. If false, looking for
|
||||||
|
# higher highs in long position and lower lows in short position
|
||||||
|
oppositeDirectionAsPosition: false
|
||||||
|
|
||||||
|
profitStatsTracker:
|
||||||
|
interval: 1d
|
||||||
|
window: 30
|
||||||
|
accumulatedProfitReport:
|
||||||
|
profitMAWindow: 60
|
||||||
|
shortTermProfitWindow: 14
|
||||||
|
accumulateTradeWindow: 30
|
||||||
|
tsvReportPath: res.tsv
|
||||||
|
trackParameters: false
|
70
config/support-margin.yaml
Normal file
70
config/support-margin.yaml
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance_margin_linkusdt:
|
||||||
|
exchange: binance
|
||||||
|
margin: true
|
||||||
|
isolatedMargin: true
|
||||||
|
isolatedMarginSymbol: LINKUSDT
|
||||||
|
|
||||||
|
riskControls:
|
||||||
|
# This is the session-based risk controller, which let you configure different risk controller by session.
|
||||||
|
sessionBased:
|
||||||
|
max:
|
||||||
|
orderExecutor:
|
||||||
|
bySymbol:
|
||||||
|
BTCUSDT:
|
||||||
|
basic:
|
||||||
|
minQuoteBalance: 100.0
|
||||||
|
maxBaseAssetBalance: 3.0
|
||||||
|
minBaseAssetBalance: 0.0
|
||||||
|
maxOrderAmount: 1000.0
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
# for testing max draw down (MDD) at 03-12
|
||||||
|
# see here for more details
|
||||||
|
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
|
||||||
|
startTime: "2020-09-04"
|
||||||
|
endTime: "2020-09-14"
|
||||||
|
symbols:
|
||||||
|
- LINKUSDT
|
||||||
|
account:
|
||||||
|
binance:
|
||||||
|
makerFeeRate: 0.075%
|
||||||
|
takerFeeRate: 0.075%
|
||||||
|
balances:
|
||||||
|
LINK: 0.0
|
||||||
|
USDT: 10000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance_margin_linkusdt
|
||||||
|
support:
|
||||||
|
symbol: LINKUSDT
|
||||||
|
interval: 1m
|
||||||
|
minVolume: 2_000
|
||||||
|
marginOrderSideEffect: borrow
|
||||||
|
|
||||||
|
scaleQuantity:
|
||||||
|
byVolume:
|
||||||
|
exp:
|
||||||
|
domain: [ 1_000, 200_000 ]
|
||||||
|
range: [ 3.0, 5.0 ]
|
||||||
|
|
||||||
|
maxBaseAssetBalance: 1000.0
|
||||||
|
minQuoteAssetBalance: 2000.0
|
||||||
|
|
||||||
|
targets:
|
||||||
|
- profitPercentage: 0.02
|
||||||
|
quantityPercentage: 0.5
|
||||||
|
marginOrderSideEffect: repay
|
||||||
|
|
56
config/support.yaml
Normal file
56
config/support.yaml
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
startTime: "2021-09-01"
|
||||||
|
endTime: "2021-11-01"
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
symbols:
|
||||||
|
- LINKUSDT
|
||||||
|
account:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
USDT: 10000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
support:
|
||||||
|
symbol: LINKUSDT
|
||||||
|
interval: 5m
|
||||||
|
minVolume: 80_000
|
||||||
|
triggerMovingAverage:
|
||||||
|
interval: 5m
|
||||||
|
window: 99
|
||||||
|
longTermMovingAverage:
|
||||||
|
interval: 1h
|
||||||
|
window: 99
|
||||||
|
|
||||||
|
scaleQuantity:
|
||||||
|
byVolume:
|
||||||
|
exp:
|
||||||
|
domain: [ 10_000, 200_000 ]
|
||||||
|
range: [ 0.5, 1.0 ]
|
||||||
|
|
||||||
|
maxBaseAssetBalance: 1000.0
|
||||||
|
minQuoteAssetBalance: 2000.0
|
||||||
|
|
||||||
|
trailingStopTarget:
|
||||||
|
callbackRatio: 1.5%
|
||||||
|
minimumProfitPercentage: 2%
|
||||||
|
|
||||||
|
targets:
|
||||||
|
- profitPercentage: 0.02
|
||||||
|
quantityPercentage: 0.5
|
43
config/swing.yaml
Normal file
43
config/swing.yaml
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
riskControls:
|
||||||
|
# This is the session-based risk controller, which let you configure different risk controller by session.
|
||||||
|
sessionBased:
|
||||||
|
# "max" is the session name that you want to configure the risk control
|
||||||
|
binance:
|
||||||
|
# orderExecutor is one of the risk control
|
||||||
|
orderExecutor:
|
||||||
|
# symbol-routed order executor
|
||||||
|
bySymbol:
|
||||||
|
BNBUSDT:
|
||||||
|
# basic risk control order executor
|
||||||
|
basic:
|
||||||
|
minQuoteBalance: 1000.0
|
||||||
|
maxBaseAssetBalance: 50.0
|
||||||
|
minBaseAssetBalance: 10.0
|
||||||
|
maxOrderAmount: 100.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
swing:
|
||||||
|
symbol: BNBUSDT
|
||||||
|
interval: 1m
|
||||||
|
minChange: 0.01
|
||||||
|
baseQuantity: 1.0
|
||||||
|
movingAverageType: EWMA
|
||||||
|
movingAverageInterval: 1m
|
||||||
|
movingAverageWindow: 99
|
64
config/sync.yaml
Normal file
64
config/sync.yaml
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
binance_margin_dotusdt:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
margin: true
|
||||||
|
isolatedMargin: true
|
||||||
|
isolatedMarginSymbol: DOTUSDT
|
||||||
|
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
|
||||||
|
kucoin:
|
||||||
|
exchange: kucoin
|
||||||
|
envVarPrefix: kucoin
|
||||||
|
|
||||||
|
okex:
|
||||||
|
exchange: okex
|
||||||
|
envVarPrefix: okex
|
||||||
|
|
||||||
|
sync:
|
||||||
|
# userDataStream is used to sync the trading data in real-time
|
||||||
|
# it uses the websocket connection to insert the trades
|
||||||
|
userDataStream:
|
||||||
|
trades: true
|
||||||
|
filledOrders: true
|
||||||
|
|
||||||
|
# since is the start date of your trading data
|
||||||
|
since: 2019-01-01
|
||||||
|
|
||||||
|
# sessions is the list of session names you want to sync
|
||||||
|
# by default, qbtrade sync all your available sessions.
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
- binance_margin_dotusdt
|
||||||
|
- max
|
||||||
|
- okex
|
||||||
|
- kucoin
|
||||||
|
|
||||||
|
# symbols is the list of symbols you want to sync
|
||||||
|
# by default, qbtrade try to guess your symbols by your existing account balances.
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
- ETHUSDT
|
||||||
|
- DOTUSDT
|
||||||
|
- binance:BNBUSDT
|
||||||
|
- max:USDTTWD
|
||||||
|
|
||||||
|
# marginHistory enables the margin history sync
|
||||||
|
marginHistory: true
|
||||||
|
|
||||||
|
# marginAssets lists the assets that are used in the margin.
|
||||||
|
# including loan, repay, interest and liquidation
|
||||||
|
marginAssets:
|
||||||
|
- USDT
|
||||||
|
|
||||||
|
depositHistory: true
|
||||||
|
rewardHistory: true
|
||||||
|
withdrawHistory: true
|
50
config/trendtrader.yaml
Normal file
50
config/trendtrader.yaml
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
# futures: true
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: binance
|
||||||
|
trendtrader:
|
||||||
|
symbol: BTCBUSD
|
||||||
|
trendLine:
|
||||||
|
interval: 30m
|
||||||
|
pivotRightWindow: 40
|
||||||
|
quantity: 1
|
||||||
|
exits:
|
||||||
|
- trailingStop:
|
||||||
|
callbackRate: 1%
|
||||||
|
activationRatio: 1%
|
||||||
|
closePosition: 100%
|
||||||
|
minProfit: 15%
|
||||||
|
interval: 1m
|
||||||
|
side: buy
|
||||||
|
- trailingStop:
|
||||||
|
callbackRate: 1%
|
||||||
|
activationRatio: 1%
|
||||||
|
closePosition: 100%
|
||||||
|
minProfit: 15%
|
||||||
|
interval: 1m
|
||||||
|
side: sell
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- binance
|
||||||
|
startTime: "2021-01-01"
|
||||||
|
endTime: "2022-08-31"
|
||||||
|
symbols:
|
||||||
|
- BTCBUSD
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
BTC: 1
|
||||||
|
BUSD: 50_000.0
|
35
config/tri.yaml
Normal file
35
config/tri.yaml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
## triangular arbitrage strategy
|
||||||
|
- on: binance
|
||||||
|
tri:
|
||||||
|
minSpreadRatio: 1.0011
|
||||||
|
separateStream: true
|
||||||
|
# resetPosition: true
|
||||||
|
limits:
|
||||||
|
BTC: 0.001
|
||||||
|
ETH: 0.01
|
||||||
|
USDT: 20.0
|
||||||
|
symbols:
|
||||||
|
- BNBUSDT
|
||||||
|
- BNBBTC
|
||||||
|
- BNBETH
|
||||||
|
- BTCUSDT
|
||||||
|
- ETHUSDT
|
||||||
|
- ETHBTC
|
||||||
|
paths:
|
||||||
|
- [ BTCUSDT, ETHBTC, ETHUSDT ]
|
||||||
|
- [ BNBBTC, BNBUSDT, BTCUSDT ]
|
||||||
|
- [ BNBETH, BNBUSDT, ETHUSDT ]
|
||||||
|
|
46
config/wall.yaml
Normal file
46
config/wall.yaml
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: MAX
|
||||||
|
|
||||||
|
|
||||||
|
logging:
|
||||||
|
trade: true
|
||||||
|
order: true
|
||||||
|
# fields:
|
||||||
|
# env: prod
|
||||||
|
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: max
|
||||||
|
wall:
|
||||||
|
symbol: DOTUSDT
|
||||||
|
|
||||||
|
# interval is how long do you want to update your order price and quantity
|
||||||
|
interval: 1m
|
||||||
|
|
||||||
|
fixedPrice: 2.0
|
||||||
|
|
||||||
|
side: buy
|
||||||
|
|
||||||
|
# quantity is the base order quantity for your buy/sell order.
|
||||||
|
# quantity: 0.05
|
||||||
|
|
||||||
|
numLayers: 3
|
||||||
|
layerSpread: 0.1
|
||||||
|
|
||||||
|
quantityScale:
|
||||||
|
byLayer:
|
||||||
|
linear:
|
||||||
|
domain: [ 1, 3 ]
|
||||||
|
range: [ 10000.0, 30000.0 ]
|
||||||
|
|
||||||
|
|
49
config/xalign.yaml
Normal file
49
config/xalign.yaml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
crossExchangeStrategies:
|
||||||
|
- xalign:
|
||||||
|
interval: 1m
|
||||||
|
sessions:
|
||||||
|
- max
|
||||||
|
- binance
|
||||||
|
|
||||||
|
## quoteCurrencies config specifies which quote currency should be used for BUY order or SELL order.
|
||||||
|
## when specifying [USDC,TWD] for "BUY", then it will consider BTCUSDC first then BTCTWD second.
|
||||||
|
quoteCurrencies:
|
||||||
|
buy: [USDC, TWD]
|
||||||
|
sell: [USDT]
|
||||||
|
expectedBalances:
|
||||||
|
BTC: 0.0440
|
||||||
|
useTakerOrder: false
|
||||||
|
dryRun: true
|
||||||
|
balanceToleranceRange: 10%
|
||||||
|
maxAmounts:
|
||||||
|
USDT: 100
|
||||||
|
USDC: 100
|
||||||
|
TWD: 3000
|
39
config/xbalance.yaml
Normal file
39
config/xbalance.yaml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
#persistence:
|
||||||
|
# json:
|
||||||
|
# directory: var/data
|
||||||
|
# redis:
|
||||||
|
# host: 127.0.0.1
|
||||||
|
# port: 6379
|
||||||
|
# db: 0
|
||||||
|
|
||||||
|
crossExchangeStrategies:
|
||||||
|
|
||||||
|
- xbalance:
|
||||||
|
interval: 1h
|
||||||
|
asset: USDT
|
||||||
|
addresses:
|
||||||
|
binance: your_whitelisted_address
|
||||||
|
max: your_whitelisted_address
|
||||||
|
low: 5000
|
||||||
|
middle: 6000
|
||||||
|
|
67
config/xdepthmaker.yaml
Normal file
67
config/xdepthmaker.yaml
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: false
|
||||||
|
submitOrder: false
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
logging:
|
||||||
|
trade: true
|
||||||
|
order: true
|
||||||
|
fields:
|
||||||
|
env: staging
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
crossExchangeStrategies:
|
||||||
|
|
||||||
|
- xdepthmaker:
|
||||||
|
symbol: "BTCUSDT"
|
||||||
|
makerExchange: max
|
||||||
|
hedgeExchange: binance
|
||||||
|
|
||||||
|
# disableHedge disables the hedge orders on the source exchange
|
||||||
|
# disableHedge: true
|
||||||
|
|
||||||
|
hedgeInterval: 10s
|
||||||
|
notifyTrade: true
|
||||||
|
|
||||||
|
margin: 0.004
|
||||||
|
askMargin: 0.4%
|
||||||
|
bidMargin: 0.4%
|
||||||
|
|
||||||
|
depthScale:
|
||||||
|
byLayer:
|
||||||
|
linear:
|
||||||
|
domain: [1, 30]
|
||||||
|
range: [50, 20_000]
|
||||||
|
|
||||||
|
# numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders
|
||||||
|
numLayers: 30
|
||||||
|
|
||||||
|
# pips is the fraction numbers between each order. for BTC, 1 pip is 0.1,
|
||||||
|
# 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and
|
||||||
|
# 18002.00
|
||||||
|
pips: 10
|
||||||
|
persistence:
|
||||||
|
type: redis
|
||||||
|
|
51
config/xfixedmaker.yaml
Normal file
51
config/xfixedmaker.yaml
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
publicOnly: true
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
startTime: "2023-01-01"
|
||||||
|
endTime: "2023-01-02"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions:
|
||||||
|
- max
|
||||||
|
- binance
|
||||||
|
accounts:
|
||||||
|
max:
|
||||||
|
balances:
|
||||||
|
BTC: 0.5
|
||||||
|
USDT: 15000.0
|
||||||
|
|
||||||
|
crossExchangeStrategies:
|
||||||
|
- xfixedmaker:
|
||||||
|
tradingExchange: max
|
||||||
|
symbol: BTCUSDT
|
||||||
|
interval: 1m
|
||||||
|
halfSpread: 0.01%
|
||||||
|
quantity: 0.005
|
||||||
|
orderType: LIMIT_MAKER
|
||||||
|
dryRun: true
|
||||||
|
|
||||||
|
referenceExchange: binance
|
||||||
|
referencePriceEMA:
|
||||||
|
interval: 1m
|
||||||
|
window: 14
|
||||||
|
orderPriceLossThreshold: -10
|
||||||
|
|
||||||
|
positionHardLimit: 200
|
||||||
|
maxPositionQuantity: 0.005
|
||||||
|
|
||||||
|
circuitBreakLossThreshold: -10
|
||||||
|
circuitBreakEMA:
|
||||||
|
interval: 1m
|
||||||
|
window: 14
|
||||||
|
|
||||||
|
inventorySkew:
|
||||||
|
inventoryRangeMultiplier: 1.0
|
||||||
|
targetBaseRatio: 0.5
|
64
config/xfunding.yaml
Normal file
64
config/xfunding.yaml
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 1
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: BINANCE
|
||||||
|
|
||||||
|
binance_futures:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: BINANCE
|
||||||
|
futures: true
|
||||||
|
|
||||||
|
crossExchangeStrategies:
|
||||||
|
|
||||||
|
- xfunding:
|
||||||
|
spotSession: binance
|
||||||
|
futuresSession: binance_futures
|
||||||
|
|
||||||
|
## symbol is the symbol name of the spot market and the futures market
|
||||||
|
## todo: provide option to separate the futures market symbol
|
||||||
|
symbol: ETHUSDT
|
||||||
|
|
||||||
|
## interval is the interval for checking futures premium and the funding rate
|
||||||
|
interval: 1m
|
||||||
|
|
||||||
|
## leverage is the leverage of the reverse futures position size.
|
||||||
|
## for example, you can buy 1 BTC and short 3 BTC in the futures account with 3x leverage.
|
||||||
|
leverage: 1.0
|
||||||
|
|
||||||
|
## incrementalQuoteQuantity is the quote quantity per maker order when creating the positions
|
||||||
|
## when in BTC-USDT 20 means 20 USDT, each buy order will hold 20 USDT quote amount.
|
||||||
|
incrementalQuoteQuantity: 10
|
||||||
|
|
||||||
|
## quoteInvestment is how much you want to invest to create your position.
|
||||||
|
## for example, when 10k USDT is given as the quote investment, and the average executed price of your position is around BTC 18k
|
||||||
|
## you will be holding around 0.555555 BTC
|
||||||
|
quoteInvestment: 50
|
||||||
|
|
||||||
|
## shortFundingRate is the funding rate range you want to create your position
|
||||||
|
shortFundingRate:
|
||||||
|
## when funding rate is higher than this high value, the strategy will start buying spot and opening a short position
|
||||||
|
high: 0.0001%
|
||||||
|
## when funding rate is lower than this low value, the strategy will start closing futures position and sell the spot
|
||||||
|
low: -0.01%
|
||||||
|
|
||||||
|
## reset will reset the spot/futures positions, the transfer stats and the position state.
|
||||||
|
# reset: true
|
||||||
|
|
||||||
|
# closeFuturesPosition: true
|
41
config/xgap.yaml
Normal file
41
config/xgap.yaml
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
publicOnly: true
|
||||||
|
|
||||||
|
crossExchangeStrategies:
|
||||||
|
|
||||||
|
- xgap:
|
||||||
|
symbol: "ETHUSDT"
|
||||||
|
sourceExchange: binance
|
||||||
|
tradingExchange: max
|
||||||
|
updateInterval: 1m
|
||||||
|
dailyMaxVolume: 100
|
||||||
|
dailyFeeBudgets:
|
||||||
|
MAX: 100
|
||||||
|
persistence:
|
||||||
|
type: redis
|
80
config/xmaker-btcusdt.yaml
Normal file
80
config/xmaker-btcusdt.yaml
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: MAX
|
||||||
|
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: BINANCE
|
||||||
|
|
||||||
|
riskControls:
|
||||||
|
# This is the session-based risk controller, which let you configure different risk controller by session.
|
||||||
|
sessionBased:
|
||||||
|
# "max" is the session name that you want to configure the risk control
|
||||||
|
max:
|
||||||
|
# orderExecutor is one of the risk control
|
||||||
|
orderExecutor:
|
||||||
|
# symbol-routed order executor
|
||||||
|
bySymbol:
|
||||||
|
BTCUSDT:
|
||||||
|
# basic risk control order executor
|
||||||
|
basic:
|
||||||
|
# keep at least X USDT (keep cash)
|
||||||
|
minQuoteBalance: 100.0
|
||||||
|
|
||||||
|
# maximum BTC balance (don't buy too much)
|
||||||
|
maxBaseAssetBalance: 1.0
|
||||||
|
|
||||||
|
# minimum BTC balance (don't sell too much)
|
||||||
|
minBaseAssetBalance: 0.01
|
||||||
|
|
||||||
|
maxOrderAmount: 1000.0
|
||||||
|
|
||||||
|
crossExchangeStrategies:
|
||||||
|
|
||||||
|
- xmaker:
|
||||||
|
symbol: BTCUSDT
|
||||||
|
sourceExchange: binance
|
||||||
|
makerExchange: max
|
||||||
|
updateInterval: 1s
|
||||||
|
|
||||||
|
# disableHedge disables the hedge orders on the source exchange
|
||||||
|
# disableHedge: true
|
||||||
|
|
||||||
|
hedgeInterval: 10s
|
||||||
|
|
||||||
|
margin: 0.004
|
||||||
|
askMargin: 0.004
|
||||||
|
bidMargin: 0.004
|
||||||
|
|
||||||
|
quantity: 0.001
|
||||||
|
quantityMultiplier: 2
|
||||||
|
|
||||||
|
# numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders
|
||||||
|
numLayers: 1
|
||||||
|
# pips is the fraction numbers between each order. for BTC, 1 pip is 0.1,
|
||||||
|
# 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and
|
||||||
|
# 18002.00
|
||||||
|
pips: 10
|
||||||
|
persistence:
|
||||||
|
type: redis
|
||||||
|
|
82
config/xmaker-ethusdt.yaml
Normal file
82
config/xmaker-ethusdt.yaml
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: false
|
||||||
|
submitOrder: false
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
riskControls:
|
||||||
|
# This is the session-based risk controller, which let you configure different risk controller by session.
|
||||||
|
sessionBased:
|
||||||
|
# "max" is the session name that you want to configure the risk control
|
||||||
|
max:
|
||||||
|
# orderExecutor is one of the risk control
|
||||||
|
orderExecutor:
|
||||||
|
# symbol-routed order executor
|
||||||
|
bySymbol:
|
||||||
|
ETHUSDT:
|
||||||
|
# basic risk control order executor
|
||||||
|
basic:
|
||||||
|
# keep at least X USDT (keep cash)
|
||||||
|
minQuoteBalance: 100.0
|
||||||
|
|
||||||
|
# maximum ETH balance (don't buy too much)
|
||||||
|
maxBaseAssetBalance: 10.0
|
||||||
|
|
||||||
|
# minimum ETH balance (don't sell too much)
|
||||||
|
minBaseAssetBalance: 0.0
|
||||||
|
|
||||||
|
maxOrderAmount: 1000.0
|
||||||
|
|
||||||
|
crossExchangeStrategies:
|
||||||
|
|
||||||
|
- xmaker:
|
||||||
|
symbol: ETHUSDT
|
||||||
|
sourceExchange: binance
|
||||||
|
makerExchange: max
|
||||||
|
updateInterval: 2s
|
||||||
|
|
||||||
|
# disableHedge disables the hedge orders on the source exchange
|
||||||
|
# disableHedge: true
|
||||||
|
|
||||||
|
hedgeInterval: 10s
|
||||||
|
|
||||||
|
margin: 0.004
|
||||||
|
askMargin: 0.004
|
||||||
|
bidMargin: 0.004
|
||||||
|
|
||||||
|
quantity: 0.01
|
||||||
|
quantityMultiplier: 2
|
||||||
|
|
||||||
|
# numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders
|
||||||
|
numLayers: 2
|
||||||
|
|
||||||
|
# pips is the fraction numbers between each order. for BTC, 1 pip is 0.1,
|
||||||
|
# 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and
|
||||||
|
# 18002.00
|
||||||
|
pips: 10
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
type: redis
|
||||||
|
|
64
config/xmaker.yaml
Normal file
64
config/xmaker.yaml
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: false
|
||||||
|
submitOrder: false
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
logging:
|
||||||
|
trade: true
|
||||||
|
order: true
|
||||||
|
fields:
|
||||||
|
env: staging
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
crossExchangeStrategies:
|
||||||
|
|
||||||
|
- xmaker:
|
||||||
|
symbol: "BTCUSDT"
|
||||||
|
sourceExchange: binance
|
||||||
|
makerExchange: max
|
||||||
|
updateInterval: 1s
|
||||||
|
|
||||||
|
# disableHedge disables the hedge orders on the source exchange
|
||||||
|
# disableHedge: true
|
||||||
|
|
||||||
|
hedgeInterval: 10s
|
||||||
|
notifyTrade: true
|
||||||
|
|
||||||
|
margin: 0.004
|
||||||
|
askMargin: 0.4%
|
||||||
|
bidMargin: 0.4%
|
||||||
|
|
||||||
|
quantity: 0.001
|
||||||
|
quantityMultiplier: 2
|
||||||
|
|
||||||
|
# numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders
|
||||||
|
numLayers: 1
|
||||||
|
# pips is the fraction numbers between each order. for BTC, 1 pip is 0.1,
|
||||||
|
# 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and
|
||||||
|
# 18002.00
|
||||||
|
pips: 10
|
||||||
|
persistence:
|
||||||
|
type: redis
|
||||||
|
|
36
config/xnav.yaml
Normal file
36
config/xnav.yaml
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "dev-qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
json:
|
||||||
|
directory: var/data
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
crossExchangeStrategies:
|
||||||
|
|
||||||
|
- xnav:
|
||||||
|
interval: 1h
|
||||||
|
# schedule: "0 * * * *" # every hour
|
||||||
|
reportOnStart: true
|
||||||
|
ignoreDusts: true
|
||||||
|
|
28
config/xpuremaker.yaml
Normal file
28
config/xpuremaker.yaml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
defaultChannel: "qbtrade"
|
||||||
|
errorChannel: "qbtrade-error"
|
||||||
|
|
||||||
|
switches:
|
||||||
|
trade: true
|
||||||
|
orderUpdate: true
|
||||||
|
submitOrder: true
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
xpuremaker:
|
||||||
|
symbol: MAXUSDT
|
||||||
|
numOrders: 2
|
||||||
|
side: both
|
||||||
|
behindVolume: 1000.0
|
||||||
|
priceTick: 0.001
|
||||||
|
baseQuantity: 100.0
|
2
go.mod
2
go.mod
|
@ -1,3 +1,3 @@
|
||||||
module git.qtrade.icu/lychiyu/qbtrade
|
module git.qtrade.icu/lychiyu/qbtrade
|
||||||
|
|
||||||
go 1.22
|
go 1.22.0
|
||||||
|
|
236
pkg/accounting/cost_distribution.go
Normal file
236
pkg/accounting/cost_distribution.go
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
package accounting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Stock types.Trade
|
||||||
|
|
||||||
|
func (stock *Stock) String() string {
|
||||||
|
return stock.Price.String() + " (" + stock.Quantity.String() + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stock *Stock) Consume(quantity fixedpoint.Value) fixedpoint.Value {
|
||||||
|
q := fixedpoint.Min(stock.Quantity, quantity)
|
||||||
|
stock.Quantity = stock.Quantity.Sub(q)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockSlice []Stock
|
||||||
|
|
||||||
|
func (slice StockSlice) QuantityBelowPrice(price fixedpoint.Value) (quantity fixedpoint.Value) {
|
||||||
|
for _, stock := range slice {
|
||||||
|
if stock.Price.Compare(price) < 0 {
|
||||||
|
quantity = quantity.Add(stock.Quantity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return quantity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slice StockSlice) Quantity() (total fixedpoint.Value) {
|
||||||
|
for _, stock := range slice {
|
||||||
|
total = total.Add(stock.Quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockDistribution struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
Symbol string
|
||||||
|
TradingFeeCurrency string
|
||||||
|
Stocks StockSlice
|
||||||
|
PendingSells StockSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
type DistributionStats struct {
|
||||||
|
PriceLevels []string `json:"priceLevels"`
|
||||||
|
TotalQuantity fixedpoint.Value `json:"totalQuantity"`
|
||||||
|
Quantities map[string]fixedpoint.Value `json:"quantities"`
|
||||||
|
Stocks map[string]StockSlice `json:"stocks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StockDistribution) DistributionStats(level int) *DistributionStats {
|
||||||
|
var d = DistributionStats{
|
||||||
|
Quantities: map[string]fixedpoint.Value{},
|
||||||
|
Stocks: map[string]StockSlice{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, stock := range m.Stocks {
|
||||||
|
n := math.Ceil(math.Log10(stock.Price.Float64()))
|
||||||
|
digits := int(n - math.Max(float64(level), 1.0))
|
||||||
|
key := stock.Price.Round(-digits, fixedpoint.Down).FormatString(2)
|
||||||
|
|
||||||
|
d.TotalQuantity = d.TotalQuantity.Add(stock.Quantity)
|
||||||
|
d.Stocks[key] = append(d.Stocks[key], stock)
|
||||||
|
d.Quantities[key] = d.Quantities[key].Add(stock.Quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
var priceLevels []float64
|
||||||
|
for priceString := range d.Stocks {
|
||||||
|
price, _ := strconv.ParseFloat(priceString, 32)
|
||||||
|
priceLevels = append(priceLevels, price)
|
||||||
|
}
|
||||||
|
sort.Float64s(priceLevels)
|
||||||
|
|
||||||
|
for _, price := range priceLevels {
|
||||||
|
d.PriceLevels = append(d.PriceLevels, strconv.FormatFloat(price, 'f', 2, 64))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StockDistribution) stock(stock Stock) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
m.Stocks = append(m.Stocks, stock)
|
||||||
|
m.mu.Unlock()
|
||||||
|
return m.flushPendingSells()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StockDistribution) squash() {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
var squashed StockSlice
|
||||||
|
for _, stock := range m.Stocks {
|
||||||
|
if !stock.Quantity.IsZero() {
|
||||||
|
squashed = append(squashed, stock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.Stocks = squashed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StockDistribution) flushPendingSells() error {
|
||||||
|
if len(m.Stocks) == 0 || len(m.PendingSells) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingSells := m.PendingSells
|
||||||
|
m.PendingSells = nil
|
||||||
|
|
||||||
|
for _, sell := range pendingSells {
|
||||||
|
if err := m.consume(sell); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StockDistribution) consume(sell Stock) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if len(m.Stocks) == 0 {
|
||||||
|
m.PendingSells = append(m.PendingSells, sell)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := len(m.Stocks) - 1
|
||||||
|
for ; idx >= 0; idx-- {
|
||||||
|
stock := m.Stocks[idx]
|
||||||
|
|
||||||
|
// find any stock price is lower than the sell trade
|
||||||
|
if stock.Price.Compare(sell.Price) >= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if stock.Quantity.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
delta := stock.Consume(sell.Quantity)
|
||||||
|
sell.Consume(delta)
|
||||||
|
m.Stocks[idx] = stock
|
||||||
|
|
||||||
|
if sell.Quantity.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idx = len(m.Stocks) - 1
|
||||||
|
for ; idx >= 0; idx-- {
|
||||||
|
stock := m.Stocks[idx]
|
||||||
|
|
||||||
|
if stock.Quantity.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
delta := stock.Consume(sell.Quantity)
|
||||||
|
sell.Consume(delta)
|
||||||
|
m.Stocks[idx] = stock
|
||||||
|
|
||||||
|
if sell.Quantity.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sell.Quantity.Sign() > 0 {
|
||||||
|
m.PendingSells = append(m.PendingSells, sell)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StockDistribution) AddTrades(trades []types.Trade) (checkpoints []int, err error) {
|
||||||
|
feeSymbol := strings.HasPrefix(m.Symbol, m.TradingFeeCurrency)
|
||||||
|
for idx, trade := range trades {
|
||||||
|
// for other market trades
|
||||||
|
// convert trading fee trades to sell trade
|
||||||
|
if trade.Symbol != m.Symbol {
|
||||||
|
if feeSymbol && trade.FeeCurrency == m.TradingFeeCurrency {
|
||||||
|
trade.Symbol = m.Symbol
|
||||||
|
trade.IsBuyer = false
|
||||||
|
trade.Quantity = trade.Fee
|
||||||
|
trade.Fee = fixedpoint.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if trade.Symbol != m.Symbol {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if trade.IsBuyer {
|
||||||
|
if idx > 0 && len(m.Stocks) == 0 {
|
||||||
|
checkpoints = append(checkpoints, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
stock := toStock(trade)
|
||||||
|
if err := m.stock(stock); err != nil {
|
||||||
|
return checkpoints, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stock := toStock(trade)
|
||||||
|
if err := m.consume(stock); err != nil {
|
||||||
|
return checkpoints, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.flushPendingSells()
|
||||||
|
|
||||||
|
m.squash()
|
||||||
|
|
||||||
|
return checkpoints, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func toStock(trade types.Trade) Stock {
|
||||||
|
if strings.HasPrefix(trade.Symbol, trade.FeeCurrency) {
|
||||||
|
if trade.IsBuyer {
|
||||||
|
trade.Quantity = trade.Quantity.Sub(trade.Fee)
|
||||||
|
} else {
|
||||||
|
trade.Quantity = trade.Quantity.Add(trade.Fee)
|
||||||
|
}
|
||||||
|
trade.Fee = fixedpoint.Zero
|
||||||
|
}
|
||||||
|
return Stock(trade)
|
||||||
|
}
|
184
pkg/accounting/cost_distribution_test.go
Normal file
184
pkg/accounting/cost_distribution_test.go
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
package accounting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStockManager(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("testdata", func(t *testing.T) {
|
||||||
|
var trades []types.Trade
|
||||||
|
|
||||||
|
out, err := ioutil.ReadFile("testdata/btcusdt-trades.json")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = json.Unmarshal(out, &trades)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var stockManager = &StockDistribution{
|
||||||
|
TradingFeeCurrency: "BNB",
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = stockManager.AddTrades(trades)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "0.72970242", stockManager.Stocks.Quantity().String())
|
||||||
|
assert.NotEmpty(t, stockManager.Stocks)
|
||||||
|
assert.Equal(t, 20, len(stockManager.Stocks))
|
||||||
|
assert.Equal(t, 0, len(stockManager.PendingSells))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stock", func(t *testing.T) {
|
||||||
|
var trades = []types.Trade{
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9100.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: true},
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9100.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: true},
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9200.0"), Quantity: fixedpoint.MustNewFromString("0.01"), IsBuyer: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
var stockManager = &StockDistribution{
|
||||||
|
TradingFeeCurrency: "BNB",
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := stockManager.AddTrades(trades)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, stockManager.Stocks, 2)
|
||||||
|
assert.Equal(t, StockSlice{
|
||||||
|
{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Price: fixedpoint.MustNewFromString("9100.0"),
|
||||||
|
Quantity: fixedpoint.MustNewFromString("0.05"),
|
||||||
|
IsBuyer: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Price: fixedpoint.MustNewFromString("9100.0"),
|
||||||
|
Quantity: fixedpoint.MustNewFromString("0.04"),
|
||||||
|
IsBuyer: true,
|
||||||
|
},
|
||||||
|
}, stockManager.Stocks)
|
||||||
|
assert.Len(t, stockManager.PendingSells, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sold out", func(t *testing.T) {
|
||||||
|
var trades = []types.Trade{
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9100.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: true},
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9200.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: false},
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9100.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: true},
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9200.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
var stockManager = &StockDistribution{
|
||||||
|
TradingFeeCurrency: "BNB",
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := stockManager.AddTrades(trades)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, stockManager.Stocks, 0)
|
||||||
|
assert.Len(t, stockManager.PendingSells, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("oversell", func(t *testing.T) {
|
||||||
|
var trades = []types.Trade{
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9100.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: true},
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9200.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: false},
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9200.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
var stockManager = &StockDistribution{
|
||||||
|
TradingFeeCurrency: "BNB",
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := stockManager.AddTrades(trades)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, stockManager.Stocks, 0)
|
||||||
|
assert.Len(t, stockManager.PendingSells, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("loss sell", func(t *testing.T) {
|
||||||
|
var trades = []types.Trade{
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9100.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: true},
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9200.0"), Quantity: fixedpoint.MustNewFromString("0.02"), IsBuyer: false},
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("8000.0"), Quantity: fixedpoint.MustNewFromString("0.01"), IsBuyer: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
var stockManager = &StockDistribution{
|
||||||
|
TradingFeeCurrency: "BNB",
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := stockManager.AddTrades(trades)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, stockManager.Stocks, 1)
|
||||||
|
assert.Equal(t, StockSlice{
|
||||||
|
{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Price: fixedpoint.MustNewFromString("9100.0"),
|
||||||
|
Quantity: fixedpoint.MustNewFromString("0.02"),
|
||||||
|
IsBuyer: true,
|
||||||
|
},
|
||||||
|
}, stockManager.Stocks)
|
||||||
|
assert.Len(t, stockManager.PendingSells, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("pending sell 1", func(t *testing.T) {
|
||||||
|
var trades = []types.Trade{
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9200.0"), Quantity: fixedpoint.MustNewFromString("0.02")},
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9100.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
var stockManager = &StockDistribution{
|
||||||
|
TradingFeeCurrency: "BNB",
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := stockManager.AddTrades(trades)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, stockManager.Stocks, 1)
|
||||||
|
assert.Equal(t, StockSlice{
|
||||||
|
{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Price: fixedpoint.MustNewFromString("9100.0"),
|
||||||
|
Quantity: fixedpoint.MustNewFromString("0.03"),
|
||||||
|
IsBuyer: true,
|
||||||
|
},
|
||||||
|
}, stockManager.Stocks)
|
||||||
|
assert.Len(t, stockManager.PendingSells, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("pending sell 2", func(t *testing.T) {
|
||||||
|
var trades = []types.Trade{
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9200.0"), Quantity: fixedpoint.MustNewFromString("0.1")},
|
||||||
|
{Symbol: "BTCUSDT", Price: fixedpoint.MustNewFromString("9100.0"), Quantity: fixedpoint.MustNewFromString("0.05"), IsBuyer: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
var stockManager = &StockDistribution{
|
||||||
|
TradingFeeCurrency: "BNB",
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := stockManager.AddTrades(trades)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, stockManager.Stocks, 0)
|
||||||
|
assert.Len(t, stockManager.PendingSells, 1)
|
||||||
|
assert.Equal(t, StockSlice{
|
||||||
|
{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Price: fixedpoint.MustNewFromString("9200.0"),
|
||||||
|
Quantity: fixedpoint.MustNewFromString("0.05"),
|
||||||
|
IsBuyer: false,
|
||||||
|
},
|
||||||
|
}, stockManager.PendingSells)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
129
pkg/accounting/pnl/avg_cost.go
Normal file
129
pkg/accounting/pnl/avg_cost.go
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
package pnl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AverageCostCalculator struct {
|
||||||
|
TradingFeeCurrency string
|
||||||
|
Market types.Market
|
||||||
|
ExchangeFee *types.ExchangeFee
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, currentPrice fixedpoint.Value) *AverageCostPnLReport {
|
||||||
|
// copy trades, so that we can truncate it.
|
||||||
|
var bidVolume = fixedpoint.Zero
|
||||||
|
var askVolume = fixedpoint.Zero
|
||||||
|
var feeUSD = fixedpoint.Zero
|
||||||
|
var grossProfit = fixedpoint.Zero
|
||||||
|
var grossLoss = fixedpoint.Zero
|
||||||
|
|
||||||
|
var position = types.NewPositionFromMarket(c.Market)
|
||||||
|
|
||||||
|
if c.ExchangeFee != nil {
|
||||||
|
position.SetFeeRate(*c.ExchangeFee)
|
||||||
|
} else {
|
||||||
|
makerFeeRate := 0.075 * 0.01
|
||||||
|
|
||||||
|
if c.Market.QuoteCurrency == "BUSD" {
|
||||||
|
makerFeeRate = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
position.SetFeeRate(types.ExchangeFee{
|
||||||
|
// binance vip 0 uses 0.075%
|
||||||
|
MakerFeeRate: fixedpoint.NewFromFloat(makerFeeRate),
|
||||||
|
TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(trades) == 0 {
|
||||||
|
return &AverageCostPnLReport{
|
||||||
|
Symbol: symbol,
|
||||||
|
Market: c.Market,
|
||||||
|
LastPrice: currentPrice,
|
||||||
|
NumTrades: 0,
|
||||||
|
Position: position,
|
||||||
|
BuyVolume: bidVolume,
|
||||||
|
SellVolume: askVolume,
|
||||||
|
FeeInUSD: feeUSD,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var currencyFees = map[string]fixedpoint.Value{}
|
||||||
|
|
||||||
|
// TODO: configure the exchange fee rate here later
|
||||||
|
// position.SetExchangeFeeRate()
|
||||||
|
var totalProfit fixedpoint.Value
|
||||||
|
var totalNetProfit fixedpoint.Value
|
||||||
|
|
||||||
|
var tradeIDs = map[uint64]types.Trade{}
|
||||||
|
|
||||||
|
for _, trade := range trades {
|
||||||
|
if _, exists := tradeIDs[trade.ID]; exists {
|
||||||
|
log.Warnf("duplicated trade: %+v", trade)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if trade.Symbol != symbol {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
profit, netProfit, madeProfit := position.AddTrade(trade)
|
||||||
|
if madeProfit {
|
||||||
|
totalProfit = totalProfit.Add(profit)
|
||||||
|
totalNetProfit = totalNetProfit.Add(netProfit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if profit.Sign() > 0 {
|
||||||
|
grossProfit = grossProfit.Add(profit)
|
||||||
|
} else if profit.Sign() < 0 {
|
||||||
|
grossLoss = grossLoss.Add(profit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if trade.IsBuyer {
|
||||||
|
bidVolume = bidVolume.Add(trade.Quantity)
|
||||||
|
} else {
|
||||||
|
askVolume = askVolume.Add(trade.Quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := currencyFees[trade.FeeCurrency]; !ok {
|
||||||
|
currencyFees[trade.FeeCurrency] = trade.Fee
|
||||||
|
} else {
|
||||||
|
currencyFees[trade.FeeCurrency] = currencyFees[trade.FeeCurrency].Add(trade.Fee)
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeIDs[trade.ID] = trade
|
||||||
|
}
|
||||||
|
|
||||||
|
unrealizedProfit := currentPrice.Sub(position.AverageCost).
|
||||||
|
Mul(position.GetBase())
|
||||||
|
|
||||||
|
return &AverageCostPnLReport{
|
||||||
|
Symbol: symbol,
|
||||||
|
Market: c.Market,
|
||||||
|
LastPrice: currentPrice,
|
||||||
|
NumTrades: len(trades),
|
||||||
|
StartTime: time.Time(trades[0].Time),
|
||||||
|
Position: position,
|
||||||
|
|
||||||
|
BuyVolume: bidVolume,
|
||||||
|
SellVolume: askVolume,
|
||||||
|
|
||||||
|
BaseAssetPosition: position.GetBase(),
|
||||||
|
Profit: totalProfit,
|
||||||
|
NetProfit: totalNetProfit,
|
||||||
|
UnrealizedProfit: unrealizedProfit,
|
||||||
|
|
||||||
|
GrossProfit: grossProfit,
|
||||||
|
GrossLoss: grossLoss,
|
||||||
|
|
||||||
|
AverageCost: position.AverageCost,
|
||||||
|
FeeInUSD: totalProfit.Sub(totalNetProfit),
|
||||||
|
CurrencyFees: currencyFees,
|
||||||
|
}
|
||||||
|
}
|
100
pkg/accounting/pnl/report.go
Normal file
100
pkg/accounting/pnl/report.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package pnl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/slack/slackstyle"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AverageCostPnLReport struct {
|
||||||
|
LastPrice fixedpoint.Value `json:"lastPrice"`
|
||||||
|
StartTime time.Time `json:"startTime"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Market types.Market `json:"market"`
|
||||||
|
|
||||||
|
NumTrades int `json:"numTrades"`
|
||||||
|
Profit fixedpoint.Value `json:"profit"`
|
||||||
|
UnrealizedProfit fixedpoint.Value `json:"unrealizedProfit"`
|
||||||
|
|
||||||
|
NetProfit fixedpoint.Value `json:"netProfit"`
|
||||||
|
GrossProfit fixedpoint.Value `json:"grossProfit"`
|
||||||
|
GrossLoss fixedpoint.Value `json:"grossLoss"`
|
||||||
|
Position *types.Position `json:"position,omitempty"`
|
||||||
|
|
||||||
|
AverageCost fixedpoint.Value `json:"averageCost"`
|
||||||
|
BuyVolume fixedpoint.Value `json:"buyVolume,omitempty"`
|
||||||
|
SellVolume fixedpoint.Value `json:"sellVolume,omitempty"`
|
||||||
|
FeeInUSD fixedpoint.Value `json:"feeInUSD"`
|
||||||
|
BaseAssetPosition fixedpoint.Value `json:"baseAssetPosition"`
|
||||||
|
CurrencyFees map[string]fixedpoint.Value `json:"currencyFees"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (report *AverageCostPnLReport) JSON() ([]byte, error) {
|
||||||
|
return json.MarshalIndent(report, "", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (report AverageCostPnLReport) Print() {
|
||||||
|
color.Green("TRADES SINCE: %v", report.StartTime)
|
||||||
|
color.Green("NUMBER OF TRADES: %d", report.NumTrades)
|
||||||
|
color.Green(report.Position.String())
|
||||||
|
color.Green("AVERAGE COST: %s", types.USD.FormatMoney(report.AverageCost))
|
||||||
|
color.Green("BASE ASSET POSITION: %s", report.BaseAssetPosition.String())
|
||||||
|
|
||||||
|
color.Green("TOTAL BUY VOLUME: %v", report.BuyVolume)
|
||||||
|
color.Green("TOTAL SELL VOLUME: %v", report.SellVolume)
|
||||||
|
|
||||||
|
color.Green("CURRENT PRICE: %s", types.USD.FormatMoney(report.LastPrice))
|
||||||
|
color.Green("CURRENCY FEES:")
|
||||||
|
for currency, fee := range report.CurrencyFees {
|
||||||
|
color.Green(" - %s: %s", currency, fee.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if report.Profit.Sign() > 0 {
|
||||||
|
color.Green("PROFIT: %s", types.USD.FormatMoney(report.Profit))
|
||||||
|
} else {
|
||||||
|
color.Red("PROFIT: %s", types.USD.FormatMoney(report.Profit))
|
||||||
|
}
|
||||||
|
|
||||||
|
if report.UnrealizedProfit.Sign() > 0 {
|
||||||
|
color.Green("UNREALIZED PROFIT: %s", types.USD.FormatMoney(report.UnrealizedProfit))
|
||||||
|
} else {
|
||||||
|
color.Red("UNREALIZED PROFIT: %s", types.USD.FormatMoney(report.UnrealizedProfit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (report AverageCostPnLReport) SlackAttachment() slack.Attachment {
|
||||||
|
var color = slackstyle.Red
|
||||||
|
|
||||||
|
if report.UnrealizedProfit.Sign() > 0 {
|
||||||
|
color = slackstyle.Green
|
||||||
|
}
|
||||||
|
|
||||||
|
return slack.Attachment{
|
||||||
|
Title: report.Symbol + " Profit and Loss report",
|
||||||
|
Text: "Profit " + types.USD.FormatMoney(report.Profit),
|
||||||
|
Color: color,
|
||||||
|
// Pretext: "",
|
||||||
|
// Text: "",
|
||||||
|
Fields: []slack.AttachmentField{
|
||||||
|
{Title: "Profit", Value: types.USD.FormatMoney(report.Profit)},
|
||||||
|
{Title: "Unrealized Profit", Value: types.USD.FormatMoney(report.UnrealizedProfit)},
|
||||||
|
{Title: "Current Price", Value: report.Market.FormatPrice(report.LastPrice), Short: true},
|
||||||
|
{Title: "Average Cost", Value: report.Market.FormatPrice(report.AverageCost), Short: true},
|
||||||
|
|
||||||
|
// FIXME:
|
||||||
|
// {Title: "Fee (USD)", Value: types.USD.FormatMoney(report.FeeInUSD), Short: true},
|
||||||
|
{Title: "Base Asset Position", Value: report.BaseAssetPosition.String(), Short: true},
|
||||||
|
{Title: "Number of Trades", Value: strconv.Itoa(report.NumTrades), Short: true},
|
||||||
|
},
|
||||||
|
Footer: report.StartTime.Format(time.RFC822),
|
||||||
|
FooterIcon: "",
|
||||||
|
}
|
||||||
|
}
|
1
pkg/accounting/testdata/btcusdt-trades.json
vendored
Normal file
1
pkg/accounting/testdata/btcusdt-trades.json
vendored
Normal file
File diff suppressed because one or more lines are too long
65
pkg/backtest/assets_dummy.go
Normal file
65
pkg/backtest/assets_dummy.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
//go:build !web
|
||||||
|
// +build !web
|
||||||
|
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var assets = map[string][]byte{}
|
||||||
|
|
||||||
|
var FS = &fs{}
|
||||||
|
|
||||||
|
type fs struct{}
|
||||||
|
|
||||||
|
func (fs *fs) Open(name string) (http.File, error) {
|
||||||
|
if name == "/" {
|
||||||
|
return fs, nil
|
||||||
|
}
|
||||||
|
b, ok := assets[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
return &file{name: name, size: len(b), Reader: bytes.NewReader(b)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *fs) Close() error { return nil }
|
||||||
|
func (fs *fs) Read(p []byte) (int, error) { return 0, nil }
|
||||||
|
func (fs *fs) Seek(offset int64, whence int) (int64, error) { return 0, nil }
|
||||||
|
func (fs *fs) Stat() (os.FileInfo, error) { return fs, nil }
|
||||||
|
func (fs *fs) Name() string { return "/" }
|
||||||
|
func (fs *fs) Size() int64 { return 0 }
|
||||||
|
func (fs *fs) Mode() os.FileMode { return 0755 }
|
||||||
|
func (fs *fs) ModTime() time.Time { return time.Time{} }
|
||||||
|
func (fs *fs) IsDir() bool { return true }
|
||||||
|
func (fs *fs) Sys() interface{} { return nil }
|
||||||
|
func (fs *fs) Readdir(count int) ([]os.FileInfo, error) {
|
||||||
|
files := []os.FileInfo{}
|
||||||
|
for name, data := range assets {
|
||||||
|
files = append(files, &file{name: name, size: len(data), Reader: bytes.NewReader(data)})
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type file struct {
|
||||||
|
name string
|
||||||
|
size int
|
||||||
|
*bytes.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *file) Close() error { return nil }
|
||||||
|
func (f *file) Readdir(count int) ([]os.FileInfo, error) {
|
||||||
|
return nil, errors.New("readdir is not supported")
|
||||||
|
}
|
||||||
|
func (f *file) Stat() (os.FileInfo, error) { return f, nil }
|
||||||
|
func (f *file) Name() string { return f.name }
|
||||||
|
func (f *file) Size() int64 { return int64(f.size) }
|
||||||
|
func (f *file) Mode() os.FileMode { return 0644 }
|
||||||
|
func (f *file) ModTime() time.Time { return time.Time{} }
|
||||||
|
func (f *file) IsDir() bool { return false }
|
||||||
|
func (f *file) Sys() interface{} { return nil }
|
97
pkg/backtest/dumper.go
Normal file
97
pkg/backtest/dumper.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/data/tsv"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DateFormat = "2006-01-02T15:04"
|
||||||
|
|
||||||
|
type symbolInterval struct {
|
||||||
|
Symbol string
|
||||||
|
Interval types.Interval
|
||||||
|
}
|
||||||
|
|
||||||
|
// KLineDumper dumps the received kline data into a folder for the backtest report to load the charts.
|
||||||
|
type KLineDumper struct {
|
||||||
|
OutputDirectory string
|
||||||
|
writers map[symbolInterval]*tsv.Writer
|
||||||
|
filenames map[symbolInterval]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKLineDumper(outputDirectory string) *KLineDumper {
|
||||||
|
return &KLineDumper{
|
||||||
|
OutputDirectory: outputDirectory,
|
||||||
|
writers: make(map[symbolInterval]*tsv.Writer),
|
||||||
|
filenames: make(map[symbolInterval]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *KLineDumper) Filenames() map[symbolInterval]string {
|
||||||
|
return d.filenames
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *KLineDumper) formatFileName(symbol string, interval types.Interval) string {
|
||||||
|
return filepath.Join(d.OutputDirectory, fmt.Sprintf("%s-%s.tsv",
|
||||||
|
symbol,
|
||||||
|
interval))
|
||||||
|
}
|
||||||
|
|
||||||
|
var csvHeader = []string{"date", "startTime", "endTime", "interval", "open", "high", "low", "close", "volume"}
|
||||||
|
|
||||||
|
func (d *KLineDumper) encode(k types.KLine) []string {
|
||||||
|
return []string{
|
||||||
|
time.Time(k.StartTime).Format(time.ANSIC), // ANSIC date - for javascript to parse (this works with Date.parse(date_str)
|
||||||
|
strconv.FormatInt(k.StartTime.Unix(), 10),
|
||||||
|
strconv.FormatInt(k.EndTime.Unix(), 10),
|
||||||
|
k.Interval.String(),
|
||||||
|
k.Open.String(),
|
||||||
|
k.High.String(),
|
||||||
|
k.Low.String(),
|
||||||
|
k.Close.String(),
|
||||||
|
k.Volume.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *KLineDumper) Record(k types.KLine) error {
|
||||||
|
si := symbolInterval{Symbol: k.Symbol, Interval: k.Interval}
|
||||||
|
|
||||||
|
w, ok := d.writers[si]
|
||||||
|
if !ok {
|
||||||
|
filename := d.formatFileName(k.Symbol, k.Interval)
|
||||||
|
w2, err := tsv.NewWriterFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w = w2
|
||||||
|
|
||||||
|
d.writers[si] = w2
|
||||||
|
d.filenames[si] = filename
|
||||||
|
|
||||||
|
if err2 := w2.Write(csvHeader); err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.Write(d.encode(k))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *KLineDumper) Close() error {
|
||||||
|
var err error = nil
|
||||||
|
for _, w := range d.writers {
|
||||||
|
w.Flush()
|
||||||
|
err2 := w.Close()
|
||||||
|
if err2 != nil {
|
||||||
|
err = multierr.Append(err, err2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
54
pkg/backtest/dumper_test.go
Normal file
54
pkg/backtest/dumper_test.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKLineDumper(t *testing.T) {
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
_ = os.Mkdir(tempDir, 0755)
|
||||||
|
dumper := NewKLineDumper(tempDir)
|
||||||
|
|
||||||
|
t1 := time.Now()
|
||||||
|
err := dumper.Record(types.KLine{
|
||||||
|
Exchange: types.ExchangeBinance,
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
StartTime: types.Time(t1),
|
||||||
|
EndTime: types.Time(t1.Add(time.Minute)),
|
||||||
|
Interval: types.Interval1m,
|
||||||
|
Open: fixedpoint.NewFromFloat(1000.0),
|
||||||
|
High: fixedpoint.NewFromFloat(2000.0),
|
||||||
|
Low: fixedpoint.NewFromFloat(3000.0),
|
||||||
|
Close: fixedpoint.NewFromFloat(4000.0),
|
||||||
|
Volume: fixedpoint.NewFromFloat(5000.0),
|
||||||
|
QuoteVolume: fixedpoint.NewFromFloat(6000.0),
|
||||||
|
NumberOfTrades: 10,
|
||||||
|
Closed: true,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = dumper.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
filenames := dumper.Filenames()
|
||||||
|
assert.NotEmpty(t, filenames)
|
||||||
|
for _, filename := range filenames {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
reader := csv.NewReader(f)
|
||||||
|
records, err2 := reader.Read()
|
||||||
|
if assert.NoError(t, err2) {
|
||||||
|
assert.NotEmptyf(t, records, "%v", records)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
451
pkg/backtest/exchange.go
Normal file
451
pkg/backtest/exchange.go
Normal file
|
@ -0,0 +1,451 @@
|
||||||
|
/*
|
||||||
|
The backtest process
|
||||||
|
|
||||||
|
The backtest engine loads the klines from the database into a kline-channel,
|
||||||
|
there are multiple matching engine that matches the order sent from the strategy.
|
||||||
|
|
||||||
|
for each kline, the backtest engine:
|
||||||
|
|
||||||
|
1) load the kline, run matching logics to send out order update and trades to the user data stream.
|
||||||
|
2) once the matching process for the kline is done, the kline will be pushed to the market data stream.
|
||||||
|
3) go to 1 and load the next kline.
|
||||||
|
|
||||||
|
There are 2 ways that a strategy could work with backtest engine:
|
||||||
|
|
||||||
|
1. the strategy receives kline from the market data stream, and then it submits the order by the given market data to the backtest engine.
|
||||||
|
backtest engine receives the order and then pushes the trade and order updates to the user data stream.
|
||||||
|
|
||||||
|
the strategy receives the trade and update its position.
|
||||||
|
|
||||||
|
2. the strategy places the orders when it starts. (like grid) the strategy then receives the order updates and then submit a new order
|
||||||
|
by its order update message.
|
||||||
|
|
||||||
|
We need to ensure that:
|
||||||
|
|
||||||
|
1. if the strategy submits the order from the market data stream, since it's a separate goroutine, the strategy should block the backtest engine
|
||||||
|
to process the trades before the next kline is published.
|
||||||
|
*/
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/cache"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/qbtrade"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/service"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = logrus.WithField("cmd", "backtest")
|
||||||
|
|
||||||
|
var ErrUnimplemented = errors.New("unimplemented method")
|
||||||
|
var ErrNegativeQuantity = errors.New("order quantity can not be negative")
|
||||||
|
var ErrZeroQuantity = errors.New("order quantity can not be zero")
|
||||||
|
var ErrEmptyOrderType = errors.New("order type can not be empty string")
|
||||||
|
|
||||||
|
type Exchange struct {
|
||||||
|
sourceName types.ExchangeName
|
||||||
|
publicExchange types.Exchange
|
||||||
|
srv *service.BacktestService
|
||||||
|
currentTime time.Time
|
||||||
|
|
||||||
|
account *types.Account
|
||||||
|
config *qbtrade.Backtest
|
||||||
|
|
||||||
|
MarketDataStream types.StandardStreamEmitter
|
||||||
|
|
||||||
|
trades map[string][]types.Trade
|
||||||
|
tradesMutex sync.Mutex
|
||||||
|
|
||||||
|
closedOrders map[string][]types.Order
|
||||||
|
closedOrdersMutex sync.Mutex
|
||||||
|
|
||||||
|
matchingBooks map[string]*SimplePriceMatching
|
||||||
|
matchingBooksMutex sync.Mutex
|
||||||
|
|
||||||
|
markets types.MarketMap
|
||||||
|
|
||||||
|
Src *ExchangeDataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExchange(
|
||||||
|
sourceName types.ExchangeName, sourceExchange types.Exchange, srv *service.BacktestService, config *qbtrade.Backtest,
|
||||||
|
) (*Exchange, error) {
|
||||||
|
ex := sourceExchange
|
||||||
|
|
||||||
|
markets, err := cache.LoadExchangeMarketsWithCache(context.Background(), ex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := config.StartTime.Time()
|
||||||
|
configAccount := config.GetAccount(sourceName.String())
|
||||||
|
|
||||||
|
account := &types.Account{
|
||||||
|
MakerFeeRate: configAccount.MakerFeeRate,
|
||||||
|
TakerFeeRate: configAccount.TakerFeeRate,
|
||||||
|
AccountType: types.AccountTypeSpot,
|
||||||
|
}
|
||||||
|
|
||||||
|
balances := configAccount.Balances.BalanceMap()
|
||||||
|
account.UpdateBalances(balances)
|
||||||
|
|
||||||
|
e := &Exchange{
|
||||||
|
sourceName: sourceName,
|
||||||
|
publicExchange: ex,
|
||||||
|
markets: markets,
|
||||||
|
srv: srv,
|
||||||
|
config: config,
|
||||||
|
account: account,
|
||||||
|
currentTime: startTime,
|
||||||
|
closedOrders: make(map[string][]types.Order),
|
||||||
|
trades: make(map[string][]types.Trade),
|
||||||
|
}
|
||||||
|
|
||||||
|
e.resetMatchingBooks()
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) addTrade(trade types.Trade) {
|
||||||
|
e.tradesMutex.Lock()
|
||||||
|
e.trades[trade.Symbol] = append(e.trades[trade.Symbol], trade)
|
||||||
|
e.tradesMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) addClosedOrder(order types.Order) {
|
||||||
|
e.closedOrdersMutex.Lock()
|
||||||
|
e.closedOrders[order.Symbol] = append(e.closedOrders[order.Symbol], order)
|
||||||
|
e.closedOrdersMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) resetMatchingBooks() {
|
||||||
|
e.matchingBooksMutex.Lock()
|
||||||
|
e.matchingBooks = make(map[string]*SimplePriceMatching)
|
||||||
|
for symbol, market := range e.markets {
|
||||||
|
e._addMatchingBook(symbol, market)
|
||||||
|
}
|
||||||
|
e.matchingBooksMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) addMatchingBook(symbol string, market types.Market) {
|
||||||
|
e.matchingBooksMutex.Lock()
|
||||||
|
e._addMatchingBook(symbol, market)
|
||||||
|
e.matchingBooksMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) _addMatchingBook(symbol string, market types.Market) {
|
||||||
|
matching := &SimplePriceMatching{
|
||||||
|
currentTime: e.currentTime,
|
||||||
|
account: e.account,
|
||||||
|
Market: market,
|
||||||
|
closedOrders: make(map[uint64]types.Order),
|
||||||
|
feeModeFunction: getFeeModeFunction(e.config.FeeMode),
|
||||||
|
}
|
||||||
|
|
||||||
|
e.matchingBooks[symbol] = matching
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) NewStream() types.Stream {
|
||||||
|
return &types.BacktestStream{
|
||||||
|
StandardStreamEmitter: &types.StandardStream{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) {
|
||||||
|
book := e.matchingBooks[q.Symbol]
|
||||||
|
oid, err := strconv.ParseUint(q.OrderID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
order, ok := book.getOrder(oid)
|
||||||
|
if ok {
|
||||||
|
return &order, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) {
|
||||||
|
symbol := order.Symbol
|
||||||
|
matching, ok := e.matchingBook(symbol)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.Price.Sign() < 0 {
|
||||||
|
return nil, fmt.Errorf("order price can not be negative, %s given", order.Price.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.Quantity.Sign() < 0 {
|
||||||
|
return nil, ErrNegativeQuantity
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.Quantity.IsZero() {
|
||||||
|
return nil, ErrZeroQuantity
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.Type == "" {
|
||||||
|
return nil, ErrEmptyOrderType
|
||||||
|
}
|
||||||
|
|
||||||
|
createdOrder, _, err = matching.PlaceOrder(order)
|
||||||
|
if createdOrder != nil {
|
||||||
|
// market order can be closed immediately.
|
||||||
|
switch createdOrder.Status {
|
||||||
|
case types.OrderStatusFilled, types.OrderStatusCanceled, types.OrderStatusRejected:
|
||||||
|
e.addClosedOrder(*createdOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdOrder, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
|
||||||
|
matching, ok := e.matchingBook(symbol)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(matching.bidOrders, matching.askOrders...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryClosedOrders(
|
||||||
|
ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64,
|
||||||
|
) (orders []types.Order, err error) {
|
||||||
|
orders, ok := e.closedOrders[symbol]
|
||||||
|
if !ok {
|
||||||
|
return orders, fmt.Errorf("matching engine is not initialized for symbol %s", symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
return orders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {
|
||||||
|
for _, order := range orders {
|
||||||
|
matching, ok := e.matchingBook(order.Symbol)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("matching engine is not initialized for symbol %s", order.Symbol)
|
||||||
|
}
|
||||||
|
_, err := matching.CancelOrder(order)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
|
||||||
|
return e.account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) {
|
||||||
|
return e.account.Balances(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryKLines(
|
||||||
|
ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions,
|
||||||
|
) ([]types.KLine, error) {
|
||||||
|
if options.EndTime != nil {
|
||||||
|
return e.srv.QueryKLinesBackward(e, symbol, interval, *options.EndTime, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.StartTime != nil {
|
||||||
|
return e.srv.QueryKLinesForward(e, symbol, interval, *options.StartTime, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("endTime or startTime can not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryTrades(
|
||||||
|
ctx context.Context, symbol string, options *types.TradeQueryOptions,
|
||||||
|
) ([]types.Trade, error) {
|
||||||
|
// we don't need query trades for backtest
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) {
|
||||||
|
matching, ok := e.matchingBook(symbol)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
kline := matching.lastKLine
|
||||||
|
return &types.Ticker{
|
||||||
|
Time: kline.EndTime.Time(),
|
||||||
|
Volume: kline.Volume,
|
||||||
|
Last: kline.Close,
|
||||||
|
Open: kline.Open,
|
||||||
|
High: kline.High,
|
||||||
|
Low: kline.Low,
|
||||||
|
Buy: kline.Close.Sub(matching.Market.TickSize),
|
||||||
|
Sell: kline.Close.Add(matching.Market.TickSize),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
|
||||||
|
// Not using Tickers in back test (yet)
|
||||||
|
return nil, ErrUnimplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) Name() types.ExchangeName {
|
||||||
|
return e.publicExchange.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) PlatformFeeCurrency() string {
|
||||||
|
return e.publicExchange.PlatformFeeCurrency()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
|
||||||
|
return e.markets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryDepositHistory(
|
||||||
|
ctx context.Context, asset string, since, until time.Time,
|
||||||
|
) (allDeposits []types.Deposit, err error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryWithdrawHistory(
|
||||||
|
ctx context.Context, asset string, since, until time.Time,
|
||||||
|
) (allWithdraws []types.Withdraw, err error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) matchingBook(symbol string) (*SimplePriceMatching, bool) {
|
||||||
|
e.matchingBooksMutex.Lock()
|
||||||
|
m, ok := e.matchingBooks[symbol]
|
||||||
|
e.matchingBooksMutex.Unlock()
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) BindUserData(userDataStream types.StandardStreamEmitter) {
|
||||||
|
userDataStream.OnTradeUpdate(func(trade types.Trade) {
|
||||||
|
e.addTrade(trade)
|
||||||
|
})
|
||||||
|
|
||||||
|
e.matchingBooksMutex.Lock()
|
||||||
|
for _, matching := range e.matchingBooks {
|
||||||
|
matching.OnTradeUpdate(userDataStream.EmitTradeUpdate)
|
||||||
|
matching.OnOrderUpdate(userDataStream.EmitOrderUpdate)
|
||||||
|
matching.OnBalanceUpdate(userDataStream.EmitBalanceUpdate)
|
||||||
|
}
|
||||||
|
e.matchingBooksMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) SubscribeMarketData(
|
||||||
|
startTime, endTime time.Time, requiredInterval types.Interval, extraIntervals ...types.Interval,
|
||||||
|
) (chan types.KLine, error) {
|
||||||
|
log.Infof("collecting backtest configurations...")
|
||||||
|
|
||||||
|
loadedSymbols := map[string]struct{}{}
|
||||||
|
loadedIntervals := map[types.Interval]struct{}{
|
||||||
|
// 1m interval is required for the backtest matching engine
|
||||||
|
requiredInterval: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, it := range extraIntervals {
|
||||||
|
loadedIntervals[it] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect subscriptions
|
||||||
|
for _, sub := range e.MarketDataStream.GetSubscriptions() {
|
||||||
|
loadedSymbols[sub.Symbol] = struct{}{}
|
||||||
|
|
||||||
|
switch sub.Channel {
|
||||||
|
case types.KLineChannel:
|
||||||
|
loadedIntervals[sub.Options.Interval] = struct{}{}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Since Environment is not yet been injected at this point, no hard error
|
||||||
|
log.Errorf("stream channel %s is not supported in backtest", sub.Channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var symbols []string
|
||||||
|
for symbol := range loadedSymbols {
|
||||||
|
symbols = append(symbols, symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
var intervals []types.Interval
|
||||||
|
for interval := range loadedIntervals {
|
||||||
|
intervals = append(intervals, interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFutures bool
|
||||||
|
if futuresExchange, ok := e.publicExchange.(types.FuturesExchange); ok {
|
||||||
|
isFutures = futuresExchange.GetFuturesSettings().IsFutures
|
||||||
|
} else {
|
||||||
|
isFutures = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFutures {
|
||||||
|
log.Infof("querying futures klines from database with exchange: %v symbols: %v and intervals: %v for back-testing", e.Name(), symbols, intervals)
|
||||||
|
} else {
|
||||||
|
log.Infof("querying klines from database with exchange: %v symbols: %v and intervals: %v for back-testing", e.Name(), symbols, intervals)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(symbols) == 0 {
|
||||||
|
log.Warnf("empty symbols, will not query kline data from the database")
|
||||||
|
|
||||||
|
c := make(chan types.KLine)
|
||||||
|
close(c)
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
klineC, errC := e.srv.QueryKLinesCh(startTime, endTime, e.publicExchange, symbols, intervals)
|
||||||
|
go func() {
|
||||||
|
if err := <-errC; err != nil {
|
||||||
|
log.WithError(err).Error("backtest data feed error")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return klineC, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) ConsumeKLine(k types.KLine, requiredInterval types.Interval) {
|
||||||
|
matching, ok := e.matchingBook(k.Symbol)
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("matching book of %s is not initialized", k.Symbol)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if matching.klineCache == nil {
|
||||||
|
matching.klineCache = make(map[types.Interval]types.KLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
requiredKline, ok := matching.klineCache[k.Interval]
|
||||||
|
if ok { // pop out all the old
|
||||||
|
if requiredKline.Interval != requiredInterval {
|
||||||
|
panic(fmt.Sprintf("expect required kline interval %s, got interval %s", requiredInterval.String(), requiredKline.Interval.String()))
|
||||||
|
}
|
||||||
|
e.currentTime = requiredKline.EndTime.Time()
|
||||||
|
// here we generate trades and order updates
|
||||||
|
matching.processKLine(requiredKline)
|
||||||
|
matching.nextKLine = &k
|
||||||
|
for _, kline := range matching.klineCache {
|
||||||
|
e.MarketDataStream.EmitKLineClosed(kline)
|
||||||
|
for _, h := range e.Src.Callbacks {
|
||||||
|
h(kline, e.Src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// reset the paramcache
|
||||||
|
matching.klineCache = make(map[types.Interval]types.KLine)
|
||||||
|
}
|
||||||
|
matching.klineCache[k.Interval] = k
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) CloseMarketData() error {
|
||||||
|
if err := e.MarketDataStream.Close(); err != nil {
|
||||||
|
log.WithError(err).Error("stream close error")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
13
pkg/backtest/exchange_klinec.go
Normal file
13
pkg/backtest/exchange_klinec.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/qbtrade"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExchangeDataSource struct {
|
||||||
|
C chan types.KLine
|
||||||
|
Exchange *Exchange
|
||||||
|
Session *qbtrade.ExchangeSession
|
||||||
|
Callbacks []func(types.KLine, *ExchangeDataSource)
|
||||||
|
}
|
57
pkg/backtest/fee.go
Normal file
57
pkg/backtest/fee.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/qbtrade"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FeeModeFunction func(order *types.Order, market *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string)
|
||||||
|
|
||||||
|
func feeModeFunctionToken(order *types.Order, _ *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) {
|
||||||
|
quoteQuantity := order.Quantity.Mul(order.Price)
|
||||||
|
feeCurrency = FeeToken
|
||||||
|
fee = quoteQuantity.Mul(feeRate)
|
||||||
|
return fee, feeCurrency
|
||||||
|
}
|
||||||
|
|
||||||
|
func feeModeFunctionNative(order *types.Order, market *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) {
|
||||||
|
switch order.Side {
|
||||||
|
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
fee = order.Quantity.Mul(feeRate)
|
||||||
|
feeCurrency = market.BaseCurrency
|
||||||
|
|
||||||
|
case types.SideTypeSell:
|
||||||
|
quoteQuantity := order.Quantity.Mul(order.Price)
|
||||||
|
fee = quoteQuantity.Mul(feeRate)
|
||||||
|
feeCurrency = market.QuoteCurrency
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return fee, feeCurrency
|
||||||
|
}
|
||||||
|
|
||||||
|
func feeModeFunctionQuote(order *types.Order, market *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) {
|
||||||
|
feeCurrency = market.QuoteCurrency
|
||||||
|
quoteQuantity := order.Quantity.Mul(order.Price)
|
||||||
|
fee = quoteQuantity.Mul(feeRate)
|
||||||
|
return fee, feeCurrency
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFeeModeFunction(feeMode qbtrade.BacktestFeeMode) FeeModeFunction {
|
||||||
|
switch feeMode {
|
||||||
|
|
||||||
|
case qbtrade.BacktestFeeModeNative:
|
||||||
|
return feeModeFunctionNative
|
||||||
|
|
||||||
|
case qbtrade.BacktestFeeModeQuote:
|
||||||
|
return feeModeFunctionQuote
|
||||||
|
|
||||||
|
case qbtrade.BacktestFeeModeToken:
|
||||||
|
return feeModeFunctionToken
|
||||||
|
|
||||||
|
default:
|
||||||
|
return feeModeFunctionQuote
|
||||||
|
}
|
||||||
|
}
|
124
pkg/backtest/fee_test.go
Normal file
124
pkg/backtest/fee_test.go
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_feeModeFunctionToken(t *testing.T) {
|
||||||
|
market := getTestMarket()
|
||||||
|
t.Run("sellOrder", func(t *testing.T) {
|
||||||
|
order := types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(20000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||||
|
fee, feeCurrency := feeModeFunctionToken(&order, &market, feeRate)
|
||||||
|
assert.Equal(t, "1.5", fee.String())
|
||||||
|
assert.Equal(t, "FEE", feeCurrency)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("buyOrder", func(t *testing.T) {
|
||||||
|
order := types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(20000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||||
|
fee, feeCurrency := feeModeFunctionToken(&order, &market, feeRate)
|
||||||
|
assert.Equal(t, "1.5", fee.String())
|
||||||
|
assert.Equal(t, "FEE", feeCurrency)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_feeModeFunctionQuote(t *testing.T) {
|
||||||
|
market := getTestMarket()
|
||||||
|
t.Run("sellOrder", func(t *testing.T) {
|
||||||
|
order := types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(20000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||||
|
fee, feeCurrency := feeModeFunctionQuote(&order, &market, feeRate)
|
||||||
|
assert.Equal(t, "1.5", fee.String())
|
||||||
|
assert.Equal(t, "USDT", feeCurrency)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("buyOrder", func(t *testing.T) {
|
||||||
|
order := types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(20000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||||
|
fee, feeCurrency := feeModeFunctionQuote(&order, &market, feeRate)
|
||||||
|
assert.Equal(t, "1.5", fee.String())
|
||||||
|
assert.Equal(t, "USDT", feeCurrency)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_feeModeFunctionNative(t *testing.T) {
|
||||||
|
market := getTestMarket()
|
||||||
|
t.Run("sellOrder", func(t *testing.T) {
|
||||||
|
order := types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(20000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||||
|
fee, feeCurrency := feeModeFunctionNative(&order, &market, feeRate)
|
||||||
|
assert.Equal(t, "1.5", fee.String())
|
||||||
|
assert.Equal(t, "USDT", feeCurrency)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("buyOrder", func(t *testing.T) {
|
||||||
|
order := types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(20000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||||
|
fee, feeCurrency := feeModeFunctionNative(&order, &market, feeRate)
|
||||||
|
assert.Equal(t, "0.000075", fee.String())
|
||||||
|
assert.Equal(t, "BTC", feeCurrency)
|
||||||
|
})
|
||||||
|
}
|
93
pkg/backtest/fixture_test.go
Normal file
93
pkg/backtest/fixture_test.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KLineFixtureGenerator struct {
|
||||||
|
Symbol string
|
||||||
|
Interval types.Interval
|
||||||
|
StartTime, EndTime time.Time
|
||||||
|
StartPrice fixedpoint.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *KLineFixtureGenerator) Generate(ctx context.Context, c chan types.KLine) error {
|
||||||
|
defer close(c)
|
||||||
|
|
||||||
|
startTime := g.StartTime
|
||||||
|
price := g.StartPrice
|
||||||
|
|
||||||
|
if price.IsZero() {
|
||||||
|
return errors.New("startPrice can not be zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
for startTime.Before(g.EndTime) {
|
||||||
|
open := price
|
||||||
|
high := price.Mul(fixedpoint.NewFromFloat(1.01))
|
||||||
|
low := price.Mul(fixedpoint.NewFromFloat(0.99))
|
||||||
|
amp := high.Sub(low)
|
||||||
|
cls := low.Add(amp.Mul(fixedpoint.NewFromFloat(rand.Float64())))
|
||||||
|
|
||||||
|
vol := fixedpoint.NewFromFloat(rand.Float64() * 1000.0)
|
||||||
|
quoteVol := fixedpoint.NewFromFloat(rand.Float64() * 1000.0).Mul(price)
|
||||||
|
|
||||||
|
nextStartTime := startTime.Add(g.Interval.Duration())
|
||||||
|
k := types.KLine{
|
||||||
|
Exchange: types.ExchangeBinance,
|
||||||
|
Symbol: g.Symbol,
|
||||||
|
StartTime: types.Time(startTime),
|
||||||
|
EndTime: types.Time(nextStartTime.Add(-time.Millisecond)),
|
||||||
|
Interval: g.Interval,
|
||||||
|
Open: open,
|
||||||
|
Close: cls,
|
||||||
|
High: high,
|
||||||
|
Low: low,
|
||||||
|
Volume: vol,
|
||||||
|
QuoteVolume: quoteVol,
|
||||||
|
Closed: true,
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case c <- k:
|
||||||
|
}
|
||||||
|
price = cls
|
||||||
|
startTime = nextStartTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKLineFixtureGenerator(t *testing.T) {
|
||||||
|
startTime := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.Local)
|
||||||
|
endTime := time.Date(2022, time.January, 31, 0, 0, 0, 0, time.Local)
|
||||||
|
ctx := context.Background()
|
||||||
|
g := &KLineFixtureGenerator{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Interval: types.Interval1m,
|
||||||
|
StartTime: startTime,
|
||||||
|
EndTime: endTime,
|
||||||
|
StartPrice: fixedpoint.NewFromFloat(18000.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
c := make(chan types.KLine, 20)
|
||||||
|
go func() {
|
||||||
|
err := g.Generate(ctx, c)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}()
|
||||||
|
for k := range c {
|
||||||
|
// high must higher than low
|
||||||
|
assert.True(t, k.High.Compare(k.Low) > 0)
|
||||||
|
assert.True(t, k.StartTime.After(startTime) || k.StartTime.Equal(startTime))
|
||||||
|
assert.True(t, k.StartTime.Before(endTime))
|
||||||
|
}
|
||||||
|
}
|
47
pkg/backtest/manifests.go
Normal file
47
pkg/backtest/manifests.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type ManifestEntry struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
StrategyID string `json:"strategyID"`
|
||||||
|
StrategyInstance string `json:"strategyInstance"`
|
||||||
|
StrategyProperty string `json:"strategyProperty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Manifests map[InstancePropertyIndex]string
|
||||||
|
|
||||||
|
func (m *Manifests) UnmarshalJSON(j []byte) error {
|
||||||
|
var entries []ManifestEntry
|
||||||
|
if err := json.Unmarshal(j, &entries); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mm := make(Manifests)
|
||||||
|
for _, entry := range entries {
|
||||||
|
index := InstancePropertyIndex{
|
||||||
|
ID: entry.StrategyID,
|
||||||
|
InstanceID: entry.StrategyInstance,
|
||||||
|
Property: entry.StrategyProperty,
|
||||||
|
}
|
||||||
|
mm[index] = entry.Filename
|
||||||
|
}
|
||||||
|
*m = mm
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Manifests) MarshalJSON() ([]byte, error) {
|
||||||
|
var arr []ManifestEntry
|
||||||
|
for k, v := range m {
|
||||||
|
arr = append(arr, ManifestEntry{
|
||||||
|
Type: "strategyProperty",
|
||||||
|
Filename: v,
|
||||||
|
StrategyID: k.ID,
|
||||||
|
StrategyInstance: k.InstanceID,
|
||||||
|
StrategyProperty: k.Property,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
return json.MarshalIndent(arr, "", " ")
|
||||||
|
}
|
725
pkg/backtest/matching.go
Normal file
725
pkg/backtest/matching.go
Normal file
|
@ -0,0 +1,725 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var orderID uint64 = 1
|
||||||
|
var tradeID uint64 = 1
|
||||||
|
|
||||||
|
func incOrderID() uint64 {
|
||||||
|
return atomic.AddUint64(&orderID, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func incTradeID() uint64 {
|
||||||
|
return atomic.AddUint64(&tradeID, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var klineMatchingLogger *logrus.Entry = nil
|
||||||
|
|
||||||
|
// FeeToken is used to simulate the exchange platform fee token
|
||||||
|
// This is to ease the back-testing environment for closing positions.
|
||||||
|
const FeeToken = "FEE"
|
||||||
|
|
||||||
|
var useFeeToken = true
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger := logrus.New()
|
||||||
|
if v, ok := util.GetEnvVarBool("DEBUG_MATCHING"); ok && v {
|
||||||
|
logger.SetLevel(logrus.DebugLevel)
|
||||||
|
} else {
|
||||||
|
logger.SetLevel(logrus.ErrorLevel)
|
||||||
|
}
|
||||||
|
klineMatchingLogger = logger.WithField("backtest", "klineEngine")
|
||||||
|
|
||||||
|
if v, ok := util.GetEnvVarBool("BACKTEST_USE_FEE_TOKEN"); ok {
|
||||||
|
useFeeToken = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimplePriceMatching implements a simple kline data driven matching engine for backtest
|
||||||
|
//
|
||||||
|
//go:generate callbackgen -type SimplePriceMatching
|
||||||
|
type SimplePriceMatching struct {
|
||||||
|
Symbol string
|
||||||
|
Market types.Market
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
bidOrders []types.Order
|
||||||
|
askOrders []types.Order
|
||||||
|
closedOrders map[uint64]types.Order
|
||||||
|
|
||||||
|
klineCache map[types.Interval]types.KLine
|
||||||
|
lastPrice fixedpoint.Value
|
||||||
|
lastKLine types.KLine
|
||||||
|
nextKLine *types.KLine
|
||||||
|
currentTime time.Time
|
||||||
|
|
||||||
|
feeModeFunction FeeModeFunction
|
||||||
|
|
||||||
|
account *types.Account
|
||||||
|
|
||||||
|
tradeUpdateCallbacks []func(trade types.Trade)
|
||||||
|
orderUpdateCallbacks []func(order types.Order)
|
||||||
|
balanceUpdateCallbacks []func(balances types.BalanceMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
|
||||||
|
found := false
|
||||||
|
|
||||||
|
switch o.Side {
|
||||||
|
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
m.mu.Lock()
|
||||||
|
var orders []types.Order
|
||||||
|
for _, order := range m.bidOrders {
|
||||||
|
if o.OrderID == order.OrderID {
|
||||||
|
found = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
orders = append(orders, order)
|
||||||
|
}
|
||||||
|
m.bidOrders = orders
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
case types.SideTypeSell:
|
||||||
|
m.mu.Lock()
|
||||||
|
var orders []types.Order
|
||||||
|
for _, order := range m.askOrders {
|
||||||
|
if o.OrderID == order.OrderID {
|
||||||
|
found = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
orders = append(orders, order)
|
||||||
|
}
|
||||||
|
m.askOrders = orders
|
||||||
|
m.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return o, fmt.Errorf("cancel order failed, order %d not found: %+v", o.OrderID, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch o.Side {
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
if err := m.account.UnlockBalance(m.Market.QuoteCurrency, o.Price.Mul(o.Quantity)); err != nil {
|
||||||
|
return o, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.SideTypeSell:
|
||||||
|
if err := m.account.UnlockBalance(m.Market.BaseCurrency, o.Quantity); err != nil {
|
||||||
|
return o, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Status = types.OrderStatusCanceled
|
||||||
|
m.EmitOrderUpdate(o)
|
||||||
|
m.EmitBalanceUpdate(m.account.Balances())
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaceOrder returns the created order object, executed trade (if any) and error
|
||||||
|
func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *types.Trade, error) {
|
||||||
|
if o.Type == types.OrderTypeMarket {
|
||||||
|
if m.lastPrice.IsZero() {
|
||||||
|
panic("unexpected error: for market order, the last price can not be zero")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isTaker := o.Type == types.OrderTypeMarket || isLimitTakerOrder(o, m.lastPrice)
|
||||||
|
|
||||||
|
// price for checking account balance, default price
|
||||||
|
price := o.Price
|
||||||
|
|
||||||
|
switch o.Type {
|
||||||
|
case types.OrderTypeMarket:
|
||||||
|
price = m.Market.TruncatePrice(m.lastPrice)
|
||||||
|
|
||||||
|
case types.OrderTypeStopMarket:
|
||||||
|
// the actual price might be different.
|
||||||
|
o.StopPrice = m.Market.TruncatePrice(o.StopPrice)
|
||||||
|
price = o.StopPrice
|
||||||
|
|
||||||
|
case types.OrderTypeLimit, types.OrderTypeStopLimit, types.OrderTypeLimitMaker:
|
||||||
|
o.Price = m.Market.TruncatePrice(o.Price)
|
||||||
|
price = o.Price
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Quantity = m.Market.TruncateQuantity(o.Quantity)
|
||||||
|
|
||||||
|
if o.Quantity.Compare(m.Market.MinQuantity) < 0 {
|
||||||
|
return nil, nil, fmt.Errorf("order quantity %s is less than minQuantity %s, order: %+v", o.Quantity.String(), m.Market.MinQuantity.String(), o)
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteQuantity := o.Quantity.Mul(price)
|
||||||
|
if quoteQuantity.Compare(m.Market.MinNotional) < 0 {
|
||||||
|
return nil, nil, fmt.Errorf("order amount %s is less than minNotional %s, order: %+v", quoteQuantity.String(), m.Market.MinNotional.String(), o)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch o.Side {
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
if err := m.account.LockBalance(m.Market.QuoteCurrency, quoteQuantity); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.SideTypeSell:
|
||||||
|
if err := m.account.LockBalance(m.Market.BaseCurrency, o.Quantity); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.EmitBalanceUpdate(m.account.Balances())
|
||||||
|
|
||||||
|
// start from one
|
||||||
|
orderID := incOrderID()
|
||||||
|
order := m.newOrder(o, orderID)
|
||||||
|
|
||||||
|
if isTaker {
|
||||||
|
var price fixedpoint.Value
|
||||||
|
if order.Type == types.OrderTypeMarket {
|
||||||
|
order.Price = m.Market.TruncatePrice(m.lastPrice)
|
||||||
|
price = order.Price
|
||||||
|
} else if order.Type == types.OrderTypeLimit {
|
||||||
|
// if limit order's price is with the range of next kline
|
||||||
|
// we assume it will be traded as a maker trade, and is traded at its original price
|
||||||
|
// TODO: if it is treated as a maker trade, fee should be specially handled
|
||||||
|
// otherwise, set NextKLine.Close(i.e., m.LastPrice) to be the taker traded price
|
||||||
|
if m.nextKLine != nil && m.nextKLine.High.Compare(order.Price) > 0 && order.Side == types.SideTypeBuy {
|
||||||
|
order.AveragePrice = order.Price
|
||||||
|
} else if m.nextKLine != nil && m.nextKLine.Low.Compare(order.Price) < 0 && order.Side == types.SideTypeSell {
|
||||||
|
order.AveragePrice = order.Price
|
||||||
|
} else {
|
||||||
|
order.AveragePrice = m.Market.TruncatePrice(m.lastPrice)
|
||||||
|
}
|
||||||
|
price = order.AveragePrice
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit the order update for Status:New
|
||||||
|
m.EmitOrderUpdate(order)
|
||||||
|
|
||||||
|
// copy the order object to avoid side effect (for different callbacks)
|
||||||
|
var order2 = order
|
||||||
|
|
||||||
|
// emit trade before we publish order
|
||||||
|
trade := m.newTradeFromOrder(&order2, false, price)
|
||||||
|
m.executeTrade(trade)
|
||||||
|
|
||||||
|
// unlock the rest balances for limit taker
|
||||||
|
if order.Type == types.OrderTypeLimit {
|
||||||
|
if order.AveragePrice.IsZero() {
|
||||||
|
return nil, nil, fmt.Errorf("the average price of the given limit taker order can not be zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch o.Side {
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
// limit buy taker, the order price is higher than the current best ask price
|
||||||
|
// the executed price is lower than the given price, so we will use less quote currency to buy the base asset.
|
||||||
|
amount := order.Price.Sub(order.AveragePrice).Mul(order.Quantity)
|
||||||
|
if amount.Sign() > 0 {
|
||||||
|
if err := m.account.UnlockBalance(m.Market.QuoteCurrency, amount); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
m.EmitBalanceUpdate(m.account.Balances())
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.SideTypeSell:
|
||||||
|
// limit sell taker, the order price is lower than the current best bid price
|
||||||
|
// the executed price is higher than the given price, so we will get more quote currency back
|
||||||
|
amount := order.AveragePrice.Sub(order.Price).Mul(order.Quantity)
|
||||||
|
if amount.Sign() > 0 {
|
||||||
|
m.account.AddBalance(m.Market.QuoteCurrency, amount)
|
||||||
|
m.EmitBalanceUpdate(m.account.Balances())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the order status
|
||||||
|
order2.Status = types.OrderStatusFilled
|
||||||
|
order2.ExecutedQuantity = order2.Quantity
|
||||||
|
order2.IsWorking = false
|
||||||
|
m.EmitOrderUpdate(order2)
|
||||||
|
|
||||||
|
// let the exchange emit the "FILLED" order update (we need the closed order)
|
||||||
|
// m.EmitOrderUpdate(order2)
|
||||||
|
return &order2, &trade, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For limit maker orders (open status)
|
||||||
|
switch o.Side {
|
||||||
|
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
m.mu.Lock()
|
||||||
|
m.bidOrders = append(m.bidOrders, order)
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
case types.SideTypeSell:
|
||||||
|
m.mu.Lock()
|
||||||
|
m.askOrders = append(m.askOrders, order)
|
||||||
|
m.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.EmitOrderUpdate(order) // emit order New status
|
||||||
|
return &order, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) executeTrade(trade types.Trade) {
|
||||||
|
var err error
|
||||||
|
// execute trade, update account balances
|
||||||
|
if trade.IsBuyer {
|
||||||
|
err = m.account.UseLockedBalance(m.Market.QuoteCurrency, trade.QuoteQuantity)
|
||||||
|
|
||||||
|
// all-in buy trade, we can only deduct the fee from the quote quantity and re-calculate the base quantity
|
||||||
|
switch trade.FeeCurrency {
|
||||||
|
case m.Market.QuoteCurrency:
|
||||||
|
m.account.AddBalance(m.Market.QuoteCurrency, trade.Fee.Neg())
|
||||||
|
m.account.AddBalance(m.Market.BaseCurrency, trade.Quantity)
|
||||||
|
case m.Market.BaseCurrency:
|
||||||
|
m.account.AddBalance(m.Market.BaseCurrency, trade.Quantity.Sub(trade.Fee))
|
||||||
|
default:
|
||||||
|
m.account.AddBalance(m.Market.BaseCurrency, trade.Quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else { // sell trade
|
||||||
|
err = m.account.UseLockedBalance(m.Market.BaseCurrency, trade.Quantity)
|
||||||
|
|
||||||
|
switch trade.FeeCurrency {
|
||||||
|
case m.Market.QuoteCurrency:
|
||||||
|
m.account.AddBalance(m.Market.QuoteCurrency, trade.QuoteQuantity.Sub(trade.Fee))
|
||||||
|
case m.Market.BaseCurrency:
|
||||||
|
m.account.AddBalance(m.Market.BaseCurrency, trade.Fee.Neg())
|
||||||
|
m.account.AddBalance(m.Market.QuoteCurrency, trade.QuoteQuantity)
|
||||||
|
default:
|
||||||
|
m.account.AddBalance(m.Market.QuoteCurrency, trade.QuoteQuantity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.Wrapf(err, "executeTrade exception, wanted to use more than the locked balance"))
|
||||||
|
}
|
||||||
|
|
||||||
|
m.EmitTradeUpdate(trade)
|
||||||
|
m.EmitBalanceUpdate(m.account.Balances())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) getFeeRate(isMaker bool) (feeRate fixedpoint.Value) {
|
||||||
|
// BINANCE uses 0.1% for both maker and taker
|
||||||
|
// MAX uses 0.050% for maker and 0.15% for taker
|
||||||
|
if isMaker {
|
||||||
|
feeRate = m.account.MakerFeeRate
|
||||||
|
} else {
|
||||||
|
feeRate = m.account.TakerFeeRate
|
||||||
|
}
|
||||||
|
return feeRate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool, price fixedpoint.Value) types.Trade {
|
||||||
|
// BINANCE uses 0.1% for both maker and taker
|
||||||
|
// MAX uses 0.050% for maker and 0.15% for taker
|
||||||
|
var feeRate = m.getFeeRate(isMaker)
|
||||||
|
var quoteQuantity = order.Quantity.Mul(price)
|
||||||
|
var fee fixedpoint.Value
|
||||||
|
var feeCurrency string
|
||||||
|
|
||||||
|
if m.feeModeFunction != nil {
|
||||||
|
fee, feeCurrency = m.feeModeFunction(order, &m.Market, feeRate)
|
||||||
|
} else {
|
||||||
|
fee, feeCurrency = feeModeFunctionQuote(order, &m.Market, feeRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update order time
|
||||||
|
order.UpdateTime = types.Time(m.currentTime)
|
||||||
|
|
||||||
|
var id = incTradeID()
|
||||||
|
return types.Trade{
|
||||||
|
ID: id,
|
||||||
|
OrderID: order.OrderID,
|
||||||
|
Exchange: types.ExchangeBacktest,
|
||||||
|
Price: price,
|
||||||
|
Quantity: order.Quantity,
|
||||||
|
QuoteQuantity: quoteQuantity,
|
||||||
|
Symbol: order.Symbol,
|
||||||
|
Side: order.Side,
|
||||||
|
IsBuyer: order.Side == types.SideTypeBuy,
|
||||||
|
IsMaker: isMaker,
|
||||||
|
Time: types.Time(m.currentTime),
|
||||||
|
Fee: fee,
|
||||||
|
FeeCurrency: feeCurrency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buyToPrice means price go up and the limit sell should be triggered
|
||||||
|
func (m *SimplePriceMatching) buyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
|
||||||
|
klineMatchingLogger.Debugf("kline buy to price %s", price.String())
|
||||||
|
|
||||||
|
var bidOrders []types.Order
|
||||||
|
for _, o := range m.bidOrders {
|
||||||
|
switch o.Type {
|
||||||
|
|
||||||
|
case types.OrderTypeStopMarket:
|
||||||
|
// the price is still lower than the stop price, we will put the order back to the list
|
||||||
|
if price.Compare(o.StopPrice) < 0 {
|
||||||
|
// not triggering it, put it back
|
||||||
|
bidOrders = append(bidOrders, o)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Type = types.OrderTypeMarket
|
||||||
|
o.ExecutedQuantity = o.Quantity
|
||||||
|
o.Price = price
|
||||||
|
o.Status = types.OrderStatusFilled
|
||||||
|
closedOrders = append(closedOrders, o)
|
||||||
|
|
||||||
|
case types.OrderTypeStopLimit:
|
||||||
|
// the price is still lower than the stop price, we will put the order back to the list
|
||||||
|
if price.Compare(o.StopPrice) < 0 {
|
||||||
|
bidOrders = append(bidOrders, o)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert this order to limit order
|
||||||
|
// we use value object here, so it's a copy
|
||||||
|
o.Type = types.OrderTypeLimit
|
||||||
|
|
||||||
|
// is it a taker order?
|
||||||
|
// higher than the current price, then it's a taker order
|
||||||
|
if o.Price.Compare(price) >= 0 {
|
||||||
|
// limit buy taker order, move it to the closed order
|
||||||
|
// we assume that we have no price slippage here, so the latest price will be the executed price
|
||||||
|
o.AveragePrice = price
|
||||||
|
o.ExecutedQuantity = o.Quantity
|
||||||
|
o.Status = types.OrderStatusFilled
|
||||||
|
closedOrders = append(closedOrders, o)
|
||||||
|
} else {
|
||||||
|
// keep it as a maker order
|
||||||
|
bidOrders = append(bidOrders, o)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
bidOrders = append(bidOrders, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.bidOrders = bidOrders
|
||||||
|
|
||||||
|
var askOrders []types.Order
|
||||||
|
for _, o := range m.askOrders {
|
||||||
|
switch o.Type {
|
||||||
|
|
||||||
|
case types.OrderTypeStopMarket:
|
||||||
|
// should we trigger the order
|
||||||
|
if price.Compare(o.StopPrice) < 0 {
|
||||||
|
// not triggering it, put it back
|
||||||
|
askOrders = append(askOrders, o)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Type = types.OrderTypeMarket
|
||||||
|
o.ExecutedQuantity = o.Quantity
|
||||||
|
o.Price = price
|
||||||
|
o.Status = types.OrderStatusFilled
|
||||||
|
closedOrders = append(closedOrders, o)
|
||||||
|
|
||||||
|
case types.OrderTypeStopLimit:
|
||||||
|
// should we trigger the order?
|
||||||
|
if price.Compare(o.StopPrice) < 0 {
|
||||||
|
askOrders = append(askOrders, o)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Type = types.OrderTypeLimit
|
||||||
|
|
||||||
|
// is it a taker order?
|
||||||
|
// higher than the current price, then it's a taker order
|
||||||
|
if o.Price.Compare(price) <= 0 {
|
||||||
|
// limit sell order as taker, move it to the closed order
|
||||||
|
// we assume that we have no price slippage here, so the latest price will be the executed price
|
||||||
|
// TODO: simulate slippage here
|
||||||
|
o.AveragePrice = price
|
||||||
|
o.ExecutedQuantity = o.Quantity
|
||||||
|
o.Status = types.OrderStatusFilled
|
||||||
|
closedOrders = append(closedOrders, o)
|
||||||
|
} else {
|
||||||
|
// maker order
|
||||||
|
askOrders = append(askOrders, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
|
||||||
|
if price.Compare(o.Price) >= 0 {
|
||||||
|
o.ExecutedQuantity = o.Quantity
|
||||||
|
o.Status = types.OrderStatusFilled
|
||||||
|
closedOrders = append(closedOrders, o)
|
||||||
|
} else {
|
||||||
|
askOrders = append(askOrders, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
askOrders = append(askOrders, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
m.askOrders = askOrders
|
||||||
|
m.lastPrice = price
|
||||||
|
|
||||||
|
for i := range closedOrders {
|
||||||
|
o := closedOrders[i]
|
||||||
|
executedPrice := o.Price
|
||||||
|
if !o.AveragePrice.IsZero() {
|
||||||
|
executedPrice = o.AveragePrice
|
||||||
|
}
|
||||||
|
|
||||||
|
trade := m.newTradeFromOrder(&o, !isTakerOrder(o), executedPrice)
|
||||||
|
m.executeTrade(trade)
|
||||||
|
closedOrders[i] = o
|
||||||
|
|
||||||
|
trades = append(trades, trade)
|
||||||
|
|
||||||
|
m.EmitOrderUpdate(o)
|
||||||
|
|
||||||
|
m.closedOrders[o.OrderID] = o
|
||||||
|
}
|
||||||
|
|
||||||
|
return closedOrders, trades
|
||||||
|
}
|
||||||
|
|
||||||
|
// sellToPrice simulates the price trend in down direction.
|
||||||
|
// When price goes down, buy orders should be executed, and the stop orders should be triggered.
|
||||||
|
func (m *SimplePriceMatching) sellToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
|
||||||
|
klineMatchingLogger.Debugf("kline sell to price %s", price.String())
|
||||||
|
|
||||||
|
// in this section we handle --- the price goes lower, and we trigger the stop sell
|
||||||
|
var askOrders []types.Order
|
||||||
|
for _, o := range m.askOrders {
|
||||||
|
switch o.Type {
|
||||||
|
|
||||||
|
case types.OrderTypeStopMarket:
|
||||||
|
// should we trigger the order
|
||||||
|
if price.Compare(o.StopPrice) > 0 {
|
||||||
|
askOrders = append(askOrders, o)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Type = types.OrderTypeMarket
|
||||||
|
o.ExecutedQuantity = o.Quantity
|
||||||
|
o.Price = price
|
||||||
|
o.Status = types.OrderStatusFilled
|
||||||
|
closedOrders = append(closedOrders, o)
|
||||||
|
|
||||||
|
case types.OrderTypeStopLimit:
|
||||||
|
// if the price is lower than the stop price
|
||||||
|
// we should trigger the stop sell order
|
||||||
|
if price.Compare(o.StopPrice) > 0 {
|
||||||
|
askOrders = append(askOrders, o)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Type = types.OrderTypeLimit
|
||||||
|
|
||||||
|
// handle TAKER SELL
|
||||||
|
// if the order price is lower than the current price
|
||||||
|
// it's a taker order
|
||||||
|
if o.Price.Compare(price) <= 0 {
|
||||||
|
o.AveragePrice = price
|
||||||
|
o.ExecutedQuantity = o.Quantity
|
||||||
|
o.Status = types.OrderStatusFilled
|
||||||
|
closedOrders = append(closedOrders, o)
|
||||||
|
} else {
|
||||||
|
askOrders = append(askOrders, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
askOrders = append(askOrders, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.askOrders = askOrders
|
||||||
|
|
||||||
|
var bidOrders []types.Order
|
||||||
|
for _, o := range m.bidOrders {
|
||||||
|
switch o.Type {
|
||||||
|
|
||||||
|
case types.OrderTypeStopMarket:
|
||||||
|
// price goes down and if the stop price is still lower than the current price
|
||||||
|
// or the stop price is not touched
|
||||||
|
// then we should skip this order
|
||||||
|
if price.Compare(o.StopPrice) > 0 {
|
||||||
|
bidOrders = append(bidOrders, o)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Type = types.OrderTypeMarket
|
||||||
|
o.ExecutedQuantity = o.Quantity
|
||||||
|
o.Price = price
|
||||||
|
o.Status = types.OrderStatusFilled
|
||||||
|
closedOrders = append(closedOrders, o)
|
||||||
|
|
||||||
|
case types.OrderTypeStopLimit:
|
||||||
|
// price goes down and if the stop price is still lower than the current price
|
||||||
|
// or the stop price is not touched
|
||||||
|
// then we should skip this order
|
||||||
|
if price.Compare(o.StopPrice) > 0 {
|
||||||
|
bidOrders = append(bidOrders, o)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Type = types.OrderTypeLimit
|
||||||
|
|
||||||
|
// handle TAKER order
|
||||||
|
if o.Price.Compare(price) >= 0 {
|
||||||
|
o.AveragePrice = price
|
||||||
|
o.ExecutedQuantity = o.Quantity
|
||||||
|
o.Status = types.OrderStatusFilled
|
||||||
|
closedOrders = append(closedOrders, o)
|
||||||
|
} else {
|
||||||
|
bidOrders = append(bidOrders, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
|
||||||
|
if price.Compare(o.Price) <= 0 {
|
||||||
|
o.ExecutedQuantity = o.Quantity
|
||||||
|
o.Status = types.OrderStatusFilled
|
||||||
|
closedOrders = append(closedOrders, o)
|
||||||
|
} else {
|
||||||
|
bidOrders = append(bidOrders, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
bidOrders = append(bidOrders, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.bidOrders = bidOrders
|
||||||
|
m.lastPrice = price
|
||||||
|
|
||||||
|
for i := range closedOrders {
|
||||||
|
o := closedOrders[i]
|
||||||
|
executedPrice := o.Price
|
||||||
|
if !o.AveragePrice.IsZero() {
|
||||||
|
executedPrice = o.AveragePrice
|
||||||
|
}
|
||||||
|
|
||||||
|
trade := m.newTradeFromOrder(&o, !isTakerOrder(o), executedPrice)
|
||||||
|
m.executeTrade(trade)
|
||||||
|
closedOrders[i] = o
|
||||||
|
|
||||||
|
trades = append(trades, trade)
|
||||||
|
|
||||||
|
m.EmitOrderUpdate(o)
|
||||||
|
|
||||||
|
m.closedOrders[o.OrderID] = o
|
||||||
|
}
|
||||||
|
|
||||||
|
return closedOrders, trades
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) getOrder(orderID uint64) (types.Order, bool) {
|
||||||
|
if o, ok := m.closedOrders[orderID]; ok {
|
||||||
|
return o, true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range m.bidOrders {
|
||||||
|
if o.OrderID == orderID {
|
||||||
|
return o, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range m.askOrders {
|
||||||
|
if o.OrderID == orderID {
|
||||||
|
return o, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.Order{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) processKLine(kline types.KLine) {
|
||||||
|
m.currentTime = kline.EndTime.Time()
|
||||||
|
|
||||||
|
if m.lastPrice.IsZero() {
|
||||||
|
m.lastPrice = kline.Open
|
||||||
|
} else {
|
||||||
|
if m.lastPrice.Compare(kline.Open) > 0 {
|
||||||
|
m.sellToPrice(kline.Open)
|
||||||
|
} else {
|
||||||
|
m.buyToPrice(kline.Open)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kline.Direction() {
|
||||||
|
case types.DirectionDown:
|
||||||
|
if kline.High.Compare(kline.Open) >= 0 {
|
||||||
|
m.buyToPrice(kline.High)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if low is lower than close, sell to low first, and then buy up to close
|
||||||
|
if kline.Low.Compare(kline.Close) < 0 {
|
||||||
|
m.sellToPrice(kline.Low)
|
||||||
|
m.buyToPrice(kline.Close)
|
||||||
|
} else {
|
||||||
|
m.sellToPrice(kline.Close)
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.DirectionUp:
|
||||||
|
if kline.Low.Compare(kline.Open) <= 0 {
|
||||||
|
m.sellToPrice(kline.Low)
|
||||||
|
}
|
||||||
|
|
||||||
|
if kline.High.Compare(kline.Close) > 0 {
|
||||||
|
m.buyToPrice(kline.High)
|
||||||
|
m.sellToPrice(kline.Close)
|
||||||
|
} else {
|
||||||
|
m.buyToPrice(kline.Close)
|
||||||
|
}
|
||||||
|
default: // no trade up or down
|
||||||
|
if m.lastPrice.IsZero() {
|
||||||
|
m.buyToPrice(kline.Close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.lastKLine = kline
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order {
|
||||||
|
return types.Order{
|
||||||
|
OrderID: orderID,
|
||||||
|
SubmitOrder: o,
|
||||||
|
Exchange: types.ExchangeBacktest,
|
||||||
|
Status: types.OrderStatusNew,
|
||||||
|
ExecutedQuantity: fixedpoint.Zero,
|
||||||
|
IsWorking: true,
|
||||||
|
CreationTime: types.Time(m.currentTime),
|
||||||
|
UpdateTime: types.Time(m.currentTime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTakerOrder(o types.Order) bool {
|
||||||
|
if o.AveragePrice.IsZero() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch o.Side {
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
return o.AveragePrice.Compare(o.Price) < 0
|
||||||
|
|
||||||
|
case types.SideTypeSell:
|
||||||
|
return o.AveragePrice.Compare(o.Price) > 0
|
||||||
|
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLimitTakerOrder(o types.SubmitOrder, currentPrice fixedpoint.Value) bool {
|
||||||
|
if currentPrice.IsZero() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.Type == types.OrderTypeLimit && ((o.Side == types.SideTypeBuy && o.Price.Compare(currentPrice) >= 0) ||
|
||||||
|
(o.Side == types.SideTypeSell && o.Price.Compare(currentPrice) <= 0))
|
||||||
|
}
|
529
pkg/backtest/matching_test.go
Normal file
529
pkg/backtest/matching_test.go
Normal file
|
@ -0,0 +1,529 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newLimitOrder(symbol string, side types.SideType, price, quantity float64) types.SubmitOrder {
|
||||||
|
return types.SubmitOrder{
|
||||||
|
Symbol: symbol,
|
||||||
|
Side: side,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(quantity),
|
||||||
|
Price: fixedpoint.NewFromFloat(price),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimplePriceMatching_orderUpdate(t *testing.T) {
|
||||||
|
account := &types.Account{
|
||||||
|
MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
|
||||||
|
TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
|
||||||
|
}
|
||||||
|
account.UpdateBalances(types.BalanceMap{
|
||||||
|
"USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(10000.0)},
|
||||||
|
})
|
||||||
|
market := types.Market{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
PricePrecision: 8,
|
||||||
|
VolumePrecision: 8,
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
BaseCurrency: "BTC",
|
||||||
|
MinNotional: fixedpoint.MustNewFromString("0.001"),
|
||||||
|
MinAmount: fixedpoint.MustNewFromString("10.0"),
|
||||||
|
MinQuantity: fixedpoint.MustNewFromString("0.001"),
|
||||||
|
StepSize: fixedpoint.MustNewFromString("0.00001"),
|
||||||
|
TickSize: fixedpoint.MustNewFromString("0.01"),
|
||||||
|
}
|
||||||
|
|
||||||
|
t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
engine := &SimplePriceMatching{
|
||||||
|
account: account,
|
||||||
|
Market: market,
|
||||||
|
currentTime: t1,
|
||||||
|
closedOrders: make(map[uint64]types.Order),
|
||||||
|
lastPrice: fixedpoint.NewFromFloat(25000),
|
||||||
|
}
|
||||||
|
|
||||||
|
orderUpdateCnt := 0
|
||||||
|
orderUpdateNewStatusCnt := 0
|
||||||
|
orderUpdateFilledStatusCnt := 0
|
||||||
|
var lastOrder types.Order
|
||||||
|
engine.OnOrderUpdate(func(order types.Order) {
|
||||||
|
lastOrder = order
|
||||||
|
|
||||||
|
orderUpdateCnt++
|
||||||
|
switch order.Status {
|
||||||
|
case types.OrderStatusNew:
|
||||||
|
orderUpdateNewStatusCnt++
|
||||||
|
|
||||||
|
case types.OrderStatusFilled:
|
||||||
|
orderUpdateFilledStatusCnt++
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// maker order
|
||||||
|
_, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 24000.0, 0.1))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, orderUpdateCnt) // should get new status
|
||||||
|
assert.Equal(t, 1, orderUpdateNewStatusCnt) // should get new status
|
||||||
|
assert.Equal(t, 0, orderUpdateFilledStatusCnt) // should get new status
|
||||||
|
assert.Equal(t, types.OrderStatusNew, lastOrder.Status)
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(0.0), lastOrder.ExecutedQuantity)
|
||||||
|
|
||||||
|
t2 := t1.Add(time.Minute)
|
||||||
|
|
||||||
|
// should match 25000, 24000
|
||||||
|
k := newKLine("BTCUSDT", types.Interval1m, t2, 26000, 27000, 23000, 25000)
|
||||||
|
engine.processKLine(k)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, orderUpdateCnt) // should got new and filled
|
||||||
|
assert.Equal(t, 1, orderUpdateNewStatusCnt) // should got new status
|
||||||
|
assert.Equal(t, 1, orderUpdateFilledStatusCnt) // should got new status
|
||||||
|
assert.Equal(t, types.OrderStatusFilled, lastOrder.Status)
|
||||||
|
assert.Equal(t, "0.1", lastOrder.ExecutedQuantity.String())
|
||||||
|
assert.Equal(t, lastOrder.Quantity.String(), lastOrder.ExecutedQuantity.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimplePriceMatching_CancelOrder(t *testing.T) {
|
||||||
|
account := getTestAccount()
|
||||||
|
market := getTestMarket()
|
||||||
|
t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
engine := &SimplePriceMatching{
|
||||||
|
account: account,
|
||||||
|
Market: market,
|
||||||
|
currentTime: t1,
|
||||||
|
closedOrders: make(map[uint64]types.Order),
|
||||||
|
lastPrice: fixedpoint.NewFromFloat(30000.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
createdOrder1, trade1, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 20000.0, 0.1))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, trade1)
|
||||||
|
assert.Len(t, engine.bidOrders, 1)
|
||||||
|
assert.Len(t, engine.askOrders, 0)
|
||||||
|
|
||||||
|
createdOrder2, trade2, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeSell, 40000.0, 0.1))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, trade2)
|
||||||
|
assert.Len(t, engine.bidOrders, 1)
|
||||||
|
assert.Len(t, engine.askOrders, 1)
|
||||||
|
|
||||||
|
if assert.NotNil(t, createdOrder1) {
|
||||||
|
retOrder, err := engine.CancelOrder(*createdOrder1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, retOrder)
|
||||||
|
assert.Len(t, engine.bidOrders, 0)
|
||||||
|
assert.Len(t, engine.askOrders, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if assert.NotNil(t, createdOrder2) {
|
||||||
|
retOrder, err := engine.CancelOrder(*createdOrder2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, retOrder)
|
||||||
|
assert.Len(t, engine.bidOrders, 0)
|
||||||
|
assert.Len(t, engine.askOrders, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimplePriceMatching_processKLine(t *testing.T) {
|
||||||
|
account := getTestAccount()
|
||||||
|
market := getTestMarket()
|
||||||
|
|
||||||
|
t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
engine := &SimplePriceMatching{
|
||||||
|
account: account,
|
||||||
|
Market: market,
|
||||||
|
currentTime: t1,
|
||||||
|
closedOrders: make(map[uint64]types.Order),
|
||||||
|
lastPrice: fixedpoint.NewFromFloat(30000.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i <= 5; i++ {
|
||||||
|
var p = 20000.0 + float64(i)*1000.0
|
||||||
|
_, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, p, 0.001))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t2 := t1.Add(time.Minute)
|
||||||
|
|
||||||
|
// should match 25000, 24000
|
||||||
|
k := newKLine("BTCUSDT", types.Interval1m, t2, 30000, 27000, 23000, 25000)
|
||||||
|
assert.Equal(t, t2.Add(time.Minute-time.Millisecond), k.EndTime.Time())
|
||||||
|
|
||||||
|
engine.processKLine(k)
|
||||||
|
assert.Equal(t, 3, len(engine.bidOrders))
|
||||||
|
assert.Len(t, engine.bidOrders, 3)
|
||||||
|
assert.Equal(t, 3, len(engine.closedOrders))
|
||||||
|
|
||||||
|
for _, o := range engine.closedOrders {
|
||||||
|
assert.Equal(t, k.EndTime.Time(), o.UpdateTime.Time())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newKLine(symbol string, interval types.Interval, startTime time.Time, o, h, l, c float64) types.KLine {
|
||||||
|
return types.KLine{
|
||||||
|
Symbol: symbol,
|
||||||
|
StartTime: types.Time(startTime),
|
||||||
|
EndTime: types.Time(startTime.Add(interval.Duration() - time.Millisecond)),
|
||||||
|
Interval: interval,
|
||||||
|
Open: fixedpoint.NewFromFloat(o),
|
||||||
|
High: fixedpoint.NewFromFloat(h),
|
||||||
|
Low: fixedpoint.NewFromFloat(l),
|
||||||
|
Close: fixedpoint.NewFromFloat(c),
|
||||||
|
Closed: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTestMarket returns the BTCUSDT market information
|
||||||
|
// for tests, we always use BTCUSDT
|
||||||
|
func getTestMarket() types.Market {
|
||||||
|
market := types.Market{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
PricePrecision: 8,
|
||||||
|
VolumePrecision: 8,
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
BaseCurrency: "BTC",
|
||||||
|
MinNotional: fixedpoint.MustNewFromString("0.001"),
|
||||||
|
MinAmount: fixedpoint.MustNewFromString("10.0"),
|
||||||
|
MinQuantity: fixedpoint.MustNewFromString("0.001"),
|
||||||
|
StepSize: fixedpoint.MustNewFromString("0.00001"),
|
||||||
|
TickSize: fixedpoint.MustNewFromString("0.01"),
|
||||||
|
}
|
||||||
|
return market
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTestAccount() *types.Account {
|
||||||
|
account := &types.Account{
|
||||||
|
MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
|
||||||
|
TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
|
||||||
|
}
|
||||||
|
account.UpdateBalances(types.BalanceMap{
|
||||||
|
"USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(1000000.0)},
|
||||||
|
"BTC": {Currency: "BTC", Available: fixedpoint.NewFromFloat(100.0)},
|
||||||
|
})
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimplePriceMatching_LimitBuyTakerOrder(t *testing.T) {
|
||||||
|
account := getTestAccount()
|
||||||
|
market := getTestMarket()
|
||||||
|
engine := &SimplePriceMatching{
|
||||||
|
account: account,
|
||||||
|
Market: market,
|
||||||
|
closedOrders: make(map[uint64]types.Order),
|
||||||
|
lastPrice: fixedpoint.NewFromFloat(19000.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
takerOrder := types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(20000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
}
|
||||||
|
createdOrder, trade, err := engine.PlaceOrder(takerOrder)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
t.Logf("created order: %+v", createdOrder)
|
||||||
|
t.Logf("executed trade: %+v", trade)
|
||||||
|
|
||||||
|
assert.Equal(t, "19000", trade.Price.String())
|
||||||
|
assert.Equal(t, "19000", createdOrder.AveragePrice.String())
|
||||||
|
assert.Equal(t, "20000", createdOrder.Price.String())
|
||||||
|
|
||||||
|
usdt, ok := account.Balance("USDT")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.True(t, usdt.Locked.IsZero())
|
||||||
|
|
||||||
|
btc, ok := account.Balance("BTC")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.True(t, btc.Locked.IsZero())
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(100.0).Add(createdOrder.Quantity).String(), btc.Available.String())
|
||||||
|
|
||||||
|
usedQuoteAmount := createdOrder.AveragePrice.Mul(createdOrder.Quantity)
|
||||||
|
assert.Equal(t, "USDT", trade.FeeCurrency)
|
||||||
|
assert.Equal(t, usdt.Available.String(), fixedpoint.NewFromFloat(1000000.0).Sub(usedQuoteAmount).Sub(trade.Fee).String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) {
|
||||||
|
account := getTestAccount()
|
||||||
|
market := getTestMarket()
|
||||||
|
engine := &SimplePriceMatching{
|
||||||
|
account: account,
|
||||||
|
Market: market,
|
||||||
|
closedOrders: make(map[uint64]types.Order),
|
||||||
|
lastPrice: fixedpoint.NewFromFloat(19000.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
stopBuyOrder := types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Type: types.OrderTypeStopLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(22000.0),
|
||||||
|
StopPrice: fixedpoint.NewFromFloat(21000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
}
|
||||||
|
createdOrder, trade, err := engine.PlaceOrder(stopBuyOrder)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, trade, "place stop order should not trigger the stop buy")
|
||||||
|
assert.NotNil(t, createdOrder, "place stop order should not trigger the stop buy")
|
||||||
|
|
||||||
|
// place some limit orders, so we ensure that the remaining orders are not removed.
|
||||||
|
_, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeBuy, 18000, 0.01))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeSell, 32000, 0.01))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 2, len(engine.bidOrders))
|
||||||
|
assert.Equal(t, 1, len(engine.askOrders))
|
||||||
|
|
||||||
|
closedOrders, trades := engine.buyToPrice(fixedpoint.NewFromFloat(20000.0))
|
||||||
|
assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy")
|
||||||
|
assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy")
|
||||||
|
assert.Equal(t, 2, len(engine.bidOrders), "bid orders should be the same")
|
||||||
|
assert.Equal(t, 1, len(engine.askOrders), "ask orders should be the same")
|
||||||
|
|
||||||
|
closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(21001.0))
|
||||||
|
assert.Len(t, closedOrders, 1, "should trigger the stop buy order")
|
||||||
|
assert.Len(t, trades, 1, "should have stop order trade executed")
|
||||||
|
|
||||||
|
assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status)
|
||||||
|
assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type)
|
||||||
|
assert.Equal(t, "21001", trades[0].Price.String())
|
||||||
|
assert.Equal(t, "22000", closedOrders[0].Price.String(), "order.Price should not be adjusted")
|
||||||
|
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(21001.0).String(), engine.lastPrice.String())
|
||||||
|
|
||||||
|
stopOrder2 := types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Type: types.OrderTypeStopLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(22000.0),
|
||||||
|
StopPrice: fixedpoint.NewFromFloat(21000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
}
|
||||||
|
createdOrder, trade, err = engine.PlaceOrder(stopOrder2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, trade, "place stop order should not trigger the stop buy")
|
||||||
|
assert.NotNil(t, createdOrder, "place stop order should not trigger the stop buy")
|
||||||
|
assert.Len(t, engine.bidOrders, 2)
|
||||||
|
|
||||||
|
closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20500.0))
|
||||||
|
assert.Len(t, closedOrders, 1, "should trigger the stop buy order")
|
||||||
|
assert.Len(t, trades, 1, "should have stop order trade executed")
|
||||||
|
assert.Len(t, engine.bidOrders, 1, "should left one bid order")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
|
||||||
|
account := getTestAccount()
|
||||||
|
market := getTestMarket()
|
||||||
|
engine := &SimplePriceMatching{
|
||||||
|
account: account,
|
||||||
|
Market: market,
|
||||||
|
closedOrders: make(map[uint64]types.Order),
|
||||||
|
lastPrice: fixedpoint.NewFromFloat(22000.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
stopSellOrder := types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Type: types.OrderTypeStopLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(20000.0),
|
||||||
|
StopPrice: fixedpoint.NewFromFloat(21000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
}
|
||||||
|
createdOrder, trade, err := engine.PlaceOrder(stopSellOrder)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, trade, "place stop order should not trigger the stop sell")
|
||||||
|
assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell")
|
||||||
|
|
||||||
|
// place some limit orders, so we ensure that the remaining orders are not removed.
|
||||||
|
_, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeBuy, 18000, 0.01))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeSell, 32000, 0.01))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(engine.bidOrders))
|
||||||
|
assert.Equal(t, 2, len(engine.askOrders))
|
||||||
|
|
||||||
|
closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(21500.0))
|
||||||
|
assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy")
|
||||||
|
assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy")
|
||||||
|
assert.Equal(t, 1, len(engine.bidOrders))
|
||||||
|
assert.Equal(t, 2, len(engine.askOrders))
|
||||||
|
|
||||||
|
closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20990.0))
|
||||||
|
assert.Len(t, closedOrders, 1, "should trigger the stop sell order")
|
||||||
|
assert.Len(t, trades, 1, "should have stop order trade executed")
|
||||||
|
assert.Equal(t, 1, len(engine.bidOrders))
|
||||||
|
assert.Equal(t, 1, len(engine.askOrders))
|
||||||
|
|
||||||
|
assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status)
|
||||||
|
assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type)
|
||||||
|
assert.Equal(t, "20000", closedOrders[0].Price.String(), "limit order price should not be changed")
|
||||||
|
assert.Equal(t, "20990", trades[0].Price.String())
|
||||||
|
assert.Equal(t, "20990", engine.lastPrice.String())
|
||||||
|
|
||||||
|
// place a stop limit sell order with a higher price than the current price
|
||||||
|
stopOrder2 := types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Type: types.OrderTypeStopLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(20000.0),
|
||||||
|
StopPrice: fixedpoint.NewFromFloat(21000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
}
|
||||||
|
|
||||||
|
createdOrder, trade, err = engine.PlaceOrder(stopOrder2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, trade, "place stop order should not trigger the stop sell")
|
||||||
|
assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell")
|
||||||
|
|
||||||
|
closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(21000.0))
|
||||||
|
if assert.Len(t, closedOrders, 1, "should trigger the stop sell order") {
|
||||||
|
assert.Len(t, trades, 1, "should have stop order trade executed")
|
||||||
|
assert.Equal(t, types.SideTypeSell, closedOrders[0].Side)
|
||||||
|
assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status)
|
||||||
|
assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type)
|
||||||
|
assert.Equal(t, "21000", trades[0].Price.String(), "trade price should be the kline price not the order price")
|
||||||
|
assert.Equal(t, "21000", engine.lastPrice.String(), "engine last price should be updated correctly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimplePriceMatching_StopMarketOrderSell(t *testing.T) {
|
||||||
|
account := getTestAccount()
|
||||||
|
market := getTestMarket()
|
||||||
|
engine := &SimplePriceMatching{
|
||||||
|
account: account,
|
||||||
|
Market: market,
|
||||||
|
closedOrders: make(map[uint64]types.Order),
|
||||||
|
lastPrice: fixedpoint.NewFromFloat(22000.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
stopOrder := types.SubmitOrder{
|
||||||
|
Symbol: market.Symbol,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Type: types.OrderTypeStopMarket,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||||
|
Price: fixedpoint.NewFromFloat(20000.0),
|
||||||
|
StopPrice: fixedpoint.NewFromFloat(21000.0),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
}
|
||||||
|
createdOrder, trade, err := engine.PlaceOrder(stopOrder)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, trade, "place stop order should not trigger the stop sell")
|
||||||
|
assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell")
|
||||||
|
|
||||||
|
closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(21500.0))
|
||||||
|
assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy")
|
||||||
|
assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy")
|
||||||
|
|
||||||
|
closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20990.0))
|
||||||
|
assert.Len(t, closedOrders, 1, "should trigger the stop sell order")
|
||||||
|
assert.Len(t, trades, 1, "should have stop order trade executed")
|
||||||
|
|
||||||
|
assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status)
|
||||||
|
assert.Equal(t, types.OrderTypeMarket, closedOrders[0].Type)
|
||||||
|
assert.Equal(t, fixedpoint.NewFromFloat(20990.0), trades[0].Price, "trade price should be adjusted to the last price")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
|
||||||
|
account := getTestAccount()
|
||||||
|
market := getTestMarket()
|
||||||
|
engine := &SimplePriceMatching{
|
||||||
|
account: account,
|
||||||
|
Market: market,
|
||||||
|
closedOrders: make(map[uint64]types.Order),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
_, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 8000.0-float64(i), 1.0))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Len(t, engine.bidOrders, 5)
|
||||||
|
assert.Len(t, engine.askOrders, 0)
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
_, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeSell, 9000.0+float64(i), 1.0))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Len(t, engine.bidOrders, 5)
|
||||||
|
assert.Len(t, engine.askOrders, 5)
|
||||||
|
|
||||||
|
closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(8100.0))
|
||||||
|
assert.Len(t, closedOrders, 0)
|
||||||
|
assert.Len(t, trades, 0)
|
||||||
|
|
||||||
|
closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(8000.0))
|
||||||
|
assert.Len(t, closedOrders, 1)
|
||||||
|
assert.Len(t, trades, 1)
|
||||||
|
for _, trade := range trades {
|
||||||
|
assert.True(t, trade.IsBuyer)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range closedOrders {
|
||||||
|
assert.Equal(t, types.SideTypeBuy, o.Side)
|
||||||
|
}
|
||||||
|
|
||||||
|
closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(7000.0))
|
||||||
|
assert.Len(t, closedOrders, 4)
|
||||||
|
assert.Len(t, trades, 4)
|
||||||
|
|
||||||
|
closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(8900.0))
|
||||||
|
assert.Len(t, closedOrders, 0)
|
||||||
|
assert.Len(t, trades, 0)
|
||||||
|
|
||||||
|
closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(9000.0))
|
||||||
|
assert.Len(t, closedOrders, 1)
|
||||||
|
assert.Len(t, trades, 1)
|
||||||
|
for _, o := range closedOrders {
|
||||||
|
assert.Equal(t, types.SideTypeSell, o.Side)
|
||||||
|
}
|
||||||
|
for _, trade := range trades {
|
||||||
|
assert.Equal(t, types.SideTypeSell, trade.Side)
|
||||||
|
}
|
||||||
|
|
||||||
|
closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(9500.0))
|
||||||
|
assert.Len(t, closedOrders, 4)
|
||||||
|
assert.Len(t, trades, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimplePriceMatching_LimitTakerOrder(t *testing.T) {
|
||||||
|
account := getTestAccount()
|
||||||
|
market := getTestMarket()
|
||||||
|
engine := &SimplePriceMatching{
|
||||||
|
account: account,
|
||||||
|
Market: market,
|
||||||
|
closedOrders: make(map[uint64]types.Order),
|
||||||
|
lastPrice: fixedpoint.NewFromFloat(20000.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
closedOrder, trade, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 21000.0, 1.0))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.NotNil(t, closedOrder) {
|
||||||
|
if assert.NotNil(t, trade) {
|
||||||
|
assert.Equal(t, "20000", trade.Price.String())
|
||||||
|
assert.False(t, trade.IsMaker, "should be taker")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closedOrder, trade, err = engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeSell, 19000.0, 1.0))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.NotNil(t, closedOrder) {
|
||||||
|
assert.Equal(t, "19000", closedOrder.Price.String())
|
||||||
|
if assert.NotNil(t, trade) {
|
||||||
|
assert.Equal(t, "20000", trade.Price.String())
|
||||||
|
assert.False(t, trade.IsMaker, "should be taker")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
pkg/backtest/priceorder.go
Normal file
77
pkg/backtest/priceorder.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PriceOrder struct {
|
||||||
|
Price fixedpoint.Value
|
||||||
|
Order types.Order
|
||||||
|
}
|
||||||
|
|
||||||
|
type PriceOrderSlice []PriceOrder
|
||||||
|
|
||||||
|
func (slice PriceOrderSlice) Len() int { return len(slice) }
|
||||||
|
func (slice PriceOrderSlice) Less(i, j int) bool { return slice[i].Price.Compare(slice[j].Price) < 0 }
|
||||||
|
func (slice PriceOrderSlice) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] }
|
||||||
|
|
||||||
|
func (slice PriceOrderSlice) InsertAt(idx int, po PriceOrder) PriceOrderSlice {
|
||||||
|
rear := append([]PriceOrder{}, slice[idx:]...)
|
||||||
|
newSlice := append(slice[:idx], po)
|
||||||
|
return append(newSlice, rear...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slice PriceOrderSlice) Remove(price fixedpoint.Value, descending bool) PriceOrderSlice {
|
||||||
|
matched, idx := slice.Find(price, descending)
|
||||||
|
if matched.Price != price {
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(slice[:idx], slice[idx+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slice PriceOrderSlice) First() (PriceOrder, bool) {
|
||||||
|
if len(slice) > 0 {
|
||||||
|
return slice[0], true
|
||||||
|
}
|
||||||
|
return PriceOrder{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindPriceVolumePair finds the pair by the given price, this function is a read-only
|
||||||
|
// operation, so we use the value receiver to avoid copy value from the pointer
|
||||||
|
// If the price is not found, it will return the index where the price can be inserted at.
|
||||||
|
// true for descending (bid orders), false for ascending (ask orders)
|
||||||
|
func (slice PriceOrderSlice) Find(price fixedpoint.Value, descending bool) (pv PriceOrder, idx int) {
|
||||||
|
idx = sort.Search(len(slice), func(i int) bool {
|
||||||
|
if descending {
|
||||||
|
return slice[i].Price.Compare(price) <= 0
|
||||||
|
}
|
||||||
|
return slice[i].Price.Compare(price) >= 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if idx >= len(slice) || slice[idx].Price != price {
|
||||||
|
return pv, idx
|
||||||
|
}
|
||||||
|
|
||||||
|
pv = slice[idx]
|
||||||
|
|
||||||
|
return pv, idx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slice PriceOrderSlice) Upsert(po PriceOrder, descending bool) PriceOrderSlice {
|
||||||
|
if len(slice) == 0 {
|
||||||
|
return append(slice, po)
|
||||||
|
}
|
||||||
|
|
||||||
|
price := po.Price
|
||||||
|
_, idx := slice.Find(price, descending)
|
||||||
|
if idx >= len(slice) || slice[idx].Price != price {
|
||||||
|
return slice.InsertAt(idx, po)
|
||||||
|
}
|
||||||
|
|
||||||
|
slice[idx].Order = po.Order
|
||||||
|
return slice
|
||||||
|
}
|
156
pkg/backtest/recorder.go
Normal file
156
pkg/backtest/recorder.go
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/data/tsv"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Instance interface {
|
||||||
|
ID() string
|
||||||
|
InstanceID() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstancePropertyIndex struct {
|
||||||
|
ID string
|
||||||
|
InstanceID string
|
||||||
|
Property string
|
||||||
|
}
|
||||||
|
|
||||||
|
type StateRecorder struct {
|
||||||
|
outputDirectory string
|
||||||
|
strategies []Instance
|
||||||
|
writers map[types.CsvFormatter]*tsv.Writer
|
||||||
|
lastLines map[types.CsvFormatter][]string
|
||||||
|
manifests Manifests
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStateRecorder(outputDir string) *StateRecorder {
|
||||||
|
return &StateRecorder{
|
||||||
|
outputDirectory: outputDir,
|
||||||
|
writers: make(map[types.CsvFormatter]*tsv.Writer),
|
||||||
|
lastLines: make(map[types.CsvFormatter][]string),
|
||||||
|
manifests: make(Manifests),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StateRecorder) Snapshot() (int, error) {
|
||||||
|
var c int
|
||||||
|
for obj, writer := range r.writers {
|
||||||
|
records := obj.CsvRecords()
|
||||||
|
lastLine, hasLastLine := r.lastLines[obj]
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
if hasLastLine && equalStringSlice(lastLine, record) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Write(record); err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
c++
|
||||||
|
r.lastLines[obj] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Flush()
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StateRecorder) Scan(instance Instance) error {
|
||||||
|
r.strategies = append(r.strategies, instance)
|
||||||
|
|
||||||
|
rt := reflect.TypeOf(instance)
|
||||||
|
rv := reflect.ValueOf(instance)
|
||||||
|
if rt.Kind() == reflect.Ptr {
|
||||||
|
rt = rt.Elem()
|
||||||
|
rv = rv.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rt.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("given object is not a struct: %+v", rt)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < rt.NumField(); i++ {
|
||||||
|
structField := rt.Field(i)
|
||||||
|
if !structField.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := rv.Field(i).Interface()
|
||||||
|
switch o := obj.(type) {
|
||||||
|
|
||||||
|
case types.CsvFormatter: // interface type
|
||||||
|
typeName := strings.ToLower(structField.Type.Elem().Name())
|
||||||
|
if typeName == "" {
|
||||||
|
return fmt.Errorf("%v is a non-defined type", structField.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.newCsvWriter(o, instance, typeName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StateRecorder) formatCsvFilename(instance Instance, objType string) string {
|
||||||
|
return filepath.Join(r.outputDirectory, fmt.Sprintf("%s-%s.tsv", instance.InstanceID(), objType))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StateRecorder) Manifests() Manifests {
|
||||||
|
return r.manifests
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StateRecorder) newCsvWriter(o types.CsvFormatter, instance Instance, typeName string) error {
|
||||||
|
fn := r.formatCsvFilename(instance, typeName)
|
||||||
|
w, err := tsv.NewWriterFile(fn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.manifests[InstancePropertyIndex{
|
||||||
|
ID: instance.ID(),
|
||||||
|
InstanceID: instance.InstanceID(),
|
||||||
|
Property: typeName,
|
||||||
|
}] = fn
|
||||||
|
|
||||||
|
r.writers[o] = w
|
||||||
|
return w.Write(o.CsvHeader())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StateRecorder) Close() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, w := range r.writers {
|
||||||
|
err2 := w.Close()
|
||||||
|
if err2 != nil {
|
||||||
|
err = multierr.Append(err, err2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
61
pkg/backtest/recorder_test.go
Normal file
61
pkg/backtest/recorder_test.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testStrategy struct {
|
||||||
|
Symbol string
|
||||||
|
|
||||||
|
Position *types.Position
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testStrategy) ID() string { return "my-test" }
|
||||||
|
func (s *testStrategy) InstanceID() string { return "my-test:" + s.Symbol }
|
||||||
|
|
||||||
|
func TestStateRecorder(t *testing.T) {
|
||||||
|
tmpDir, _ := os.MkdirTemp(os.TempDir(), "qbtrade")
|
||||||
|
t.Logf("tmpDir: %s", tmpDir)
|
||||||
|
|
||||||
|
st := &testStrategy{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Position: types.NewPosition("BTCUSDT", "BTC", "USDT"),
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := NewStateRecorder(tmpDir)
|
||||||
|
err := recorder.Scan(st)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, recorder.writers, 1)
|
||||||
|
|
||||||
|
st.Position.AddTrade(types.Trade{
|
||||||
|
OrderID: 1,
|
||||||
|
Exchange: types.ExchangeBinance,
|
||||||
|
Price: fixedpoint.NewFromFloat(18000.0),
|
||||||
|
Quantity: fixedpoint.NewFromFloat(1.0),
|
||||||
|
QuoteQuantity: fixedpoint.NewFromFloat(18000.0),
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
IsBuyer: true,
|
||||||
|
IsMaker: false,
|
||||||
|
Time: types.Time(time.Now()),
|
||||||
|
Fee: fixedpoint.NewFromFloat(0.00001),
|
||||||
|
FeeCurrency: "BNB",
|
||||||
|
IsMargin: false,
|
||||||
|
IsFutures: false,
|
||||||
|
IsIsolated: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
n, err := recorder.Snapshot()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, n)
|
||||||
|
|
||||||
|
err = recorder.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
261
pkg/backtest/report.go
Normal file
261
pkg/backtest/report.go
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/gofrs/flock"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/accounting/pnl"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/qbtrade"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Run struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Config *qbtrade.Config `json:"config"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReportIndex struct {
|
||||||
|
Runs []Run `json:"runs,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SummaryReport is the summary of the back-test session
|
||||||
|
type SummaryReport struct {
|
||||||
|
StartTime time.Time `json:"startTime"`
|
||||||
|
EndTime time.Time `json:"endTime"`
|
||||||
|
Sessions []string `json:"sessions"`
|
||||||
|
Symbols []string `json:"symbols"`
|
||||||
|
Intervals []types.Interval `json:"intervals"`
|
||||||
|
InitialTotalBalances types.BalanceMap `json:"initialTotalBalances"`
|
||||||
|
FinalTotalBalances types.BalanceMap `json:"finalTotalBalances"`
|
||||||
|
|
||||||
|
InitialEquityValue fixedpoint.Value `json:"initialEquityValue"`
|
||||||
|
FinalEquityValue fixedpoint.Value `json:"finalEquityValue"`
|
||||||
|
|
||||||
|
// TotalProfit is the profit aggregated from the symbol reports
|
||||||
|
TotalProfit fixedpoint.Value `json:"totalProfit,omitempty"`
|
||||||
|
TotalUnrealizedProfit fixedpoint.Value `json:"totalUnrealizedProfit,omitempty"`
|
||||||
|
|
||||||
|
TotalGrossProfit fixedpoint.Value `json:"totalGrossProfit,omitempty"`
|
||||||
|
TotalGrossLoss fixedpoint.Value `json:"totalGrossLoss,omitempty"`
|
||||||
|
|
||||||
|
SymbolReports []SessionSymbolReport `json:"symbolReports,omitempty"`
|
||||||
|
|
||||||
|
Manifests Manifests `json:"manifests,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadSummaryReport(filename string) (*SummaryReport, error) {
|
||||||
|
o, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var report SummaryReport
|
||||||
|
err = json.Unmarshal(o, &report)
|
||||||
|
return &report, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionSymbolReport is the report per exchange session
|
||||||
|
// trades are merged, collected and re-calculated
|
||||||
|
type SessionSymbolReport struct {
|
||||||
|
Exchange types.ExchangeName `json:"exchange"`
|
||||||
|
Symbol string `json:"symbol,omitempty"`
|
||||||
|
Intervals []types.Interval `json:"intervals,omitempty"`
|
||||||
|
Subscriptions []types.Subscription `json:"subscriptions"`
|
||||||
|
Market types.Market `json:"market"`
|
||||||
|
LastPrice fixedpoint.Value `json:"lastPrice,omitempty"`
|
||||||
|
StartPrice fixedpoint.Value `json:"startPrice,omitempty"`
|
||||||
|
PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"`
|
||||||
|
InitialBalances types.BalanceMap `json:"initialBalances,omitempty"`
|
||||||
|
FinalBalances types.BalanceMap `json:"finalBalances,omitempty"`
|
||||||
|
Manifests Manifests `json:"manifests,omitempty"`
|
||||||
|
Sharpe fixedpoint.Value `json:"sharpeRatio"`
|
||||||
|
Sortino fixedpoint.Value `json:"sortinoRatio"`
|
||||||
|
ProfitFactor fixedpoint.Value `json:"profitFactor"`
|
||||||
|
WinningRatio fixedpoint.Value `json:"winningRatio"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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.LastPrice)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionSymbolReport) Print(wantBaseAssetBaseline bool) {
|
||||||
|
color.Green("%s %s PROFIT AND LOSS REPORT", r.Exchange, r.Symbol)
|
||||||
|
color.Green("===============================================")
|
||||||
|
r.PnL.Print()
|
||||||
|
|
||||||
|
initQuoteAsset := r.InitialEquityValue()
|
||||||
|
finalQuoteAsset := r.FinalEquityValue()
|
||||||
|
color.Green("INITIAL ASSET IN %s ~= %s %s (1 %s = %v)", r.Market.QuoteCurrency, r.Market.FormatQuantity(initQuoteAsset), r.Market.QuoteCurrency, r.Market.BaseCurrency, r.StartPrice)
|
||||||
|
color.Green("FINAL ASSET IN %s ~= %s %s (1 %s = %v)", r.Market.QuoteCurrency, r.Market.FormatQuantity(finalQuoteAsset), r.Market.QuoteCurrency, r.Market.BaseCurrency, r.LastPrice)
|
||||||
|
|
||||||
|
if r.PnL.Profit.Sign() > 0 {
|
||||||
|
color.Green("REALIZED PROFIT: +%v %s", r.PnL.Profit, r.Market.QuoteCurrency)
|
||||||
|
} else {
|
||||||
|
color.Red("REALIZED PROFIT: %v %s", r.PnL.Profit, r.Market.QuoteCurrency)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.PnL.UnrealizedProfit.Sign() > 0 {
|
||||||
|
color.Green("UNREALIZED PROFIT: +%v %s", r.PnL.UnrealizedProfit, r.Market.QuoteCurrency)
|
||||||
|
} else {
|
||||||
|
color.Red("UNREALIZED PROFIT: %v %s", r.PnL.UnrealizedProfit, r.Market.QuoteCurrency)
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalQuoteAsset.Compare(initQuoteAsset) > 0 {
|
||||||
|
color.Green("ASSET INCREASED: +%v %s (+%s)", finalQuoteAsset.Sub(initQuoteAsset), r.Market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2))
|
||||||
|
} else {
|
||||||
|
color.Red("ASSET DECREASED: %v %s (%s)", finalQuoteAsset.Sub(initQuoteAsset), r.Market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Sharpe.Sign() > 0 {
|
||||||
|
color.Green("REALIZED SHARPE RATIO: %s", r.Sharpe.FormatString(4))
|
||||||
|
} else {
|
||||||
|
color.Red("REALIZED SHARPE RATIO: %s", r.Sharpe.FormatString(4))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Sortino.Sign() > 0 {
|
||||||
|
color.Green("REALIZED SORTINO RATIO: %s", r.Sortino.FormatString(4))
|
||||||
|
} else {
|
||||||
|
color.Red("REALIZED SORTINO RATIO: %s", r.Sortino.FormatString(4))
|
||||||
|
}
|
||||||
|
|
||||||
|
if wantBaseAssetBaseline {
|
||||||
|
if r.LastPrice.Compare(r.StartPrice) > 0 {
|
||||||
|
color.Green("%s BASE ASSET PERFORMANCE: +%s (= (%s - %s) / %s)",
|
||||||
|
r.Market.BaseCurrency,
|
||||||
|
r.LastPrice.Sub(r.StartPrice).Div(r.StartPrice).FormatPercentage(2),
|
||||||
|
r.LastPrice.FormatString(2),
|
||||||
|
r.StartPrice.FormatString(2),
|
||||||
|
r.StartPrice.FormatString(2))
|
||||||
|
} else {
|
||||||
|
color.Red("%s BASE ASSET PERFORMANCE: %s (= (%s - %s) / %s)",
|
||||||
|
r.Market.BaseCurrency,
|
||||||
|
r.LastPrice.Sub(r.StartPrice).Div(r.StartPrice).FormatPercentage(2),
|
||||||
|
r.LastPrice.FormatString(2),
|
||||||
|
r.StartPrice.FormatString(2),
|
||||||
|
r.StartPrice.FormatString(2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SessionTimeFormat = "2006-01-02T15_04"
|
||||||
|
|
||||||
|
// FormatSessionName returns the back-test session name
|
||||||
|
func FormatSessionName(sessions []string, symbols []string, startTime, endTime time.Time) string {
|
||||||
|
return fmt.Sprintf("%s_%s_%s-%s",
|
||||||
|
strings.Join(sessions, "-"),
|
||||||
|
strings.Join(symbols, "-"),
|
||||||
|
startTime.Format(SessionTimeFormat),
|
||||||
|
endTime.Format(SessionTimeFormat),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteReportIndex(outputDirectory string, reportIndex *ReportIndex) error {
|
||||||
|
indexFile := getReportIndexPath(outputDirectory)
|
||||||
|
indexLock := flock.New(indexFile)
|
||||||
|
|
||||||
|
if err := indexLock.Lock(); err != nil {
|
||||||
|
log.WithError(err).Errorf("report index file lock error while write report: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := indexLock.Unlock(); err != nil {
|
||||||
|
log.WithError(err).Errorf("report index file unlock error while write report: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return writeReportIndexLocked(outputDirectory, reportIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadReportIndex(outputDirectory string) (*ReportIndex, error) {
|
||||||
|
indexFile := getReportIndexPath(outputDirectory)
|
||||||
|
indexLock := flock.New(indexFile)
|
||||||
|
|
||||||
|
if err := indexLock.Lock(); err != nil {
|
||||||
|
log.WithError(err).Errorf("report index file lock error while load report: %s", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := indexLock.Unlock(); err != nil {
|
||||||
|
log.WithError(err).Errorf("report index file unlock error while load report: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return loadReportIndexLocked(indexFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddReportIndexRun(outputDirectory string, run Run) error {
|
||||||
|
// append report index
|
||||||
|
indexFile := getReportIndexPath(outputDirectory)
|
||||||
|
indexLock := flock.New(indexFile)
|
||||||
|
|
||||||
|
if err := indexLock.Lock(); err != nil {
|
||||||
|
log.WithError(err).Errorf("report index file lock error: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := indexLock.Unlock(); err != nil {
|
||||||
|
log.WithError(err).Errorf("report index file unlock error: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
reportIndex, err := loadReportIndexLocked(indexFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reportIndex.Runs = append(reportIndex.Runs, run)
|
||||||
|
return writeReportIndexLocked(indexFile, reportIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InQuoteAsset converts all balances in quote asset
|
||||||
|
func InQuoteAsset(balances types.BalanceMap, market types.Market, price fixedpoint.Value) fixedpoint.Value {
|
||||||
|
quote := balances[market.QuoteCurrency]
|
||||||
|
base := balances[market.BaseCurrency]
|
||||||
|
return base.Total().Mul(price).Add(quote.Total())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReportIndexPath(outputDirectory string) string {
|
||||||
|
return filepath.Join(outputDirectory, "index.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeReportIndexLocked must be protected by file lock
|
||||||
|
func writeReportIndexLocked(indexFilePath string, reportIndex *ReportIndex) error {
|
||||||
|
if err := util.WriteJsonFile(indexFilePath, reportIndex); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadReportIndexLocked must be protected by file lock
|
||||||
|
func loadReportIndexLocked(indexFilePath string) (*ReportIndex, error) {
|
||||||
|
var reportIndex ReportIndex
|
||||||
|
if fileInfo, err := os.Stat(indexFilePath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if fileInfo.Size() != 0 {
|
||||||
|
o, err := ioutil.ReadFile(indexFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(o, &reportIndex); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &reportIndex, nil
|
||||||
|
}
|
37
pkg/backtest/simplepricematching_callbacks.go
Normal file
37
pkg/backtest/simplepricematching_callbacks.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// Code generated by "callbackgen -type SimplePriceMatching"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) OnTradeUpdate(cb func(trade types.Trade)) {
|
||||||
|
m.tradeUpdateCallbacks = append(m.tradeUpdateCallbacks, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) EmitTradeUpdate(trade types.Trade) {
|
||||||
|
for _, cb := range m.tradeUpdateCallbacks {
|
||||||
|
cb(trade)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) OnOrderUpdate(cb func(order types.Order)) {
|
||||||
|
m.orderUpdateCallbacks = append(m.orderUpdateCallbacks, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) EmitOrderUpdate(order types.Order) {
|
||||||
|
for _, cb := range m.orderUpdateCallbacks {
|
||||||
|
cb(order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) OnBalanceUpdate(cb func(balances types.BalanceMap)) {
|
||||||
|
m.balanceUpdateCallbacks = append(m.balanceUpdateCallbacks, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceMatching) EmitBalanceUpdate(balances types.BalanceMap) {
|
||||||
|
for _, cb := range m.balanceUpdateCallbacks {
|
||||||
|
cb(balances)
|
||||||
|
}
|
||||||
|
}
|
57
pkg/backtest/utils.go
Normal file
57
pkg/backtest/utils.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package backtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/qbtrade"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CollectSubscriptionIntervals(environ *qbtrade.Environment) (allKLineIntervals map[types.Interval]struct{}, requiredInterval types.Interval, backTestIntervals []types.Interval) {
|
||||||
|
// default extra back-test intervals
|
||||||
|
backTestIntervals = []types.Interval{types.Interval1h, types.Interval1d}
|
||||||
|
// all subscribed intervals
|
||||||
|
allKLineIntervals = make(map[types.Interval]struct{})
|
||||||
|
|
||||||
|
for _, interval := range backTestIntervals {
|
||||||
|
allKLineIntervals[interval] = struct{}{}
|
||||||
|
}
|
||||||
|
// default interval is 1m for all exchanges
|
||||||
|
requiredInterval = types.Interval1m
|
||||||
|
for _, session := range environ.Sessions() {
|
||||||
|
for _, sub := range session.Subscriptions {
|
||||||
|
if sub.Channel == types.KLineChannel {
|
||||||
|
if sub.Options.Interval.Seconds()%60 > 0 {
|
||||||
|
// if any subscription interval is less than 60s, then we will use 1s for back-testing
|
||||||
|
requiredInterval = types.Interval1s
|
||||||
|
logrus.Warnf("found kline subscription interval less than 60s, modify default backtest interval to 1s")
|
||||||
|
}
|
||||||
|
allKLineIntervals[sub.Options.Interval] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allKLineIntervals, requiredInterval, backTestIntervals
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitializeExchangeSources(sessions map[string]*qbtrade.ExchangeSession, startTime, endTime time.Time, requiredInterval types.Interval, extraIntervals ...types.Interval) (exchangeSources []*ExchangeDataSource, err error) {
|
||||||
|
for _, session := range sessions {
|
||||||
|
backtestEx := session.Exchange.(*Exchange)
|
||||||
|
|
||||||
|
c, err := backtestEx.SubscribeMarketData(startTime, endTime, requiredInterval, extraIntervals...)
|
||||||
|
if err != nil {
|
||||||
|
return exchangeSources, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionCopy := session
|
||||||
|
src := &ExchangeDataSource{
|
||||||
|
C: c,
|
||||||
|
Exchange: backtestEx,
|
||||||
|
Session: sessionCopy,
|
||||||
|
}
|
||||||
|
backtestEx.Src = src
|
||||||
|
exchangeSources = append(exchangeSources, src)
|
||||||
|
}
|
||||||
|
return exchangeSources, nil
|
||||||
|
}
|
178
pkg/cache/cache.go
vendored
Normal file
178
pkg/cache/cache.go
vendored
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/util"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/util/backoff"
|
||||||
|
)
|
||||||
|
|
||||||
|
const memCacheExpiry = 5 * time.Minute
|
||||||
|
const fileCacheExpiry = 24 * time.Hour
|
||||||
|
|
||||||
|
var globalMarketMemCache *marketMemCache = newMarketMemCache()
|
||||||
|
|
||||||
|
type marketMemCache struct {
|
||||||
|
sync.Mutex
|
||||||
|
markets map[string]marketMapWithTime
|
||||||
|
}
|
||||||
|
|
||||||
|
type marketMapWithTime struct {
|
||||||
|
updatedAt time.Time
|
||||||
|
markets types.MarketMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMarketMemCache() *marketMemCache {
|
||||||
|
cache := &marketMemCache{
|
||||||
|
markets: make(map[string]marketMapWithTime),
|
||||||
|
}
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *marketMemCache) IsOutdated(exName string) bool {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
data, ok := c.markets[exName]
|
||||||
|
return !ok || time.Since(data.updatedAt) > memCacheExpiry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *marketMemCache) Set(exName string, markets types.MarketMap) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
c.markets[exName] = marketMapWithTime{
|
||||||
|
updatedAt: time.Now(),
|
||||||
|
markets: markets,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *marketMemCache) Get(exName string) (types.MarketMap, bool) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
markets, ok := c.markets[exName]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
copied := types.MarketMap{}
|
||||||
|
for key, val := range markets.markets {
|
||||||
|
copied[key] = val
|
||||||
|
}
|
||||||
|
return copied, true
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataFetcher func() (interface{}, error)
|
||||||
|
|
||||||
|
// WithCache let you use the cache with the given cache key, variable reference and your data fetcher,
|
||||||
|
// The key must be an unique ID.
|
||||||
|
// obj is the pointer of your local variable
|
||||||
|
// fetcher is the closure that will fetch your remote data or some slow operation.
|
||||||
|
func WithCache(key string, obj interface{}, fetcher DataFetcher) error {
|
||||||
|
cacheDir := CacheDir()
|
||||||
|
cacheFile := path.Join(cacheDir, key+".json")
|
||||||
|
|
||||||
|
stat, err := os.Stat(cacheFile)
|
||||||
|
if os.IsNotExist(err) || (stat != nil && time.Since(stat.ModTime()) > fileCacheExpiry) {
|
||||||
|
log.Debugf("cache %s not found or cache expired, executing fetcher callback to get the data", cacheFile)
|
||||||
|
|
||||||
|
data, err := fetcher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(cacheFile, out, 0666); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := reflect.ValueOf(obj).Elem()
|
||||||
|
if !rv.CanSet() {
|
||||||
|
return errors.New("can not set cache object value")
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.Set(reflect.ValueOf(data))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.Debugf("cache %s found", cacheFile)
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(cacheFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, obj); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadExchangeMarketsWithCache(ctx context.Context, ex types.ExchangePublic) (markets types.MarketMap, err error) {
|
||||||
|
inMem, ok := util.GetEnvVarBool("USE_MARKETS_CACHE_IN_MEMORY")
|
||||||
|
if ok && inMem {
|
||||||
|
return loadMarketsFromMem(ctx, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to use files as cache
|
||||||
|
return loadMarketsFromFile(ctx, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadMarketsFromMem is useful for one process to run multiple qbtrades in different go routines.
|
||||||
|
func loadMarketsFromMem(ctx context.Context, ex types.ExchangePublic) (markets types.MarketMap, _ error) {
|
||||||
|
exName := ex.Name().String()
|
||||||
|
if globalMarketMemCache.IsOutdated(exName) {
|
||||||
|
op := func() error {
|
||||||
|
rst, err2 := ex.QueryMarkets(ctx)
|
||||||
|
if err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
|
||||||
|
markets = rst
|
||||||
|
globalMarketMemCache.Set(exName, rst)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := backoff.RetryGeneral(ctx, op); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return markets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rst, _ := globalMarketMemCache.Get(exName)
|
||||||
|
return rst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMarketsFromFile(ctx context.Context, ex types.ExchangePublic) (markets types.MarketMap, err error) {
|
||||||
|
key := fmt.Sprintf("%s-markets", ex.Name())
|
||||||
|
if futureExchange, implemented := ex.(types.FuturesExchange); implemented {
|
||||||
|
settings := futureExchange.GetFuturesSettings()
|
||||||
|
if settings.IsFutures {
|
||||||
|
key = fmt.Sprintf("%s-futures-markets", ex.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = WithCache(key, &markets, func() (interface{}, error) {
|
||||||
|
return ex.QueryMarkets(ctx)
|
||||||
|
})
|
||||||
|
return markets, err
|
||||||
|
}
|
103
pkg/cache/cache_test.go
vendored
Normal file
103
pkg/cache/cache_test.go
vendored
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uber.org/mock/gomock"
|
||||||
|
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
||||||
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types/mocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_newMarketMemCache(t *testing.T) {
|
||||||
|
cache := newMarketMemCache()
|
||||||
|
assert.NotNil(t, cache)
|
||||||
|
assert.NotNil(t, cache.markets)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_marketMemCache_GetSet(t *testing.T) {
|
||||||
|
cache := newMarketMemCache()
|
||||||
|
cache.Set("max", types.MarketMap{
|
||||||
|
"btctwd": types.Market{
|
||||||
|
Symbol: "btctwd",
|
||||||
|
LocalSymbol: "btctwd",
|
||||||
|
},
|
||||||
|
"ethtwd": types.Market{
|
||||||
|
Symbol: "ethtwd",
|
||||||
|
LocalSymbol: "ethtwd",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
markets, ok := cache.Get("max")
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
btctwd, ok := markets["btctwd"]
|
||||||
|
assert.True(t, ok)
|
||||||
|
ethtwd, ok := markets["ethtwd"]
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, types.Market{
|
||||||
|
Symbol: "btctwd",
|
||||||
|
LocalSymbol: "btctwd",
|
||||||
|
}, btctwd)
|
||||||
|
assert.Equal(t, types.Market{
|
||||||
|
Symbol: "ethtwd",
|
||||||
|
LocalSymbol: "ethtwd",
|
||||||
|
}, ethtwd)
|
||||||
|
|
||||||
|
_, ok = cache.Get("binance")
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
expired := cache.IsOutdated("max")
|
||||||
|
assert.False(t, expired)
|
||||||
|
|
||||||
|
detailed := cache.markets["max"]
|
||||||
|
detailed.updatedAt = time.Now().Add(-2 * memCacheExpiry)
|
||||||
|
cache.markets["max"] = detailed
|
||||||
|
expired = cache.IsOutdated("max")
|
||||||
|
assert.True(t, expired)
|
||||||
|
|
||||||
|
expired = cache.IsOutdated("binance")
|
||||||
|
assert.True(t, expired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_loadMarketsFromMem(t *testing.T) {
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
|
||||||
|
mockEx := mocks.NewMockExchangePublic(mockCtrl)
|
||||||
|
mockEx.EXPECT().Name().Return(types.ExchangeName("max")).AnyTimes()
|
||||||
|
mockEx.EXPECT().QueryMarkets(gomock.Any()).Return(nil, errors.New("faked")).Times(1)
|
||||||
|
mockEx.EXPECT().QueryMarkets(gomock.Any()).Return(types.MarketMap{
|
||||||
|
"btctwd": types.Market{
|
||||||
|
Symbol: "btctwd",
|
||||||
|
LocalSymbol: "btctwd",
|
||||||
|
},
|
||||||
|
"ethtwd": types.Market{
|
||||||
|
Symbol: "ethtwd",
|
||||||
|
LocalSymbol: "ethtwd",
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
markets, err := loadMarketsFromMem(context.Background(), mockEx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
btctwd, ok := markets["btctwd"]
|
||||||
|
assert.True(t, ok)
|
||||||
|
ethtwd, ok := markets["ethtwd"]
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, types.Market{
|
||||||
|
Symbol: "btctwd",
|
||||||
|
LocalSymbol: "btctwd",
|
||||||
|
}, btctwd)
|
||||||
|
assert.Equal(t, types.Market{
|
||||||
|
Symbol: "ethtwd",
|
||||||
|
LocalSymbol: "ethtwd",
|
||||||
|
}, ethtwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalMarketMemCache = newMarketMemCache() // reset the global cache
|
||||||
|
}
|
31
pkg/cache/home.go
vendored
Normal file
31
pkg/cache/home.go
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareDir(p string) string {
|
||||||
|
_, err := os.Stat(p)
|
||||||
|
if err != nil {
|
||||||
|
_ = os.Mkdir(p, 0777)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func CacheDir() string {
|
||||||
|
home := HomeDir()
|
||||||
|
dir := path.Join(home, "cache")
|
||||||
|
return prepareDir(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HomeDir() string {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := path.Join(homeDir, ".qbtrade")
|
||||||
|
return prepareDir(dir)
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user