Merge branch 'master' into inverse-futures

This commit is contained in:
Saleh Mir
2021-03-08 17:25:17 +01:00
15 changed files with 141 additions and 134 deletions

View File

@@ -219,13 +219,13 @@ def get_arrow(timestamp: int) -> arrow.arrow.Arrow:
return timestamp_to_arrow(timestamp)
def get_candle_source(candles: np.ndarray, source_type: str = "close") -> np.array:
def get_candle_source(candles: np.ndarray, source_type: str = "close") -> np.ndarray:
"""
Returns the candles corresponding the selected type.
:param candles: np.ndarray
:param source_type: string
:return: np.array
:return: np.ndarray
"""
if source_type == "close":
@@ -422,7 +422,18 @@ def now_to_timestamp() -> int:
return arrow.utcnow().int_timestamp * 1000
def np_shift(arr: np.array, num: int, fill_value=0) -> np.array:
def np_ffill(arr: np.ndarray, axis: int = 0) -> np.ndarray:
idx_shape = tuple([slice(None)] + [np.newaxis] * (len(arr.shape) - axis - 1))
idx = np.where(~np.isnan(arr), np.arange(arr.shape[axis])[idx_shape], 0)
np.maximum.accumulate(idx, axis=axis, out=idx)
slc = [np.arange(k)[tuple([slice(None) if dim == i else np.newaxis
for dim in range(len(arr.shape))])]
for i, k in enumerate(arr.shape)]
slc[axis] = idx
return arr[tuple(slc)]
def np_shift(arr: np.ndarray, num: int, fill_value=0) -> np.ndarray:
result = np.empty_like(arr)
if num > 0:
@@ -541,6 +552,7 @@ def readable_duration(seconds: int, granularity: int = 2) -> str:
)
result = []
seconds = int(seconds)
for name, count in intervals:
value = seconds // count

View File

@@ -34,7 +34,7 @@ def alligator(candles: np.ndarray, source_type: str = "close", sequential: bool
return AG(jaw[-1], teeth[-1], lips[-1])
def numpy_ewma(data: np.array, window: int):
def numpy_ewma(data: np.ndarray, window: int):
"""
:param data:

View File

@@ -42,7 +42,7 @@ def chande(candles: np.ndarray, period: int = 22, mult: float = 3.0, direction:
return result if sequential else result[-1]
def filter1d_same(a: np.array, W: int, type: str, fillna=np.nan):
def filter1d_same(a: np.ndarray, W: int, type: str, fillna=np.nan):
out_dtype = np.full(0, fillna).dtype
hW = (W - 1) // 2 # Half window size
if type == 'max':

View File

@@ -32,7 +32,7 @@ def fwma(candles: np.ndarray, period: int = 5, source_type: str = "close", seque
return np.concatenate((np.full((candles.shape[0] - res.shape[0]), np.nan), res), axis=0) if sequential else res[-1]
def fibonacci(n: int = 2) -> np.array:
def fibonacci(n: int = 2) -> np.ndarray:
"""Fibonacci Sequence as a numpy array"""
n = int(fabs(n)) if n >= 0 else 2

View File

@@ -3,7 +3,7 @@ from collections import namedtuple
import numpy as np
from scipy.signal import argrelextrema
from jesse.helpers import get_config
from jesse.helpers import get_config, np_ffill
EXTREMA = namedtuple('EXTREMA', ['min', 'max', 'last_min', 'last_max'])
@@ -44,19 +44,3 @@ def minmax(candles: np.ndarray, order: int = 3, sequential: bool = False) -> EXT
else:
return EXTREMA(min[-1], max[-1], last_min[-1], last_max[-1])
def np_ffill(arr, axis=0):
"""
:param arr:
:param axis:
:return:
"""
idx_shape = tuple([slice(None)] + [np.newaxis] * (len(arr.shape) - axis - 1))
idx = np.where(~np.isnan(arr), np.arange(arr.shape[axis])[idx_shape], 0)
np.maximum.accumulate(idx, axis=axis, out=idx)
slc = [np.arange(k)[tuple([slice(None) if dim == i else np.newaxis
for dim in range(len(arr.shape))])]
for i, k in enumerate(arr.shape)]
slc[axis] = idx
return arr[tuple(slc)]

View File

@@ -91,8 +91,12 @@ class Position:
"""
if self.is_close:
return np.nan
return self.entry_price * abs(self.qty) / self.exchange.futures_leverage
base_cost = self.entry_price * abs(self.qty)
if self.strategy:
return base_cost / self.strategy.leverage
return base_cost
@property
def entry_margin(self) -> float:

View File

@@ -37,14 +37,6 @@ class Broker:
if price < 0:
raise ValueError('price cannot be negative.')
# if price <= self.position.current_price:
# raise OrderNotAllowed(
# 'Cannot LIMIT sell at ${} when current_price is ${}'.format(
# price,
# self.position.current_price
# )
# )
return self.api.limit_order(
self.exchange,
self.symbol,
@@ -74,14 +66,6 @@ class Broker:
if price < 0:
raise ValueError('price cannot be negative.')
# if price >= self.position.current_price:
# raise OrderNotAllowed(
# 'Cannot LIMIT buy at ${} when current_price is ${}'.format(
# price,
# self.position.current_price
# )
# )
return self.api.limit_order(
self.exchange,
self.symbol,
@@ -109,23 +93,6 @@ class Broker:
side = jh.opposite_side(jh.type_to_side(self.position.type))
# validation
if side == 'buy' and price > self.position.current_price:
raise OrderNotAllowed(
'Cannot reduce (via LIMIT) buy at ${} when current_price is ${}'.format(
price,
self.position.current_price
)
)
# validation
if side == 'sell' and price < self.position.current_price:
raise OrderNotAllowed(
'Cannot reduce (via LIMIT) sell at ${} when current_price is ${}'.format(
price,
self.position.current_price
)
)
if price == self.position.current_price:
return self.api.market_order(
self.exchange,
@@ -137,15 +104,28 @@ class Broker:
[order_flags.REDUCE_ONLY]
)
return self.api.limit_order(
self.exchange,
self.symbol,
qty,
price,
side,
role,
[order_flags.REDUCE_ONLY]
)
elif (side == 'sell' and self.position.type == 'long' and price > self.position.current_price) or (side == 'buy' and self.position.type == 'short' and price < self.position.current_price):
return self.api.limit_order(
self.exchange,
self.symbol,
qty,
price,
side,
role,
[order_flags.REDUCE_ONLY]
)
elif (side == 'sell' and self.position.type == 'long' and price < self.position.current_price) or (side == 'buy' and self.position.type == 'short' and price > self.position.current_price):
return self.api.stop_order(
self.exchange,
self.symbol,
abs(qty),
price,
side,
role,
[order_flags.REDUCE_ONLY]
)
else:
raise OrderNotAllowed("This order doesn't seem to be for reducing the position.")
def start_profit_at(self, side: str, qty: float, price: float, role: str = None) -> Order:
self._validate_qty(qty)

View File

@@ -7,7 +7,7 @@ import jesse.helpers as jh
def generate_candle_from_one_minutes(timeframe: str,
candles: np.ndarray,
accept_forming_candles: bool = False) -> np.array:
accept_forming_candles: bool = False) -> np.ndarray:
if len(candles) == 0:
raise ValueError('No candles were passed')
@@ -29,7 +29,7 @@ def generate_candle_from_one_minutes(timeframe: str,
])
def print_candle(candle: np.array, is_partial: bool, symbol: str) -> None:
def print_candle(candle: np.ndarray, is_partial: bool, symbol: str) -> None:
if jh.should_execute_silently():
return

View File

@@ -522,33 +522,13 @@ class Strategy(ABC):
for o in self._take_profit:
self._log_take_profit.append(o)
if o[1] == self.price:
if self.is_long:
self._take_profit_orders.append(
self.broker.sell_at_market(o[0], role=order_roles.CLOSE_POSITION)
)
elif self.is_short:
self._take_profit_orders.append(
self.broker.buy_at_market(o[0], role=order_roles.CLOSE_POSITION)
)
else:
if (self.is_long and o[1] > self.price) or (self.is_short and o[1] < self.price):
self._take_profit_orders.append(
self.broker.reduce_position_at(
o[0],
o[1],
order_roles.CLOSE_POSITION
)
)
elif (self.is_long and o[1] < self.price) or (self.is_short and o[1] > self.price):
self._take_profit_orders.append(
self.broker.stop_loss_at(
o[0],
o[1],
order_roles.CLOSE_POSITION
)
)
self._take_profit_orders.append(
self.broker.reduce_position_at(
o[0],
o[1],
order_roles.CLOSE_POSITION
)
)
if self.position.is_open and self.stop_loss is not None:
self._validate_stop_loss()
@@ -574,23 +554,13 @@ class Strategy(ABC):
for o in self._stop_loss:
self._log_stop_loss.append(o)
if o[1] == self.price:
if self.is_long:
self._stop_loss_orders.append(
self.broker.sell_at_market(o[0], role=order_roles.CLOSE_POSITION)
)
elif self.is_short:
self._stop_loss_orders.append(
self.broker.buy_at_market(o[0], role=order_roles.CLOSE_POSITION)
)
else:
self._stop_loss_orders.append(
self.broker.stop_loss_at(
o[0],
o[1],
order_roles.CLOSE_POSITION
)
self._stop_loss_orders.append(
self.broker.reduce_position_at(
o[0],
o[1],
order_roles.CLOSE_POSITION
)
)
except TypeError:
raise exceptions.InvalidStrategy(
'Something odd is going on with your strategy. '

View File

@@ -0,0 +1,25 @@
from jesse.strategies import Strategy
from jesse import utils
class TestReduceOnlyMarketOrders(Strategy):
def should_long(self) -> bool:
return self.price == 10
def should_short(self) -> bool:
return False
def go_long(self):
entry = self.price
qty = utils.size_to_qty(self.capital, entry, fee_rate=self.fee_rate)
self.buy = qty, entry
def go_short(self):
pass
def should_cancel(self):
return False
def update_position(self):
if self.price == 20:
self.liquidate()

View File

@@ -25,53 +25,63 @@ def anchor_timeframe(timeframe: str) -> str:
timeframes.MINUTE_5: timeframes.MINUTE_30,
timeframes.MINUTE_15: timeframes.HOUR_2,
timeframes.MINUTE_30: timeframes.HOUR_3,
timeframes.MINUTE_45: timeframes.HOUR_3,
timeframes.HOUR_1: timeframes.HOUR_4,
timeframes.HOUR_2: timeframes.HOUR_6,
timeframes.HOUR_3: timeframes.DAY_1,
timeframes.HOUR_4: timeframes.DAY_1,
timeframes.HOUR_6: timeframes.DAY_1,
timeframes.HOUR_8: timeframes.DAY_1,
timeframes.HOUR_12: timeframes.DAY_1,
timeframes.DAY_1: timeframes.WEEK_1,
timeframes.DAY_3: timeframes.WEEK_1,
}
return dic[timeframe]
def crossed(series1: np.array, series2: Union[float, int, np.array], direction: str = None,
def crossed(series1: np.ndarray, series2: Union[float, int, np.array], direction: str = None,
sequential: bool = False) -> bool:
"""
Helper for detecion of crosses
:param series1: np.array
:param series1: np.ndarray
:param series2: float, int, np.array
:param direction: str - default: None - above or below
:return: bool
"""
series1 = pd.Series(series1)
series2 = pd.Series(index=series1.index, data=series2)
if sequential:
series1_shifted = jh.np_shift(series1, 1, np.nan)
if type(series2) is np.ndarray:
series2_shifted = jh.np_shift(series2, 1, np.nan)
else:
series2_shifted = series2
if direction is None or direction == "above":
cross_above = pd.Series((series1 > series2) & (series1.shift(1) <= series2.shift(1)))
cross_above = np.logical_and(series1 > series2, series1_shifted <= series2_shifted)
if direction is None or direction == "below":
cross_below = pd.Series((series1 < series2) & (series1.shift(1) >= series2.shift(1)))
cross_below = np.logical_and(series1 < series2, series1_shifted >= series2_shifted)
if direction is None:
cross_any = cross_above | cross_below
return cross_any.to_numpy()
cross_any = np.logical_or(cross_above, cross_below)
return cross_any
if direction == "above":
return cross_above.to_numpy()
return cross_above
else:
return cross_below.to_numpy()
return cross_below
else:
if not type(series2) is np.ndarray:
series2 = np.array([series2, series2])
if direction is None or direction == "above":
cross_above = series1.iloc[-2] <= series2.iloc[-2] and series1.iloc[-1] > series2.iloc[-1]
cross_above = series1[-2] <= series2[-2] and series1[-1] > series2[-1]
if direction is None or direction == "below":
cross_below = series1.iloc[-2] >= series2.iloc[-2] and series1.iloc[-1] < series2.iloc[-1]
cross_below = series1[-2] >= series2[-2] and series1[-1] < series2[-1]
if direction is None:
return cross_above or cross_below
@@ -233,19 +243,19 @@ def sum_floats(float1: float, float2: float) -> float:
return float(Decimal(str(float1)) + Decimal(str(float2)))
def strictly_increasing(series: np.array, lookback: int) -> bool:
def strictly_increasing(series: np.ndarray, lookback: int) -> bool:
a = series[-lookback:]
diff = np.diff(a)
return np.all(diff > 0)
def strictly_decreasing(series: np.array, lookback: int) -> bool:
def strictly_decreasing(series: np.ndarray, lookback: int) -> bool:
a = series[-lookback:]
diff = np.diff(a)
return np.all(diff < 0)
def streaks(series: np.array, use_diff=True) -> np.array:
def streaks(series: np.ndarray, use_diff=True) -> np.ndarray:
if use_diff:
series = np.diff(series)
pos = np.clip(series, 0, 1).astype(bool).cumsum()
@@ -257,7 +267,7 @@ def streaks(series: np.array, use_diff=True) -> np.array:
return res
def signal_line(series: np.array, period: int = 10, matype: int = 0) -> np.array:
def signal_line(series: np.ndarray, period: int = 10, matype: int = 0) -> np.ndarray:
return MA(series, timeperiod=period, matype=matype)

View File

@@ -1,20 +1,20 @@
arrow==1.0.2
arrow==1.0.3
blinker==1.4
click==7.1.2
crypto-empyrical==1.0.4
matplotlib==3.3.4
newtulipy==0.4.4
numba==0.53.0rc2
numba==0.53.0rc3
numpy==1.20.1
numpy_groupies==0.9.13
pandas==1.2.3
peewee==3.14.1
peewee==3.14.2
psycopg2-binary==2.8.6
pydash==4.9.2
pydash==4.9.3
pytest==6.2.2
requests==2.25.1
scipy==1.6.1
TA-Lib==0.4.19
tabulate==0.8.9
timeloop==1.0.2
websocket-client==0.57.0
websocket-client==0.58.0

View File

@@ -322,6 +322,12 @@ def test_now_to_timestamp():
from jesse.store import store
assert jh.now_to_timestamp() == store.app.time
def test_np_ffill():
arr = np.array([0, 1, np.nan, np.nan])
res = jh.np_ffill(arr)
expected = np.array([0, 1, 1, 1])
np.equal(res, expected)
def test_np_shift():
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])

View File

@@ -891,6 +891,10 @@ def test_leverage_property():
single_route_backtest('TestLeverageProperty2', is_futures_trading=True, leverage=2)
def test_reduce_only_market_orders():
single_route_backtest('TestReduceOnlyMarketOrders', is_futures_trading=True, leverage=1)
# def test_route_capital_isolation():
# set_up(
# [

View File

@@ -35,6 +35,18 @@ def test_crossed():
assert seq_cross_200[-5] == False
seq_cross_120 = utils.crossed(candles[:, 2], 120, sequential=True)
assert seq_cross_120[-1] == True
array_array_cross_above = utils.crossed(np.array([1., 2, 3, 4, 5, 6]), np.array([3., 3, 3, 3, 3, 3]),
direction="above",
sequential=True)
assert array_array_cross_above[-3] == True
array_array_cross_below = utils.crossed(np.array([1., 2, 3, 2, 1, 6]), np.array([3., 3, 3, 3, 3, 3]),
direction="below",
sequential=True)
assert array_array_cross_below[-3] == True
array_array_cross = utils.crossed(np.array([1., 2, 3, 4, 1, 2]), np.array([3., 3, 3, 3, 3, 3]),
sequential=True)
assert array_array_cross[-3] == True
assert array_array_cross[-2] == True
def test_estimate_risk():