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