This commit is contained in:
Ichinga Samuel
2026-02-28 18:16:28 +01:00
parent 98e1ec34ac
commit d47a853f94
9 changed files with 1684 additions and 15 deletions
+1578
View File
File diff suppressed because it is too large Load Diff
View File
+16
View File
@@ -0,0 +1,16 @@
import asyncio
from aiomql import Bot, ForexSymbol
from .strategies.ribbon_scalper import RibbonScalper
async def rs_bot():
bot = Bot()
syms = ["ETHUSD", "BTCUSD", "ADAUSD", "SOLUSD", "LTCUSD", "XRPUSD"]
strategies = [RibbonScalper(symbol=ForexSymbol(name=sym)) for sym in syms]
bot.add_strategies(strategies=strategies)
await bot.start()
if __name__ == "__main__":
asyncio.run(rs_bot())
@@ -0,0 +1,59 @@
from aiomql import Strategy, ForexSymbol, TimeFrame, OrderType, Tracker, Trader
from ..traders.rs_trader import RSTrader
class RibbonScalper(Strategy):
"""
Entry Strategy (Long): When the 5 and 8 EMA cross above the 13 EMA and the EMAs start spreading out (indicating a trend).
Entry Strategy (Short): When the 5 and 8 EMA cross below the 13 EMA and spread out.
Exit Strategy (Take Profit): Set a small, fixed profit target (e.g., 5-10 pips) or exit when the EMA ribbon starts to flatten or twist.
Exit Strategy (Stop Loss): Tight stop-loss the previous candle high/low to limit risk.
"""
fast_ema: int # fast moving average
medium_ema: int # medium moving average
slow_ema: int # slow moving average
time_frame: TimeFrame # time frame
candles_count: int # lookback period
tracker: Tracker # a tracker class
pips_target: int # number of pips to target
interval: int # interval between successive runs in seconds
parameters = {"fast_ema": 5, "medium_ema": 8, "slow_ema": 13, "time_frame": TimeFrame.M1,
"candles_count": 120, "pips_target": 10, "interval": 30}
def __init__(self, *, symbol: ForexSymbol, params: dict = None, trader: Trader = None, name="RibbonScalper"):
super().__init__(symbol=symbol, params=params, name=name)
self.tracker = Tracker()
self.trader = trader or RSTrader(symbol=symbol)
self.interval = self.time_frame.seconds
async def find_entry(self):
rates = await self.symbol.copy_rates_from_pos(count=self.candles_count, timeframe=self.time_frame)
rates.ta.ema(length=self.fast_ema, append=True)
rates.ta.ema(length=self.medium_ema, append=True)
rates.ta.ema(length=self.slow_ema, append=True)
rates.rename(**{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.medium_ema}": "medium", f"EMA_{self.slow_ema}": "slow"})
find_long_fast = rates.ta_lib.cross(rates.fast, rates.slow)
find_long_medium = rates.ta_lib.cross(rates.medium, rates.slow)
if find_long_fast.iloc[-1] and find_long_medium.iloc[-1]:
self.tracker.update(order_type=OrderType.BUY, trend="bullish", sl=rates[-2].low, snooze=300)
return
find_short_fast = rates.ta_lib.cross(rates.fast, rates.slow, above=False)
find_short_medium = rates.ta_lib.cross(rates.medium, rates.slow, above=False)
if find_short_fast.iloc[-1] and find_short_medium.iloc[-1]:
self.tracker.update(order_type=OrderType.SELL, trend="bearish", sl=rates[-2].high, snooze=300)
return
self.tracker.update(order_type=None, trend="ranging", snooze=self.interval)
async def trade(self):
await self.find_entry()
if self.tracker.order_type is not None:
await self.trader.place_trade(parameters=self.parameters, order_type=self.tracker.order_type,
sl=self.tracker.sl, pips_target=self.pips_target)
await self.sleep(secs=self.tracker.snooze)
return
else:
await self.sleep(secs=self.tracker.snooze)
@@ -0,0 +1,30 @@
from logging import getLogger
from aiomql import Trader, OrderType
logger = getLogger(__name__)
class RSTrader(Trader):
async def create_order(self, sl: float, pips_target: int = 5):
try:
amount = await self.ram.get_amount()
tick = await self.symbol.info_tick()
price = tick.ask if self.order.type.is_long else tick.bid
volume = await self.symbol.compute_volume_sl(sl=sl, amount=amount, price=price)
tp = price + (self.symbol.point * 10 * pips_target)
self.order.set_attributes(sl=sl, tp=tp, volume=volume, price=price)
except Exception as exe:
logger.error("%s: unable to create order", exe)
async def place_trade(self, sl: float, order_type: OrderType, pips_target: int = 10, parameters: dict = None):
try:
self.order.type = order_type
await self.create_order(sl=sl, pips_target=pips_target)
parameters = parameters or {}
self.order.set_attributes(comment=comment) if (comment := parameters.get("name")) else ...
res = await self.send_order()
if res and res.retcode == 10009:
await self.record_trade(result=res, parameters=parameters)
self.reset_order()
except Exception as exe:
logger.error("%s: unable to place trade", exe)
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "aiomql" name = "aiomql"
version = "4.1.1" version = "4.1.2"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
classifiers = [ classifiers = [
-14
View File
@@ -17,7 +17,6 @@ import inspect
import os import os
import time import time
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from signal import signal, SIGINT
from typing import Coroutine, Callable from typing import Coroutine, Callable
from logging import getLogger from logging import getLogger
@@ -61,7 +60,6 @@ class Executor:
self.functions: dict[Callable:dict] = {} self.functions: dict[Callable:dict] = {}
self.config = Config() self.config = Config()
self.timeout = None # Timeout for the executor. For testing purposes only self.timeout = None # Timeout for the executor. For testing purposes only
signal(SIGINT, self.sigint_handle)
def add_function(self, *, function: Callable, kwargs: dict = None): def add_function(self, *, function: Callable, kwargs: dict = None):
"""Registers a synchronous function to run in the executor. """Registers a synchronous function to run in the executor.
@@ -144,16 +142,6 @@ class Executor:
""" """
function(**kwargs) function(**kwargs)
def sigint_handle(self, signum, frame):
"""Handles SIGINT (Ctrl+C) by signaling a shutdown.
Args:
signum: The signal number received.
frame: The current stack frame.
"""
print("shutting down")
self.config.shutdown = True
def exit(self): def exit(self):
"""Monitors for shutdown signals and gracefully shuts down the executor. """Monitors for shutdown signals and gracefully shuts down the executor.
@@ -193,11 +181,9 @@ class Executor:
Notes: Notes:
No matter the number specified, the executor will always use a minimum of 5 workers. No matter the number specified, the executor will always use a minimum of 5 workers.
""" """
signal(SIGINT, self.sigint_handle)
workers_ = len(self.strategy_runners) + len(self.functions) + len(self.coroutine_threads) + 3 workers_ = len(self.strategy_runners) + len(self.functions) + len(self.coroutine_threads) + 3
workers = max(workers, workers_) workers = max(workers, workers_)
with ThreadPoolExecutor(max_workers=workers) as executor: with ThreadPoolExecutor(max_workers=workers) as executor:
signal(SIGINT, self.sigint_handle)
self.executor = executor self.executor = executor
[self.executor.submit(self.run_strategy, strategy) for strategy in self.strategy_runners] [self.executor.submit(self.run_strategy, strategy) for strategy in self.strategy_runners]
[self.executor.submit(function, **kwargs) for function, kwargs in self.functions.items()] [self.executor.submit(function, **kwargs) for function, kwargs in self.functions.items()]