Merge branch 'master' into inverse-futures
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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. '
|
||||
|
||||
25
jesse/strategies/TestReduceOnlyMarketOrders/__init__.py
Normal file
25
jesse/strategies/TestReduceOnlyMarketOrders/__init__.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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(
|
||||
# [
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user