This commit is contained in:
Ichinga Samuel
2025-01-16 19:32:41 +01:00
parent efb43d5be7
commit 77bea7fcb9
16 changed files with 122 additions and 38 deletions
+15
View File
@@ -0,0 +1,15 @@
# Changelog
## [4.0.7](https://github.com/Ichinga-Samuel/aiomql/releases/edit/untagged-7d784e53ee13316aac97) - 2025-01-16
### Changed
- Candles underlying DataFrame is now indexed by datetime.
- Executor runs a strategy via the `run_strategy` method directly with `asyncio.run` without creating as a task.
- `Strategy` class now has a initialize method that is called before the strategy is run.
### Added
- `__add__` and `__iadd__` dunder methods for addition and inplace addition of dataframes or series objects to the Candles object.
- `add` method for adding dataframes or series objects to the Candles object.
### Fixed
- Candles timeframe attribute returns the correct TimeFrame object.
+5
View File
@@ -210,6 +210,11 @@ see [API Documentation](docs) for more details
### Contributing ### Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
### Changelog
See [CHANGELOG](CHANGELOG.md) for more details
### Support ### Support
Feeling generous, like the package or want to see it become a more mature package? Feeling generous, like the package or want to see it become a more mature package?
@@ -0,0 +1,17 @@
{
"balance": 297007.45,
"profit": 0,
"equity": 297007.45,
"margin": 0.0,
"margin_free": 297007.45,
"margin_level": 0,
"wins": 307,
"losses": 336,
"total": 643,
"win_percentage": 47.74,
"win": 619421.92,
"loss": -323164.47,
"net_profit": 296257.45,
"profit_factor": 1.92,
"profitability": 39500.99
}
@@ -1,17 +1,17 @@
{ {
"balance": 504.26, "balance": 1251.1,
"profit": 0, "profit": 0,
"equity": 504.26, "equity": 1251.1,
"margin": 0.0, "margin": 0.0,
"margin_free": 504.26, "margin_free": 1251.1,
"margin_level": 0, "margin_level": 0,
"wins": 12, "wins": 29,
"losses": 15, "losses": 31,
"total": 27, "total": 60,
"win_percentage": 44.44, "win_percentage": 48.33,
"win": 326.55, "win": 1530.54,
"loss": -172.29, "loss": -1029.44,
"net_profit": 154.26, "net_profit": 501.1,
"profit_factor": 1.9, "profit_factor": 1.49,
"profitability": 44.07 "profitability": 66.81
} }
+4 -4
View File
@@ -14,12 +14,12 @@ def back_tester():
syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 25 Index", "Volatility 10 Index"] syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 25 Index", "Volatility 10 Index"]
symbols = [ForexSymbol(name=sym) for sym in syms] symbols = [ForexSymbol(name=sym) for sym in syms]
strategies = [FingerTrap(symbol=symbol) for symbol in symbols] strategies = [FingerTrap(symbol=symbol) for symbol in symbols]
start = datetime(2024, 5, 1, tzinfo=UTC) start = datetime(2024, 1, 1, tzinfo=UTC)
stop_time = datetime(2024, 5, 2, tzinfo=UTC) stop_time = datetime(2024, 12, 2, tzinfo=UTC)
end = datetime(2024, 5, 7, tzinfo=UTC) end = datetime(2024, 5, 7, tzinfo=UTC)
back_test_engine = BackTestEngine(start=start, end=end, speed=7200, back_test_engine = BackTestEngine(start=start, end=end, speed=3600,
close_open_positions_on_exit=True, assign_to_config=True, preload=True, close_open_positions_on_exit=True, assign_to_config=True, preload=True,
account_info={"balance": 350}) account_info={"balance": 750})
backtester = BackTester(backtest_engine=back_test_engine) backtester = BackTester(backtest_engine=back_test_engine)
backtester.add_strategies(strategies=strategies) backtester.add_strategies(strategies=strategies)
backtester.execute() backtester.execute()
-1
View File
@@ -13,7 +13,6 @@ def sample_bot():
strategies = [Chaos(symbol=symbol) for symbol in symbols] strategies = [Chaos(symbol=symbol) for symbol in symbols]
bot = Bot() bot = Bot()
bot.executor.timeout = 10 bot.executor.timeout = 10
bot.add_coroutine(coroutine=sleep_run) bot.add_coroutine(coroutine=sleep_run)
bot.add_strategies(strategies=strategies) bot.add_strategies(strategies=strategies)
bot.execute() bot.execute()
+4
View File
@@ -0,0 +1,4 @@
slow_ema,fast_ema,order,htf,actual_profit,symbol,date,closed,name,price,deal,ltf,bid,win,ask,lcc,volume,expected_profit,hcc
20,8,8218315320,TIMEFRAME_M2,0,Volatility 75 Index,2025-01-16 11:49:50.421946,False,Chaos,96536.19,8126025355,TIMEFRAME_M1,96536.19,False,96562.33,100,0.001,0,100
20,8,8218315356,TIMEFRAME_M2,0,Volatility 50 Index,2025-01-16 11:49:54.394496,False,Chaos,272.9174,8126025389,TIMEFRAME_M1,272.9174,False,272.9584,100,4.0,0,100
20,8,8218315338,TIMEFRAME_M2,0,Volatility 100 Index,2025-01-16 11:49:54.395497,False,Chaos,1939.81,8126025382,TIMEFRAME_M1,1939.27,False,1939.81,100,0.5,0,100
1 slow_ema fast_ema order htf actual_profit symbol date closed name price deal ltf bid win ask lcc volume expected_profit hcc
2 20 8 8218315320 TIMEFRAME_M2 0 Volatility 75 Index 2025-01-16 11:49:50.421946 False Chaos 96536.19 8126025355 TIMEFRAME_M1 96536.19 False 96562.33 100 0.001 0 100
3 20 8 8218315356 TIMEFRAME_M2 0 Volatility 50 Index 2025-01-16 11:49:54.394496 False Chaos 272.9174 8126025389 TIMEFRAME_M1 272.9174 False 272.9584 100 4.0 0 100
4 20 8 8218315338 TIMEFRAME_M2 0 Volatility 100 Index 2025-01-16 11:49:54.395497 False Chaos 1939.81 8126025382 TIMEFRAME_M1 1939.27 False 1939.81 100 0.5 0 100
@@ -95,6 +95,8 @@ class BackTestController:
await self.backtest_engine.wrap_up() await self.backtest_engine.wrap_up()
self.stop_backtesting() self.stop_backtesting()
except BrokenBarrierError: except BrokenBarrierError:
await self.backtest_engine.wrap_up()
self.stop_backtesting()
return return
except Exception as err: except Exception as err:
@@ -71,6 +71,7 @@ class BackTestEngine:
preload: bool preload: bool
account_lock: RLock account_lock: RLock
account_info: dict account_info: dict
checkpoint: float
def __init__( def __init__(
self, self,
@@ -87,6 +88,7 @@ class BackTestEngine:
preload=True, preload=True,
assign_to_config: bool = True, assign_to_config: bool = True,
account_info: dict = None, account_info: dict = None,
checkpoint: float = 0.02
): ):
self._data = data or BackTestData() self._data = data or BackTestData()
self.mt5 = MetaTrader() self.mt5 = MetaTrader()
@@ -115,6 +117,7 @@ class BackTestEngine:
self.preloaded_ticks = {} self.preloaded_ticks = {}
self.account_lock = RLock() self.account_lock = RLock()
self.account_info = account_info or {} self.account_info = account_info or {}
self.checkpoint = checkpoint
def __next__(self) -> Cursor: def __next__(self) -> Cursor:
try: try:
@@ -252,6 +255,8 @@ class BackTestEngine:
profit = sum(pos.profit for pos in self.positions.open_positions) profit = sum(pos.profit for pos in self.positions.open_positions)
self.update_account(profit=profit) self.update_account(profit=profit)
self.check_account() self.check_account()
if int(self.cursor.index % (self.range.stop * self.checkpoint)) == 0:
await asyncio.to_thread(self.save_result_to_json)
except Exception as exe: except Exception as exe:
logger.critical("Error in tracker: %s at %d", exe, self.cursor.time) logger.critical("Error in tracker: %s at %d", exe, self.cursor.time)
-1
View File
@@ -19,7 +19,6 @@ logger = getLogger(__name__)
class Cursor(NamedTuple): class Cursor(NamedTuple):
"""A cursor to iterate over the data. Marks the current position.""" """A cursor to iterate over the data. Marks the current position."""
index: int index: int
time: int time: int
+6 -2
View File
@@ -26,6 +26,8 @@ class Config:
record_trades: bool record_trades: bool
records_dir: Path records_dir: Path
backtest_dir: Path backtest_dir: Path
records_dir_name: str
backtest_dir_name: str #Todo: add to docs
task_queue: TaskQueue task_queue: TaskQueue
_backtest_engine: BackTestEngine _backtest_engine: BackTestEngine
bot: Bot bot: Bot
@@ -38,6 +40,8 @@ class Config:
_defaults = { _defaults = {
"timeout": 60000, "timeout": 60000,
"record_trades": True, "record_trades": True,
"records_dir_name": "trade_records",
"backtest_dir_name": "backtesting",
"config_file": None, "config_file": None,
"trade_record_mode": "csv", "trade_record_mode": "csv",
"mode": "live", "mode": "live",
@@ -168,13 +172,13 @@ class Config:
@property @property
def records_dir(self): def records_dir(self):
rec_dir = self.root / 'trade_records' rec_dir = self.root / self.records_dir_name or 'trade_records'
rec_dir.mkdir(parents=True, exist_ok=True) if rec_dir.exists() is False else ... rec_dir.mkdir(parents=True, exist_ok=True) if rec_dir.exists() is False else ...
return rec_dir return rec_dir
@property @property
def backtest_dir(self) -> Path: def backtest_dir(self) -> Path:
b_dir = self.root / 'backtesting' b_dir = self.root / self.backtest_dir_name or 'backtesting'
b_dir.mkdir(parents=True, exist_ok=True) if b_dir.exists() is False else ... b_dir.mkdir(parents=True, exist_ok=True) if b_dir.exists() is False else ...
return b_dir return b_dir
-1
View File
@@ -149,7 +149,6 @@ class BackTester:
strategy (Strategy): Strategy class strategy (Strategy): Strategy class
params (dict): A dictionary of parameters for the strategy params (dict): A dictionary of parameters for the strategy
symbols (list): A list of symbols to run the strategy on symbols (list): A list of symbols to run the strategy on
**kwargs: Additional keyword arguments for the strategy
""" """
[self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs)) for symbol in symbols] [self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs)) for symbol in symbols]
-1
View File
@@ -157,7 +157,6 @@ class Bot:
strategy (Strategy): Strategy class strategy (Strategy): Strategy class
params (dict): A dictionary of parameters for the strategy params (dict): A dictionary of parameters for the strategy
symbols (list): A list of symbols to run the strategy on symbols (list): A list of symbols to run the strategy on
**kwargs: Additional keyword arguments for the strategy
""" """
[self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs)) for symbol in symbols] [self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs)) for symbol in symbols]
+47 -10
View File
@@ -5,6 +5,7 @@ from typing import Type, Self, Iterable
from logging import getLogger from logging import getLogger
from pandas import DataFrame, Series from pandas import DataFrame, Series
import pandas as pd
import pandas_ta as ta import pandas_ta as ta
from ..core.constants import TimeFrame from ..core.constants import TimeFrame
@@ -27,8 +28,7 @@ class Candle:
spread (float): Spread spread (float): Spread
Index (int): Custom attribute representing the position of the candle in a sequence. Index (int): Custom attribute representing the position of the candle in a sequence.
""" """
time: int
time: float
open: float open: float
high: float high: float
low: float low: float
@@ -47,8 +47,8 @@ class Candle:
""" """
if not all(i in kwargs for i in ["open", "high", "low", "close"]): if not all(i in kwargs for i in ["open", "high", "low", "close"]):
raise ValueError("Candle must be instantiated with open, high, low and close prices") raise ValueError("Candle must be instantiated with open, high, low and close prices")
self.time = kwargs.pop("time", time.monotonic_ns()) self.time = kwargs.pop("time", int(time.time()))
self.Index = kwargs.pop("Index", 0) self.Index = kwargs.pop("Index", self.time)
self.real_volume = kwargs.pop("real_volume", 0) self.real_volume = kwargs.pop("real_volume", 0)
self.spread = kwargs.pop("spread", 0) self.spread = kwargs.pop("spread", 0)
self.tick_volume = kwargs.pop("tick_volume", 0) self.tick_volume = kwargs.pop("tick_volume", 0)
@@ -188,7 +188,9 @@ class Candles:
else: else:
raise ValueError(f"Cannot create DataFrame from object of {type(data)}") raise ValueError(f"Cannot create DataFrame from object of {type(data)}")
self._data = data.loc[::-1].reset_index(drop=True) if flip else data self._data = data.loc[::-1] if flip else data
if 'time' in self._data.columns and self._data.index.name != 'time':
self._data.set_index('time', inplace=True, drop=False)
self.Candle = candle_class or Candle self.Candle = candle_class or Candle
def __repr__(self): def __repr__(self):
@@ -198,13 +200,12 @@ class Candles:
return len(self._data.index) return len(self._data.index)
def __contains__(self, item: Candle): def __contains__(self, item: Candle):
return item.time == self[item.Index].time return item.time == self._data.loc[int(item.Index)].time
def __getitem__(self, index: slice | int | str) -> Self | Series | Candle: def __getitem__(self, index: slice | int | str) -> Self | Series | Candle:
if isinstance(index, slice): if isinstance(index, slice):
cls = self.__class__ cls = self.__class__
data = self._data.iloc[index] data = self._data.iloc[index]
data.reset_index(drop=True, inplace=True)
return cls(data=data) return cls(data=data)
elif isinstance(index, str): elif isinstance(index, str):
@@ -213,8 +214,8 @@ class Candles:
return self._data[index] return self._data[index]
elif isinstance(index, int): elif isinstance(index, int):
index_ = index if index >= 0 else len(self) + index candle = self._data.iloc[index]
return self.Candle(**self._data.iloc[index], Index=index_) return self.Candle(**candle, Index=int(candle.time))
raise TypeError(f"Expected int, slice or str got {type(index)}") raise TypeError(f"Expected int, slice or str got {type(index)}")
def __setitem__(self, index, value: Series): def __setitem__(self, index, value: Series):
@@ -236,7 +237,7 @@ class Candles:
@property @property
def timeframe(self): def timeframe(self):
tf = self.time[1] - self.time[0] tf = self.time.iloc[1] - self.time.iloc[0]
return TimeFrame.get_timeframe(abs(tf)) return TimeFrame.get_timeframe(abs(tf))
@property @property
@@ -278,3 +279,39 @@ class Candles:
""" """
res = self._data.rename(columns=kwargs, inplace=inplace) res = self._data.rename(columns=kwargs, inplace=inplace)
return self if inplace else self.__class__(data=res) return self if inplace else self.__class__(data=res)
def __iadd__(self, row: DataFrame | Series):
"""Add a new row to the candles class."""
# self._data = pd.concat([self._data, row])
if isinstance(row, Series):
self._data.loc[int(row.time)] = row
elif isinstance(row, DataFrame):
for r in row.iloc:
self._data.loc[int(r.time)] = r
return self
def __add__(self, row: DataFrame | Series):
"""Add a new row to the candles class."""
if isinstance(row, Series):
return pd.concat([self._data, pd.DataFrame(row).T])
elif isinstance(row, DataFrame):
return pd.concat([self._data, row])
def add(self, row: DataFrame | Series) -> bool:
"""Add a new row to the candles class."""
new = True
if isinstance(row, Series):
if (index := int(row.time)) in self._data.index:
new = False
self._data.loc[index] = row
elif isinstance(row, DataFrame):
for r in row.iloc:
if (index := int(r.time)) in self._data.index:
new = False
self._data.loc[index] = r
return new
+5 -5
View File
@@ -60,10 +60,10 @@ class Executor:
""" """
self.strategy_runners.append(strategy) self.strategy_runners.append(strategy)
async def create_strategy_task(self, strategy: Strategy): # async def create_strategy_task(self, strategy: Strategy):
task = asyncio.create_task(strategy.run_strategy()) # task = asyncio.create_task(strategy.run_strategy())
self.tasks.append(task) # self.tasks.append(task)
await task # await task
def run_strategy(self, strategy: Strategy): def run_strategy(self, strategy: Strategy):
"""Wraps the coroutine trade method of each strategy with 'asyncio.run'. """Wraps the coroutine trade method of each strategy with 'asyncio.run'.
@@ -71,7 +71,7 @@ class Executor:
Args: Args:
strategy (Strategy): A strategy object strategy (Strategy): A strategy object
""" """
asyncio.run(self.create_strategy_task(strategy)) asyncio.run(strategy.run_strategy())
async def create_coroutine_task(self, coroutine: Coroutine): async def create_coroutine_task(self, coroutine: Coroutine):
task = asyncio.create_task(coroutine) task = asyncio.create_task(coroutine)
-1
View File
@@ -66,7 +66,6 @@ class TestCandles:
candle = candles[10] candle = candles[10]
assert isinstance(candle, Candle) assert isinstance(candle, Candle)
assert candle in candles assert candle in candles
assert candle.Index == 10
def test_slice(self, candles): def test_slice(self, candles):
sliced = candles[10:15] sliced = candles[10:15]