Skip to content

Commit

Permalink
Add Turtle Strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
math-a3k committed Dec 1, 2023
1 parent 8c54900 commit db370cf
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 5 deletions.
2 changes: 2 additions & 0 deletions base/indicators.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ def calculate(self):
ts_l = ts_l[-min_len:].reset_index(drop=True)
ts_s = ts_s[-min_len:].reset_index(drop=True)
ts_s_var = ts_s.pct_change().dropna()
ts_m_var = ts_m.pct_change().dropna()
ts_l_var = ts_l.pct_change().dropna()
ts_diff_ml = ts_m - ts_l
ts_diff_sm = ts_s - ts_m
Expand Down Expand Up @@ -204,6 +205,7 @@ def calculate(self):
"line_l": ts_l.to_list(),
"line_s": ts_s.to_list(),
"line_s_var": ts_s_var.to_list(),
"line_m_var": ts_m_var.to_list(),
"line_l_var": ts_l_var.to_list(),
"line_diff_ml": ts_diff_ml.to_list(),
"line_diff_sm": ts_diff_sm.to_list(),
Expand Down
112 changes: 107 additions & 5 deletions base/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ def local_memory_update(self):
def has_local_memory(self, symbol):
pass

def local_memory_available(self):
return self.use_local_memory and self.bot.has_local_memory(
self.symbol, strategy=self
)

def get_param(self, param, kwargs):
param_value = kwargs.get(param, self.params[param]["default"])
if self.params[param]["type"] == "bool":
Expand Down Expand Up @@ -216,6 +221,108 @@ def buying_protections(self):
return (True, None)


class Turtle(TradingStrategy):
"""
Turtle Trading Strategy
(requires the SCG, ATR and DC indicator)
"""

slug = "turtle"
requires = ["scg", "atr", "dc"]
params = {
"use_matrix_time_res": {"default": "0", "type": "bool"},
"vr24h_min": {"default": "3", "type": "decimal"},
}

def __init__(self, bot, symbol=None, **kwargs):
self.bot = bot
self.symbol = symbol or self.bot.symbol
self.local_memory = self.bot.get_local_memory(self.symbol)
self.scg = self.symbol.others["scg"]
self.atr = self.symbol.others["atr"]
self.dc = self.symbol.others["dc"]
self.m_var = self.scg["line_m_var"] # Var of the Middle tendency
#
self.use_local_memory = True
self.use_matrix_time_res = self.get_param(
"use_matrix_time_res", kwargs
)
self.vr24h_min = self.get_param("vr24h_min", kwargs)

def evaluate_buy(self):
if self.use_matrix_time_res and self.time_safeguard:
return (False, "Holding - Using matrix's time resolution...")
should_continue, message = self.buying_protections()
if not should_continue:
return False, message
if self.is_on_good_status() and self.dc["upper_break"]:
return True, None
return (False, "Symbol is not in good status and with upper break...")

def evaluate_sell(self):
if self.use_matrix_time_res and self.time_safeguard:
return (False, "Holding - Using matrix's time resolution")
if self.bot.price_current <= self.get_stop_loss_threshold():
return True, None
if self.dc["lower_break"]:
return True, None
return (False, "Still on board with the strategy")

def evaluate_jump(self):
if self.use_matrix_time_res and self.time_safeguard:
return False, None
symbols_with_siblings = self.get_symbols_with_siblings()
symbols = self.bot.symbol._meta.concrete_model.objects.top_symbols()
symbols = sorted(
symbols, key=lambda s: s.others["dc"]["upper_break"], reverse=True
)
symbols = self.apply_jumpy_lists(symbols)
for symbol in symbols:
strat_in_symbol = self.bot.get_strategy(symbol)
should_buy, _ = strat_in_symbol.evaluate_buy()
if should_buy and symbol.pk not in symbols_with_siblings:
return True, symbol
return False, None

def is_on_good_status(self):
return self.m_var[-1] > 0

def get_stop_loss_threshold(self):
return self.local_memory.get("stop_loss_threshold", 0)

def local_memory_update(self):
if self.use_matrix_time_res and self.time_safeguard:
return
lm = self.bot.get_local_memory()
if not lm:
lm = {"stop_loss_threshold": None}
if self.bot.price_buying and not lm["stop_loss_threshold"]:
lm["stop_loss_threshold"] = float(self.bot.price_buying) - 2 * (
self.symbol.others["atr"]["current"]
)
self.local_memory = lm
self.bot.set_local_memory(self.symbol, lm)

def has_local_memory(self, symbol=None):
symbol = symbol or self.symbol
lm = self.bot.get_local_memory(symbol)
return lm.get("stop_loss_threshold") is not None

def buying_protections(self):
if (
self.vr24h_min > 0
and self.bot.symbol.variation_range_24h < self.vr24h_min
):
return (
False,
f"VR24h below threshold "
f"({self.bot.symbol.variation_range_24h:.3f} < "
f"{self.vr24h_min:.3f}) - waiting for next turn...",
)
return (True, None)


class CatchTheWave(TradingStrategy):
"""
Catch The Wave Trading Strategy
Expand Down Expand Up @@ -361,11 +468,6 @@ def has_local_memory(self, symbol=None):
return True
return False

def local_memory_available(self):
return self.use_local_memory and self.bot.has_local_memory(
self.symbol, strategy=self
)

def buying_protections(self):
if (
self.vr24h_min > 0
Expand Down
67 changes: 67 additions & 0 deletions base/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1864,6 +1864,73 @@ def test_catchthewave(self):
self.bot1.others["last_logs"][-1],
)

def test_turtle(self):
with requests_mock.Mocker() as m:
m.get(
f"{BINANCE_API_URL}/api/v3/ticker/price",
json={"symbol": "S1BUSD", "price": "1.0"},
)
self.bot1.symbol = self.s1
self.bot1.strategy = "turtle"
#
self.s1.others["scg"]["line_m_var"] = [1, 1, 1]
self.s1.others["atr"]["current"] = 0.1
self.s1.others["dc"]["upper_break"] = True
self.bot1.on()
self.bot1.decide()
self.assertIn("Bought", self.bot1.others["last_logs"][-1])
self.bot1.price_current = 0.9
self.bot1.decide()
self.assertIn("Still on board", self.bot1.others["last_logs"][-1])
self.bot1.price_current = 0.7
self.bot1.decide()
self.assertIn("Sold", self.bot1.others["last_logs"][-1])
self.s1.others["dc"]["upper_break"] = False
self.bot1.decide()
self.assertIn(
"not in good status", self.bot1.others["last_logs"][-1]
)
self.bot1.symbol.last_updated = timezone.now()
self.bot1.symbol.variation_range_24h = 1
self.bot1.decide()
self.assertIn(
"VR24h",
self.bot1.others["last_logs"][-1],
)
self.bot1.strategy_params = "use_matrix_time_res=1"
#
self.s1.others["dc"]["upper_break"] = True
self.bot1.symbol.last_updated = (
timezone.now() - timezone.timedelta(minutes=2)
)
self.bot1.price_current = 1.1
self.bot1.decide()
self.assertIn(
"Using matrix's time resolution",
self.bot1.others["last_logs"][-1],
)
#
self.bot1.buy()
self.bot1.decide()
self.assertIn(
"Using matrix's time resolution",
self.bot1.others["last_logs"][-1],
)
self.bot1.strategy_params = None
self.s1.others["dc"]["lower_break"] = True
self.bot1.decide()
self.assertIn("Sold", self.bot1.others["last_logs"][-1])
#
self.bot1.symbol.variation_range_24h = 4
self.s1.others["dc"]["upper_break"] = False
self.s2.others["dc"]["upper_break"] = True
self.s2.others["scg"]["line_m_var"] = [1, 1, 1]
self.s2.others["atr"]["current"] = 0.1
self.s2.save()
self.bot1.decide()
self.assertIn("Jumped", self.bot1.others["last_logs"][-3])
self.assertEqual(self.bot1.has_local_memory(), False)


@pytest.mark.usefixtures("celery_session_app")
@pytest.mark.usefixtures("celery_session_worker")
Expand Down
20 changes: 20 additions & 0 deletions docs/trading_bots.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,28 @@ Parameters
Minimum Variation Range (24h) of a Symbol to buy (``Decimal``, defaults to ``3``) if enabled (greater than zero). Meant to keep the bot out of the market when it is moving sideways narrowly.


Turtle
------

Also known as *little turtle bots*, this strategy [5]_ relies on the :ref:`scg`, ref:`atr`, and ref:`dc` indicators for deciding.

Inspired by this `video <https://www.youtube.com/watch?v=X9edzFqmUyk>`_, this is the implementation of the strategy proposed by Richard Dennis and William Eckhardt in the 1980s, using the `Donchian channel <https://admiralmarkets.com/es/education/articles/forex-indicators/lo-que-todos-deberian-saber-sobre-el-indicador-de-canal-donchian>`_.

Given a Symbol, the bot will buy when it is on "good status" (the middle-term tendency is ascending) and an upper break of the Donchian channel has occurred, while selling when a lower break of the channel occurs or a stop-loss is executed when the price reaches the buying price minus 2 ATRs.

Parameters
^^^^^^^^^^

``use_matrix_time_res``
Use matrix's time resolution (:setting:`TIME_INTERVAL`) (``1`` | ``0``, defaults to ``0``)

``vr24h_min``
Minimum Variation Range (24h) of a Symbol to buy (``Decimal``, defaults to ``3``) if enabled (greater than zero). Meant to keep the bot out of the market when it is moving sideways narrowly.


.. rubric:: References
.. [1] .. autoclass:: base.models.TraderoBot
.. [2] .. autoclass:: base.strategies.TradingStrategy
.. [3] .. autoclass:: base.strategies.ACMadness
.. [4] .. autoclass:: base.strategies.CatchTheWave
.. [5] .. autoclass:: base.strategies.Turtle

0 comments on commit db370cf

Please sign in to comment.