14 Commits

Author SHA1 Message Date
Saleh Mir
7953fbc90b Implement PNL and PNL% for inverse futures 2021-03-09 17:47:45 +01:00
Saleh Mir
e3bbd8a4fc Implement the contract_size property 2021-03-09 16:34:04 +01:00
Saleh Mir
53992318ff add contract_size as a config to inverse futures 2021-03-09 16:20:50 +01:00
Saleh Mir
25fb188331 fix leverage property for inverse futures 2021-03-09 15:03:22 +01:00
Saleh Mir
b2ec3d42ce Merge branch 'master' into inverse-futures 2021-03-08 17:25:17 +01:00
Saleh Mir
71adce0cca add unit test: test_can_detect_inverse_futures 2021-03-08 15:44:30 +01:00
Saleh Mir
8b359f06df fix 👷‍♂️ 2021-03-05 17:35:04 +01:00
Saleh Mir
b0ca75394d add support for inverse futures' symbol to app_currency() helper function 2021-03-05 17:34:20 +01:00
Saleh Mir
b7c07d2913 fix 2021-03-05 17:09:28 +01:00
Saleh Mir
9c252a50d4 Add support for inverse_futures for the single_route_backtest test utility 2021-03-04 19:51:32 +01:00
Saleh Mir
243d4bb9dc initial implementation of InverseFuturesExchange 2021-03-03 18:15:59 +01:00
Saleh Mir
b4cc5be234 add "Binance Inverse Futures" to config file 2021-03-03 16:58:32 +01:00
Saleh Mir
8a052cb55b Clean comments 2021-03-02 16:01:25 +01:00
Saleh Mir
d7216eac15 Merge pull request #140 from jesse-ai/master
update to latest changes on master
2021-03-02 15:49:13 +01:00
16 changed files with 394 additions and 45 deletions

View File

@@ -85,7 +85,7 @@ config = {
],
},
# https://www.binance.com
# https://www.binance.com/en/futures/BTC_USDT
'Binance Futures': {
'fee': 0.0004,
@@ -105,6 +105,26 @@ config = {
],
},
# https://www.binance.com/en/delivery/btcusd_perpetual
'Binance Inverse Futures': {
'fee': 0.0004,
# backtest mode only: accepted are 'spot' and 'futures'
'type': 'inverse futures',
# accepted values are: 'cross' and 'isolated'
'futures_leverage_mode': 'cross',
# 1x, 2x, 10x, 50x, etc. Enter as integers
'futures_leverage': 1,
# Price per contract, also called the contract-multiplier
'contract_size': 100,
'assets': [
{'asset': 'BTC', 'balance': 1},
{'asset': 'ETH', 'balance': 10},
],
},
# https://testnet.binancefuture.com
'Testnet Binance Futures': {
'fee': 0.0004,

View File

@@ -16,7 +16,10 @@ CACHED_CONFIG = dict()
def app_currency() -> str:
from jesse.routes import router
return quote_asset(router.routes[0].symbol)
underlying = quote_asset(router.routes[0].symbol)
if underlying.upper() == 'PERP':
underlying = base_asset(router.routes[0].symbol)
return underlying
def app_mode() -> str:

View File

@@ -48,9 +48,9 @@ class Exchange(ABC):
pass
@abstractmethod
def add_realized_pnl(self, realized_pnl: float):
def add_realized_pnl(self, symbol: str, realized_pnl: float):
pass
@abstractmethod
def charge_fee(self, amount):
def charge_fee(self, symbol: str, amount):
pass

View File

@@ -87,7 +87,7 @@ class FuturesExchange(Exchange):
return temp_credit
def charge_fee(self, amount):
def charge_fee(self, symbol: str, amount):
fee_amount = abs(amount) * self.fee_rate
new_balance = self.assets[self.settlement_currency] - fee_amount
logger.info(
@@ -99,7 +99,7 @@ class FuturesExchange(Exchange):
)
self.assets[self.settlement_currency] = new_balance
def add_realized_pnl(self, realized_pnl: float):
def add_realized_pnl(self, symbol: str, realized_pnl: float):
new_balance = self.assets[self.settlement_currency] + realized_pnl
logger.info('Added realized PNL of {}. Balance for {} on {} changed from {} to {}'.format(
round(realized_pnl, 2),
@@ -171,4 +171,4 @@ class FuturesExchange(Exchange):
if item[0] == order.qty and item[1] == order.price:
self.sell_orders[base_asset][index] = np.array([0, 0])
break
return
return

View File

@@ -0,0 +1,187 @@
import jesse.helpers as jh
import jesse.services.logger as logger
from jesse.exceptions import InsufficientMargin
from jesse.models import Order
from jesse.enums import sides, order_types
from jesse.libs import DynamicNumpyArray
import numpy as np
from jesse.services import selectors
from .Exchange import Exchange
class InverseFuturesExchange(Exchange):
"""
In inverse future contracts, the settlement_currency is the base asset itself.
"""
# current holding assets
assets = {}
# current available assets (dynamically changes based on active orders)
available_assets = {}
buy_orders = {}
sell_orders = {}
def __init__(
self,
name: str,
starting_assets: list,
fee_rate: float,
futures_leverage_mode: str,
futures_leverage: int,
contract_size: int,
):
super().__init__(name, starting_assets, fee_rate, 'inverse futures')
self.futures_leverage_mode = futures_leverage_mode
self.futures_leverage = futures_leverage
self.contract_size = contract_size
for item in starting_assets:
self.buy_orders[item['asset']] = DynamicNumpyArray((10, 2))
self.sell_orders[item['asset']] = DynamicNumpyArray((10, 2))
# make sure trading routes exist in starting_assets
from jesse.routes import router
for r in router.routes:
base = jh.base_asset(r.symbol)
if base not in self.assets:
self.assets[base] = 0
if base not in self.buy_orders:
self.buy_orders[base] = DynamicNumpyArray((10, 2))
if base not in self.sell_orders:
self.sell_orders[base] = DynamicNumpyArray((10, 2))
self.starting_assets = self.assets.copy()
self.available_assets = self.assets.copy()
# start from 0 balance for self.available_assets which acts as a temp variable
for k in self.available_assets:
self.available_assets[k] = 0
def wallet_balance(self, symbol=''):
if symbol == '':
raise ValueError
settlement_currency = jh.base_asset(symbol)
return self.assets[settlement_currency]
def available_margin(self, symbol=''):
if symbol == '':
raise ValueError
settlement_currency = jh.base_asset(symbol)
temp_credit = self.assets[settlement_currency] * self.futures_leverage
# we need to consider buy and sell orders of ALL pairs
# also, consider the value of all open positions
for asset in self.assets:
if asset == settlement_currency:
continue
position = selectors.get_position(self.name, asset + "-" + settlement_currency)
if position is None:
continue
if position.is_open:
# add unrealized PNL
temp_credit += position.pnl
# subtract worst scenario orders' used margin
sum_buy_orders = (self.buy_orders[asset][:][:, 0] * self.buy_orders[asset][:][:, 1]).sum()
sum_sell_orders = (self.sell_orders[asset][:][:, 0] * self.sell_orders[asset][:][:, 1]).sum()
if position.is_open:
if position.type == 'long':
sum_buy_orders += position.value
else:
sum_sell_orders -= abs(position.value)
temp_credit -= max(abs(sum_buy_orders), abs(sum_sell_orders))
return temp_credit
def charge_fee(self, symbol, amount):
settlement_currency = jh.base_asset(symbol)
fee_amount = abs(amount) * self.fee_rate
new_balance = self.assets[settlement_currency] - fee_amount
logger.info(
'Charged {} as fee. Balance for {} on {} changed from {} to {}'.format(
round(fee_amount, 2), settlement_currency, self.name,
round(self.assets[settlement_currency], 2),
round(new_balance, 2),
)
)
self.assets[settlement_currency] = new_balance
def add_realized_pnl(self, symbol: str, realized_pnl: float):
settlement_currency = jh.base_asset(symbol)
new_balance = self.assets[settlement_currency] + realized_pnl
logger.info('Added realized PNL of {}. Balance for {} on {} changed from {} to {}'.format(
round(realized_pnl, 2),
settlement_currency, self.name,
round(self.assets[settlement_currency], 2),
round(new_balance, 2),
))
self.assets[settlement_currency] = new_balance
def on_order_submission(self, order: Order, skip_market_order=True):
base_asset = jh.base_asset(order.symbol)
# make sure we don't spend more than we're allowed considering current allowed leverage
if order.type != order_types.MARKET or skip_market_order:
if not order.is_reduce_only:
# TODO: add contract_multiplier to the equation
order_size = abs(order.qty / order.price)
remaining_margin = self.available_margin(order.symbol)
if order_size > remaining_margin:
raise InsufficientMargin(
'You cannot submit an order for ${} when your margin balance is ${}'.format(
round(order_size), round(remaining_margin)
))
# skip market order at the time of submission because we don't have
# the exact order.price. Instead, we call on_order_submission() one
# more time at time of execution without "skip_market_order=False".
if order.type == order_types.MARKET and skip_market_order:
return
self.available_assets[base_asset] += order.qty
if order.side == sides.BUY:
self.buy_orders[base_asset].append(np.array([order.qty, order.price]))
else:
self.sell_orders[base_asset].append(np.array([order.qty, order.price]))
def on_order_execution(self, order: Order):
base_asset = jh.base_asset(order.symbol)
if order.type == order_types.MARKET:
self.on_order_submission(order, skip_market_order=False)
if order.side == sides.BUY:
# find and set order to [0, 0] (same as removing it)
for index, item in enumerate(self.buy_orders[base_asset]):
if item[0] == order.qty and item[1] == order.price:
self.buy_orders[base_asset][index] = np.array([0, 0])
break
else:
# find and set order to [0, 0] (same as removing it)
for index, item in enumerate(self.sell_orders[base_asset]):
if item[0] == order.qty and item[1] == order.price:
self.sell_orders[base_asset][index] = np.array([0, 0])
break
return
def on_order_cancellation(self, order: Order):
base_asset = jh.base_asset(order.symbol)
self.available_assets[base_asset] -= order.qty
# self.available_assets[quote_asset] += order.qty * order.price
if order.side == sides.BUY:
# find and set order to [0, 0] (same as removing it)
for index, item in enumerate(self.buy_orders[base_asset]):
if item[0] == order.qty and item[1] == order.price:
self.buy_orders[base_asset][index] = np.array([0, 0])
break
else:
# find and set order to [0, 0] (same as removing it)
for index, item in enumerate(self.sell_orders[base_asset]):
if item[0] == order.qty and item[1] == order.price:
self.sell_orders[base_asset][index] = np.array([0, 0])
break
return

View File

@@ -39,6 +39,11 @@ class Position:
# # TODO: make sure that it is available only for live trading in futures markets
# return self._mark_price
# TODO: more properties to add:
# - Margin Ratio
# * Liquidation price
# * mark price?!
@property
def value(self) -> float:
"""
@@ -75,7 +80,8 @@ class Position:
def roi(self) -> float:
"""
Return on Investment in percentage
More at: https://www.binance.com/en/support/faq/5b9ad93cb4854f5990b9fb97c03cfbeb
For futures: https://www.binance.com/en/support/faq/5b9ad93cb4854f5990b9fb97c03cfbeb
"""
return self.pnl / self.total_cost * 100
@@ -110,9 +116,12 @@ class Position:
if self.qty == 0:
return 0
diff = self.value - abs(self.entry_price * self.qty)
if self.exchange.type != 'inverse futures':
diff = self.value - abs(self.entry_price * self.qty)
return -diff if self.type == 'short' else diff
return -diff if self.type == 'short' else diff
# PNL = side * Qty * contract_multiplier * (1 / entry price - 1 / exit price)
return self.qty * self.exchange.contract_size * (1 / self.entry_price - 1 / self.current_price)
@property
def is_open(self) -> bool:
@@ -133,23 +142,12 @@ class Position:
return self.qty == 0
@property
def mode(self) -> str:
def margin_mode(self) -> str:
if self.exchange.type == 'spot':
return 'spot'
else:
return self.exchange.futures_leverage_mode
# - Margin Ratio
# * Liquidation price
# * mark price?!
# * ROE(PNL?)
# * Maintenance futures
# * futures balance
# @property
# def futures_ratio(self):
# return 0
def _close(self, close_price: float) -> None:
if self.is_open is False:
raise EmptyPosition('The position is already closed.')
@@ -166,7 +164,7 @@ class Position:
self.exit_price = close_price
if self.exchange:
self.exchange.add_realized_pnl(estimated_profit)
self.exchange.add_realized_pnl(self.symbol, estimated_profit)
self.exchange.temp_reduced_amount[jh.base_asset(self.symbol)] += abs(close_qty * close_price)
self.qty = 0
self.entry_price = None
@@ -196,7 +194,7 @@ class Position:
if self.exchange:
# self.exchange.increase_futures_balance(qty * self.entry_price + estimated_profit)
self.exchange.add_realized_pnl(estimated_profit)
self.exchange.add_realized_pnl(self.symbol, estimated_profit)
self.exchange.temp_reduced_amount[jh.base_asset(self.symbol)] += abs(qty * price)
if self.type == trade_types.LONG:
@@ -267,7 +265,7 @@ class Position:
# TODO: detect reduce_only order, and if so, see if you need to adjust qty and price (above variables)
self.exchange.charge_fee(qty * price)
self.exchange.charge_fee(self.symbol, qty * price)
# order opens position
if self.qty == 0:

View File

@@ -7,10 +7,10 @@ from .Exchange import Exchange
class SpotExchange(Exchange):
def add_realized_pnl(self, realized_pnl: float):
def add_realized_pnl(self, symbol: str, realized_pnl: float):
pass
def charge_fee(self, amount):
def charge_fee(self, symbol: str, amount):
pass
# current holding assets

View File

@@ -3,6 +3,7 @@ from .CompletedTrade import CompletedTrade
from .Exchange import Exchange
from .SpotExchange import SpotExchange
from .FuturesExchange import FuturesExchange
from .InverseFuturesExchange import InverseFuturesExchange
from .Order import Order
from .Position import Position
from .Route import Route

View File

@@ -75,7 +75,7 @@ config = {
],
},
# https://www.binance.com
# https://www.binance.com/en/futures/BTC_USDT
'Binance Futures': {
'fee': 0.0004,
@@ -95,6 +95,26 @@ config = {
],
},
# https://www.binance.com/en/delivery/btcusd_perpetual
'Binance Inverse Futures': {
'fee': 0.0004,
# backtest mode only: accepted are 'spot' and 'futures'
'type': 'inverse futures',
# accepted values are: 'cross' and 'isolated'
'futures_leverage_mode': 'cross',
# 1x, 2x, 10x, 50x, etc. Enter as integers
'futures_leverage': 1,
# Price per contract, also called the contract-multiplier
'contract_size': 100,
'assets': [
{'asset': 'BTC', 'balance': 1},
{'asset': 'ETH', 'balance': 10},
],
},
# https://testnet.binancefuture.com
'Testnet Binance Futures': {
'fee': 0.0004,

View File

@@ -1,7 +1,7 @@
from typing import Union, ValuesView
from jesse.config import config
from jesse.models import SpotExchange, FuturesExchange
from jesse.models import SpotExchange, FuturesExchange, InverseFuturesExchange
from jesse.exceptions import InvalidConfig
import jesse.helpers as jh
@@ -24,5 +24,12 @@ class ExchangesState:
futures_leverage_mode=jh.get_config('env.exchanges.{}.futures_leverage_mode'.format(name)),
futures_leverage=jh.get_config('env.exchanges.{}.futures_leverage'.format(name)),
)
elif exchange_type == 'inverse futures':
self.storage[name] = InverseFuturesExchange(
name, starting_assets, fee,
futures_leverage_mode=jh.get_config('env.exchanges.{}.futures_leverage_mode'.format(name)),
futures_leverage=jh.get_config('env.exchanges.{}.futures_leverage'.format(name)),
contract_size=jh.get_config('env.exchanges.{}.contract_size'.format(name)),
)
else:
raise InvalidConfig('Value for exchange type in your config file in not valid. Supported values are "spot" and "futures"')

View File

@@ -10,7 +10,7 @@ import jesse.services.logger as logger
import jesse.services.selectors as selectors
from jesse import exceptions
from jesse.enums import sides, trade_types, order_roles
from jesse.models import CompletedTrade, Order, Route, FuturesExchange, SpotExchange, Position
from jesse.models import CompletedTrade, Order, Route, FuturesExchange, SpotExchange, InverseFuturesExchange, Position
from jesse.models.utils import store_completed_trade_into_db, store_order_into_db
from jesse.services import metrics
from jesse.services.broker import Broker
@@ -1201,5 +1201,18 @@ class Strategy(ABC):
return 1
elif type(self.position.exchange) is FuturesExchange:
return self.position.exchange.futures_leverage
elif type(self.position.exchange) is InverseFuturesExchange:
return self.position.exchange.futures_leverage
else:
raise ValueError('exchange type not supported!')
@property
def contract_size(self):
if self.position.exchange.type != 'inverse futures':
raise exceptions.InvalidStrategy(
'Only inverse futures have access to the contract_size property. You are accessing it from a {} exchange'.format(
self.position.exchange.type
)
)
return self.position.exchange.contract_size

View File

@@ -0,0 +1,27 @@
from jesse.strategies import Strategy
from jesse import utils
class TestCanDetectInverseFutures(Strategy):
def should_long(self) -> bool:
if self.index == 0:
# current capital should be 1 BTC
assert self.capital == 100
assert self.position.exchange.type == 'inverse futures'
assert self.symbol == 'BTC-PERP'
assert self.leverage == 2
assert self.contract_size == 1
return False
def should_short(self) -> bool:
return False
def go_long(self):
pass
def go_short(self):
pass
def should_cancel(self):
return False

View File

@@ -0,0 +1,31 @@
from jesse.strategies import Strategy
from jesse import utils
class TestInverseFuturesLongTrade(Strategy):
def should_long(self) -> bool:
if self.index == 0:
assert self.position.exchange.contract_size == 100
return self.price == 10
def should_short(self) -> bool:
return False
def go_long(self):
entry = 10
# assuming contract size is 100, buy with all capital
qty = self.capital * self.price / 100
self.buy = qty, entry
self.take_profit = qty, 20
def go_short(self):
pass
def should_cancel(self):
return False
def update_position(self):
if 20 > self.price > 10:
print(self.position.qty)
assert self.position.qty == 10

View File

@@ -13,6 +13,11 @@ def test_app_currency():
])
assert jh.app_currency() == 'USD'
router.set_routes([
(exchanges.BITFINEX, 'ETH-PERP', timeframes.HOUR_3, 'Test19'),
])
assert jh.app_currency() == 'ETH'
def test_app_mode():
assert jh.app_mode() == 'backtest'
@@ -29,6 +34,8 @@ def test_base_asset():
assert jh.base_asset('DEFI-USDT') == 'DEFI'
assert jh.base_asset('DEFI-USD') == 'DEFI'
assert jh.base_asset('BTC-PERP') == 'BTC'
def test_binary_search():
arr = [0, 11, 22, 33, 44, 54, 55]

View File

@@ -0,0 +1,14 @@
from .utils import get_btc_candles, get_btc_and_eth_candles, set_up, single_route_backtest
def test_can_detect_inverse_futures():
single_route_backtest('TestCanDetectInverseFutures', is_futures_trading=True, is_inverse_futures=True, leverage=2)
def test_long_trade():
single_route_backtest(
'TestInverseFuturesLongTrade',
is_futures_trading=True,
is_inverse_futures=True,
contract_size=100
)

View File

@@ -23,32 +23,51 @@ def get_btc_and_eth_candles():
return candles
def get_btc_candles():
def get_btc_candles(symbol='BTC-USDT'):
candles = {}
candles[jh.key(exchanges.SANDBOX, 'BTC-USDT')] = {
candles[jh.key(exchanges.SANDBOX, symbol)] = {
'exchange': exchanges.SANDBOX,
'symbol': 'BTC-USDT',
'symbol': symbol,
'candles': fake_range_candle_from_range_prices(range(1, 100))
}
return candles
def set_up(routes=None, is_futures_trading=True, leverage=1, leverage_mode='cross', zero_fee=False):
def set_up(
routes=None,
is_futures_trading=True,
leverage=1,
leverage_mode='cross',
zero_fee=False,
is_inverse_futures=False,
contract_size=1,
):
reset_config()
config['env']['exchanges'][exchanges.SANDBOX]['assets'] = [
{'asset': 'USDT', 'balance': 10_000},
{'asset': 'BTC', 'balance': 0},
{'asset': 'ETH', 'balance': 0},
]
if is_inverse_futures:
config['env']['exchanges'][exchanges.SANDBOX]['assets'] = [
{'asset': 'USDT', 'balance': 0},
{'asset': 'BTC', 'balance': 100},
]
else:
config['env']['exchanges'][exchanges.SANDBOX]['assets'] = [
{'asset': 'USDT', 'balance': 10_000},
{'asset': 'BTC', 'balance': 0},
{'asset': 'ETH', 'balance': 0},
]
if zero_fee:
config['env']['exchanges']['Sandbox']['fee'] = 0
if is_futures_trading:
# used only in futures trading
if is_futures_trading and not is_inverse_futures:
config['env']['exchanges'][exchanges.SANDBOX]['type'] = 'futures'
config['env']['exchanges'][exchanges.SANDBOX]['futures_leverage_mode'] = leverage_mode
config['env']['exchanges'][exchanges.SANDBOX]['futures_leverage'] = leverage
elif is_inverse_futures:
config['env']['exchanges'][exchanges.SANDBOX]['type'] = 'inverse futures'
config['env']['exchanges'][exchanges.SANDBOX]['futures_leverage_mode'] = leverage_mode
config['env']['exchanges'][exchanges.SANDBOX]['futures_leverage'] = leverage
config['env']['exchanges'][exchanges.SANDBOX]['contract_size'] = contract_size
else:
config['env']['exchanges'][exchanges.SANDBOX]['type'] = 'spot'
@@ -58,14 +77,16 @@ def set_up(routes=None, is_futures_trading=True, leverage=1, leverage_mode='cros
store.reset(True)
def single_route_backtest(strategy_name: str, is_futures_trading=True, leverage=1):
def single_route_backtest(strategy_name: str, is_futures_trading=True, is_inverse_futures=False, leverage=1, contract_size=1):
"""
used to simplify simple tests
"""
set_up(
[(exchanges.SANDBOX, 'BTC-USDT', timeframes.MINUTE_1, strategy_name)],
[(exchanges.SANDBOX, 'BTC-USDT' if not is_inverse_futures else 'BTC-PERP', timeframes.MINUTE_1, strategy_name)],
is_futures_trading=is_futures_trading,
leverage=leverage
is_inverse_futures=is_inverse_futures,
leverage=leverage,
contract_size=contract_size
)
# dates are fake. just to pass required parameters
backtest_mode.run('2019-04-01', '2019-04-02', get_btc_candles())
backtest_mode.run('2019-04-01', '2019-04-02', get_btc_candles('BTC-USDT' if not is_inverse_futures else 'BTC-PERP'))