mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-07-27 20:27:43 +00:00
v4.0.7
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user