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
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
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,
"equity": 504.26,
"equity": 1251.1,
"margin": 0.0,
"margin_free": 504.26,
"margin_free": 1251.1,
"margin_level": 0,
"wins": 12,
"losses": 15,
"total": 27,
"win_percentage": 44.44,
"win": 326.55,
"loss": -172.29,
"net_profit": 154.26,
"profit_factor": 1.9,
"profitability": 44.07
"wins": 29,
"losses": 31,
"total": 60,
"win_percentage": 48.33,
"win": 1530.54,
"loss": -1029.44,
"net_profit": 501.1,
"profit_factor": 1.49,
"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"]
symbols = [ForexSymbol(name=sym) for sym in syms]
strategies = [FingerTrap(symbol=symbol) for symbol in symbols]
start = datetime(2024, 5, 1, tzinfo=UTC)
stop_time = datetime(2024, 5, 2, tzinfo=UTC)
start = datetime(2024, 1, 1, tzinfo=UTC)
stop_time = datetime(2024, 12, 2, 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,
account_info={"balance": 350})
account_info={"balance": 750})
backtester = BackTester(backtest_engine=back_test_engine)
backtester.add_strategies(strategies=strategies)
backtester.execute()
-1
View File
@@ -13,7 +13,6 @@ def sample_bot():
strategies = [Chaos(symbol=symbol) for symbol in symbols]
bot = Bot()
bot.executor.timeout = 10
bot.add_coroutine(coroutine=sleep_run)
bot.add_strategies(strategies=strategies)
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()
self.stop_backtesting()
except BrokenBarrierError:
await self.backtest_engine.wrap_up()
self.stop_backtesting()
return
except Exception as err:
@@ -71,6 +71,7 @@ class BackTestEngine:
preload: bool
account_lock: RLock
account_info: dict
checkpoint: float
def __init__(
self,
@@ -87,6 +88,7 @@ class BackTestEngine:
preload=True,
assign_to_config: bool = True,
account_info: dict = None,
checkpoint: float = 0.02
):
self._data = data or BackTestData()
self.mt5 = MetaTrader()
@@ -115,6 +117,7 @@ class BackTestEngine:
self.preloaded_ticks = {}
self.account_lock = RLock()
self.account_info = account_info or {}
self.checkpoint = checkpoint
def __next__(self) -> Cursor:
try:
@@ -252,6 +255,8 @@ class BackTestEngine:
profit = sum(pos.profit for pos in self.positions.open_positions)
self.update_account(profit=profit)
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:
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):
"""A cursor to iterate over the data. Marks the current position."""
index: int
time: int
+6 -2
View File
@@ -26,6 +26,8 @@ class Config:
record_trades: bool
records_dir: Path
backtest_dir: Path
records_dir_name: str
backtest_dir_name: str #Todo: add to docs
task_queue: TaskQueue
_backtest_engine: BackTestEngine
bot: Bot
@@ -38,6 +40,8 @@ class Config:
_defaults = {
"timeout": 60000,
"record_trades": True,
"records_dir_name": "trade_records",
"backtest_dir_name": "backtesting",
"config_file": None,
"trade_record_mode": "csv",
"mode": "live",
@@ -168,13 +172,13 @@ class Config:
@property
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 ...
return rec_dir
@property
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 ...
return b_dir
-1
View File
@@ -149,7 +149,6 @@ class BackTester:
strategy (Strategy): Strategy class
params (dict): A dictionary of parameters for the strategy
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]
-1
View File
@@ -157,7 +157,6 @@ class Bot:
strategy (Strategy): Strategy class
params (dict): A dictionary of parameters for the strategy
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]
+47 -10
View File
@@ -5,6 +5,7 @@ from typing import Type, Self, Iterable
from logging import getLogger
from pandas import DataFrame, Series
import pandas as pd
import pandas_ta as ta
from ..core.constants import TimeFrame
@@ -27,8 +28,7 @@ class Candle:
spread (float): Spread
Index (int): Custom attribute representing the position of the candle in a sequence.
"""
time: float
time: int
open: float
high: float
low: float
@@ -47,8 +47,8 @@ class Candle:
"""
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")
self.time = kwargs.pop("time", time.monotonic_ns())
self.Index = kwargs.pop("Index", 0)
self.time = kwargs.pop("time", int(time.time()))
self.Index = kwargs.pop("Index", self.time)
self.real_volume = kwargs.pop("real_volume", 0)
self.spread = kwargs.pop("spread", 0)
self.tick_volume = kwargs.pop("tick_volume", 0)
@@ -188,7 +188,9 @@ class Candles:
else:
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
def __repr__(self):
@@ -198,13 +200,12 @@ class Candles:
return len(self._data.index)
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:
if isinstance(index, slice):
cls = self.__class__
data = self._data.iloc[index]
data.reset_index(drop=True, inplace=True)
return cls(data=data)
elif isinstance(index, str):
@@ -213,8 +214,8 @@ class Candles:
return self._data[index]
elif isinstance(index, int):
index_ = index if index >= 0 else len(self) + index
return self.Candle(**self._data.iloc[index], Index=index_)
candle = self._data.iloc[index]
return self.Candle(**candle, Index=int(candle.time))
raise TypeError(f"Expected int, slice or str got {type(index)}")
def __setitem__(self, index, value: Series):
@@ -236,7 +237,7 @@ class Candles:
@property
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))
@property
@@ -278,3 +279,39 @@ class Candles:
"""
res = self._data.rename(columns=kwargs, inplace=inplace)
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)
async def create_strategy_task(self, strategy: Strategy):
task = asyncio.create_task(strategy.run_strategy())
self.tasks.append(task)
await task
# async def create_strategy_task(self, strategy: Strategy):
# task = asyncio.create_task(strategy.run_strategy())
# self.tasks.append(task)
# await task
def run_strategy(self, strategy: Strategy):
"""Wraps the coroutine trade method of each strategy with 'asyncio.run'.
@@ -71,7 +71,7 @@ class Executor:
Args:
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):
task = asyncio.create_task(coroutine)
-1
View File
@@ -66,7 +66,6 @@ class TestCandles:
candle = candles[10]
assert isinstance(candle, Candle)
assert candle in candles
assert candle.Index == 10
def test_slice(self, candles):
sliced = candles[10:15]