From 2a55f49f783ff0ef3f641d206dd96a7351dea98b Mon Sep 17 00:00:00 2001 From: Ichinga Samuel Date: Wed, 13 Nov 2024 04:29:37 +0100 Subject: [PATCH] v4 --- docs/TOC.md | 4 +- docs/contrib/candle_patterns/fractals.md | 22 + docs/contrib/utils/tracker.md | Bin 760 -> 760 bytes docs/core/backtesting/backtest_account.md | 58 + docs/core/backtesting/backtest_controller.md | 125 ++ docs/core/backtesting/backtest_engine.md | 1102 +++++++++++++++++ docs/core/backtesting/get_data.md | 185 +++ docs/core/backtesting/trades_manager.md | 447 +++++++ docs/core/base.md | 80 +- docs/core/meta_trader.md | 565 +++++---- docs/core/task_queue.md | 154 ++- pyproject.toml | 4 +- .../contrib/candle_patterns/fractals.py | 2 + .../core/backtesting/backtest_engine.py | 363 +++++- src/aiomql/core/backtesting/get_data.py | 91 +- src/aiomql/core/backtesting/trades_manager.py | 176 ++- src/aiomql/core/base.py | 23 +- src/aiomql/core/config.py | 53 +- src/aiomql/core/meta_backtester.py | 6 +- src/aiomql/core/meta_trader.py | 4 +- src/aiomql/core/task_queue.py | 61 +- src/aiomql/lib/candle.py | 4 +- 22 files changed, 3088 insertions(+), 441 deletions(-) create mode 100644 docs/contrib/candle_patterns/fractals.md create mode 100644 docs/core/backtesting/backtest_account.md create mode 100644 docs/core/backtesting/backtest_controller.md create mode 100644 docs/core/backtesting/backtest_engine.md create mode 100644 docs/core/backtesting/get_data.md create mode 100644 docs/core/backtesting/trades_manager.md diff --git a/docs/TOC.md b/docs/TOC.md index 2d7503b..0198f20 100644 --- a/docs/TOC.md +++ b/docs/TOC.md @@ -1,6 +1,6 @@ # Table of Contents -- [MetaTrader](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/core/meta_trader.md) -- [Config](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/core/config.md) +- [MetaTrader](core/meta_trader.md) +- [Config](core/config.md) - [Base](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/core/base.md) - [Constants](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/core/constants.md) - [TaskQueue](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/core/task_queue.md) diff --git a/docs/contrib/candle_patterns/fractals.md b/docs/contrib/candle_patterns/fractals.md new file mode 100644 index 0000000..4c52e92 --- /dev/null +++ b/docs/contrib/candle_patterns/fractals.md @@ -0,0 +1,22 @@ +# Fractals + +## Table of Contents +- [fractals](#fractals) +- [find_bearish_fractal](#fractals.find_bearish_fractal) +- [find_bullish_fractal](#fractals.find_bullish_fractal) + + + +### find_bearish_fractal +```python +def find_bearish_fractal(candles: Candles) -> Candle | None +``` +Given a candles object, find the most recent bearish fractal. + + + +### find_bullish_fractal +```python +def find_bullish_fractal(candles: Candles) -> Candle | None +``` +Given a candles object, find the most recent bullish fractal. diff --git a/docs/contrib/utils/tracker.md b/docs/contrib/utils/tracker.md index 84ed7fcbb8e9df7ecab4222cfb7b0735309de147..5d024bfb103c872d91c88e49b5d3651d184c039d 100644 GIT binary patch delta 18 acmeyt`h#`D6voLj7_%mRQkty5bOQiTK?n!{ delta 22 ecmeyt`h#`D6h>wZ2F=Nn8M7yTlbfu-bOQikWC&OQ diff --git a/docs/core/backtesting/backtest_account.md b/docs/core/backtesting/backtest_account.md new file mode 100644 index 0000000..758b142 --- /dev/null +++ b/docs/core/backtesting/backtest_account.md @@ -0,0 +1,58 @@ +# Table of Contents + +* [backtest\_account](#backtest_account) + * [BackTestAccount](#backtest_account.BackTestAccount) + * [get\_dict](#backtest_account.BackTestAccount.get_dict) + * [asdict](#backtest_account.BackTestAccount.asdict) + * [set\_attrs](#backtest_account.BackTestAccount.set_attrs) + + + +# backtest\_account + + + +## BackTestAccount Objects + +```python +@dataclass +class BackTestAccount() +``` + +Account data for backtesting + + + +#### get\_dict + +```python +def get_dict(exclude: set = None, include: set = None) +``` + +Returns a dictionary of the account data. Using the exclude and include arguments, you can filter the data + +**Arguments**: + +- `exclude` _set_ - A set of keys to exclude +- `include` _set_ - A set of keys to include + + + +#### asdict + +```python +def asdict() +``` + +Returns a dictionary of the account data + + + +#### set\_attrs + +```python +def set_attrs(**kwargs) +``` + +Se the attributes of the account data to the instance + diff --git a/docs/core/backtesting/backtest_controller.md b/docs/core/backtesting/backtest_controller.md new file mode 100644 index 0000000..fdefb28 --- /dev/null +++ b/docs/core/backtesting/backtest_controller.md @@ -0,0 +1,125 @@ +# Table of Contents + +* [backtest\_controller](#backtest_controller) + * [BackTestController](#backtest_controller.BackTestController) + * [backtest\_engine](#backtest_controller.BackTestController.backtest_engine) + * [add\_tasks](#backtest_controller.BackTestController.add_tasks) + * [set\_parties](#backtest_controller.BackTestController.set_parties) + * [parties](#backtest_controller.BackTestController.parties) + * [control](#backtest_controller.BackTestController.control) + * [stop\_backtesting](#backtest_controller.BackTestController.stop_backtesting) + * [wait](#backtest_controller.BackTestController.wait) + * [abort](#backtest_controller.BackTestController.abort) + + + +# backtest\_controller + + + +## BackTestController Objects + +```python +class BackTestController() +``` + +The controller for the backtesting engine. +It also act's as a synchronizier for running multiple strategies (tasks) using a threading.Barrier primitive. +It handles the updating of open positions and close them when necessary. +It handles the iterator for the backtesting engine and handles it movement in time by moving it to the next time step. + +**Attributes**: + +- `_instance` _Self_ - The instance of the controller +- `config` _Config_ - The configuration for the backtesting engine +- `tasks` _list[Task]_ - The tasks that are being run +- `barrier` _Barrier_ - The barrier for synchronizing the tasks + + + +#### backtest\_engine + +```python +@property +def backtest_engine() +``` + +Returns the backtest engine + + + +#### add\_tasks + +```python +def add_tasks(*tasks: Task) +``` + +Adds tasks to the tasks list + + + +#### set\_parties + +```python +def set_parties(*, parties: int) +``` + +Sets the number of parties for the barrier. The barrier will wait for the number of parties to reach the barrier. +This has to be done here as it can be impossible to know the eventual number of parties to set the barrier to during initialization. + +**Arguments**: + +- `parties` _int_ - The number of parties to set the barrier to + + + +#### parties + +```python +@property +def parties() +``` + +Returns the number of parties for the barrier + + + +#### control + +```python +async def control() +``` + +The backtest controller. It controls the backtesting engine and the tasks that are being run. +It acts as a synchronizer for the tasks and the backtesting engine. + + + +#### stop\_backtesting + +```python +def stop_backtesting() +``` + +Stop the backtester, and shutdown the executor + + + +#### wait + +```python +def wait() +``` + +Called by individual tasks to indicate completion of their cycle + + + +#### abort + +```python +def abort() +``` + +Aborts the barrier + diff --git a/docs/core/backtesting/backtest_engine.md b/docs/core/backtesting/backtest_engine.md new file mode 100644 index 0000000..632c5b5 --- /dev/null +++ b/docs/core/backtesting/backtest_engine.md @@ -0,0 +1,1102 @@ +# Table of Contents + +* [backtest\_engine](#backtest_engine) + * [BackTestEngine](#backtest_engine.BackTestEngine) + * [\_\_init\_\_](#backtest_engine.BackTestEngine.__init__) + * [setup\_test\_range](#backtest_engine.BackTestEngine.setup_test_range) + * [setup\_data](#backtest_engine.BackTestEngine.setup_data) + * [next](#backtest_engine.BackTestEngine.next) + * [data](#backtest_engine.BackTestEngine.data) + * [reset](#backtest_engine.BackTestEngine.reset) + * [go\_to](#backtest_engine.BackTestEngine.go_to) + * [fast\_forward](#backtest_engine.BackTestEngine.fast_forward) + * [tracker](#backtest_engine.BackTestEngine.tracker) + * [save\_result\_to\_json](#backtest_engine.BackTestEngine.save_result_to_json) + * [close\_all\_open](#backtest_engine.BackTestEngine.close_all_open) + * [wrap\_up](#backtest_engine.BackTestEngine.wrap_up) + * [preload\_ticks](#backtest_engine.BackTestEngine.preload_ticks) + * [get\_price\_tick](#backtest_engine.BackTestEngine.get_price_tick) + * [check\_order](#backtest_engine.BackTestEngine.check_order) + * [check\_account](#backtest_engine.BackTestEngine.check_account) + * [check\_position](#backtest_engine.BackTestEngine.check_position) + * [close\_position\_manually](#backtest_engine.BackTestEngine.close_position_manually) + * [close\_position](#backtest_engine.BackTestEngine.close_position) + * [modify\_stops](#backtest_engine.BackTestEngine.modify_stops) + * [update\_account](#backtest_engine.BackTestEngine.update_account) + * [deposit](#backtest_engine.BackTestEngine.deposit) + * [withdraw](#backtest_engine.BackTestEngine.withdraw) + * [setup\_account](#backtest_engine.BackTestEngine.setup_account) + * [setup\_account\_sync](#backtest_engine.BackTestEngine.setup_account_sync) + * [prices](#backtest_engine.BackTestEngine.prices) + * [ticks](#backtest_engine.BackTestEngine.ticks) + * [rates](#backtest_engine.BackTestEngine.rates) + * [symbols](#backtest_engine.BackTestEngine.symbols) + * [order\_send](#backtest_engine.BackTestEngine.order_send) + * [order\_check](#backtest_engine.BackTestEngine.order_check) + * [get\_terminal\_info](#backtest_engine.BackTestEngine.get_terminal_info) + * [get\_version](#backtest_engine.BackTestEngine.get_version) + * [get\_symbols\_total](#backtest_engine.BackTestEngine.get_symbols_total) + * [get\_symbols](#backtest_engine.BackTestEngine.get_symbols) + * [get\_account\_info](#backtest_engine.BackTestEngine.get_account_info) + * [get\_symbol\_info\_tick](#backtest_engine.BackTestEngine.get_symbol_info_tick) + * [get\_symbol\_info](#backtest_engine.BackTestEngine.get_symbol_info) + * [get\_rates\_from](#backtest_engine.BackTestEngine.get_rates_from) + * [get\_rates\_from\_pos](#backtest_engine.BackTestEngine.get_rates_from_pos) + * [get\_rates\_range](#backtest_engine.BackTestEngine.get_rates_range) + * [get\_ticks\_from](#backtest_engine.BackTestEngine.get_ticks_from) + * [get\_ticks\_range](#backtest_engine.BackTestEngine.get_ticks_range) + * [order\_calc\_margin](#backtest_engine.BackTestEngine.order_calc_margin) + * [order\_calc\_profit](#backtest_engine.BackTestEngine.order_calc_profit) + * [get\_orders\_total](#backtest_engine.BackTestEngine.get_orders_total) + * [get\_orders](#backtest_engine.BackTestEngine.get_orders) + * [get\_positions\_total](#backtest_engine.BackTestEngine.get_positions_total) + * [get\_positions](#backtest_engine.BackTestEngine.get_positions) + * [get\_history\_orders\_total](#backtest_engine.BackTestEngine.get_history_orders_total) + * [get\_history\_orders](#backtest_engine.BackTestEngine.get_history_orders) + * [get\_history\_deals\_total](#backtest_engine.BackTestEngine.get_history_deals_total) + * [get\_history\_deals](#backtest_engine.BackTestEngine.get_history_deals) + + + +# backtest\_engine + + + +## BackTestEngine Objects + +```python +class BackTestEngine() +``` + + + +#### \_\_init\_\_ + +```python +def __init__(*, + data: BackTestData = None, + speed: int = 60, + start: float | datetime = 0, + end: float | datetime = 0, + restart: bool = True, + use_terminal: bool = None, + name: str = "", + stop_time: float | datetime = None, + close_open_positions_on_exit: bool = True, + preload=True, + assign_to_config: bool = True, + account_info: dict = None) +``` + +The BackTestEngine class is used to simulate trading strategies on historical data. +It can accept already saved data or create new data for backtesting on the fly. Ideally only one instance of +this class should be created per session. By default it is automatically assigned to the global config instance +during instantiation, replacing any existing backtest engine instance. But this is a configurable behaviour. +The start and end time can still be specified even when test data is provided. In that case it will be used +to set the range of the backtest. + +**Arguments**: + +- `data` _BackTestData, optional_ - The data to use for backtesting. Defaults to None. + +- `speed` _int, optional_ - The speed of the backtest. Defaults to 60 seconds. + +- `start` _float | datetime, optional_ - The start time of the backtest. Defaults to 0. If a float is passed, + it is assumed to be a timestamp. + +- `end` _float | datetime, optional_ - The end time of the backtest. Defaults to 0. If a float is passed, + it is assumed to be a timestamp. + +- `restart` _bool, optional_ - Whether to restart the backtest from the beginning. Defaults to True. + This is useful when resuming a backtest using a saved BackTestData instance. + +- `use_terminal` _bool, optional_ - Whether to use the terminal for backtesting. Defaults to None. If None, + it uses the global config setting. If use terminal is true, the backtest engine will use the terminal to + get price data, compute margins, profit and check order viability. If false, it will use the data + provided in the BackTestData instance and default algorithm for the calculations + +- `name` _str, optional_ - The name of the backtest. Defaults to "". If not provided, + it is generated from the start and end times. + +- `stop_time` _float | datetime, optional_ - The time to stop the backtest. Defaults to None. + If a float is passed, it is assumed to be a timestamp. If not given it is assumed to be the end of the backtest range. + +- `close_open_positions_on_exit` _bool, optional_ - Whether to close all open positions when the backtest + is stopped. Defaults to True. + +- `preload` _bool, optional_ - Whether to preload the ticks for the backtest. Defaults to True. + +- `assign_to_config` _bool, optional_ - Whether to assign the backtest engine to the global config instance. + Defaults to True. + +- `account_info` _dict, optional_ - A dictionary of account information to use for the backtest. Defaults to None. Use this to set + the account information for the backtest. + + +**Attributes**: + +- `_data` _BackTestData_ - The data used for backtesting. This is the data that is saved to disk when the + backtest is stopped. + +- `mt5` _MetaTrader_ - The MetaTrader instance for the backtest engine. + +- `config` _Config_ - The global configuration instance. + +- `name` _str_ - The name of the backtest. + +- `stop_testing` _bool_ - Whether to stop the backtest. + +- `use_terminal` _bool_ - Whether to use the terminal for backtesting. + +- `close_open_positions_on_exit` _bool_ - Whether to close all open positions when the backtest is stopped. + +- `stop_time` _int_ - The time to stop the backtest. + +- `preload` _bool_ - Whether to preload the ticks for the backtest. + +- `preloaded_ticks` _dict_ - A dictionary of preloaded ticks for the backtest. + +- `account_lock` _RLock_ - A reentrant lock for the account data. + +- `account_info` _dict_ - A dictionary of account information for the backtest. + + + +#### setup\_test\_range + +```python +def setup_test_range(*, + start: float | datetime = None, + end: float | datetime = None, + speed: int = 60, + restart: bool = True) +``` + +Setup the test range for the backtest engine. This is used to set the range of the backtest and the speed +at which it runs. + +**Arguments**: + +- `start` _float | datetime, optional_ - The start time of the backtest. Defaults to None. If a float is passed, + it is assumed to be a timestamp. + +- `end` _float | datetime, optional_ - The end time of the backtest. Defaults to None. If a float is passed, + it is assumed to be a timestamp. + +- `speed` _int, optional_ - The speed of the backtest. Defaults to 60. + +- `restart` _bool, optional_ - Whether to restart the backtest. Defaults to True. + This is useful when resuming a backtest using a saved BackTestData. + + + +#### setup\_data + +```python +def setup_data(*, restart: bool = True) +``` + +Sets up the data for the backtest engine. This includes the orders, positions, deals and account +information. This data is handled by specialized classes such as the BackTestAccount and the TradeManager +classes. + +**Arguments**: + +- `restart` _bool, optional_ - Whether to restart the data. Defaults to True. + + + +#### next + +```python +def next() -> Cursor +``` + +Move the cursor to the next time step in the backtest range. + + + +#### data + +```python +@property +def data() +``` + +The BackTestData instance used for the backtest. If not provided, a new instance is created, +and the data is made persistent when the backtest is stopped. + + + +#### reset + +```python +def reset(clear_data: bool = False) +``` + +Reset the backtest engine. This is useful when restarting the backtest from the beginning. + + + +#### go\_to + +```python +def go_to(*, time: datetime | float) +``` + +Move the cursor to a specific time in the backtest range. You can pass a datetime object or a timestamp. +You can't go back in time or beyond the limits of the range. + + + +#### fast\_forward + +```python +def fast_forward(*, steps: int) +``` + +Fast-forward the backtester by the given steps. + + + +#### tracker + +```python +async def tracker() +``` + +The tracker monitors and updates open positions on every iteration. It is called by the controller. + + + +#### save\_result\_to\_json + +```python +@error_handler_sync +def save_result_to_json() +``` + +Saves the result to a json file at the end of testing. + + + +#### close\_all\_open + +```python +async def close_all_open() +``` + +Closes all open position at the end of testing + + + +#### wrap\_up + +```python +@error_handler +async def wrap_up() +``` + +Wraps up the backtest. This is called at the end of testing to save the results and close all open +positions. + + + +#### preload\_ticks + +```python +async def preload_ticks(*, symbol: str) +``` + +Pull a month data on ticks from the terminal. Starting from the current time. + +**Arguments**: + +- `symbol` _str_ - The symbol to preload ticks for. + + + +#### get\_price\_tick + +```python +@async_cache +async def get_price_tick(*, symbol: str, time: int) -> Tick | None +``` + +Get the price tick for a symbol at a given time. If the preload option is set to True, +it will use the preloaded ticks when available. + +**Arguments**: + +- `symbol` _str_ - The symbol to get the price tick for. +- `time` _int_ - The time to get the price tick. + + + +#### check\_order + +```python +@error_handler +async def check_order(*, ticket: int) +``` + +" +Check if the order has reached its take profit or stop loss levels and close the order if it has. +Checks only **OrderType.BUY** and **OrderType.SELL** orders that have reached their take profit or stop loss levels. + +**Arguments**: + +- `ticket` _int_ - Order ticket + + + +#### check\_account + +```python +def check_account() +``` + +Checks an account status. This method is called at each iteration to check if the account has burned out. + + + +#### check\_position + +```python +async def check_position(*, ticket: int) +``` + +Update the profit of an open position based on the current price of the symbol. It is called by the +tracker to update the profit of open positions. + +**Arguments**: + +- `ticket` _int_ - Position ticket + + + +#### close\_position\_manually + +```python +@error_handler_sync +async def close_position_manually(*, ticket: int) +``` + +Close a position manually without. Usually at the end of testing. + + + +#### close\_position + +```python +async def close_position(*, ticket: int) -> bool +``` + +Close an open position for the trading account using the position ticket. + +**Arguments**: + +- `ticket` - Position ticket + + +**Returns**: + +- `bool` - True if the position is closed successfully, False otherwise + + + +#### modify\_stops + +```python +@error_handler(response=False) +def modify_stops(*, ticket: int, sl: int, tp: int) -> bool +``` + +Modify the stop loss and take profit levels of an open position. + +**Arguments**: + +- `ticket` _int_ - Position ticket +- `sl` _int_ - stop loss level +- `tp` _int_ - Take profit level + + +**Returns**: + +- `bool` - True if the stops are modified successfully, False otherwise + + + +#### update\_account + +```python +def update_account(*, + profit: float = None, + margin: float = 0, + gain: float = 0) +``` + +Update the account. This method is protected by thread lock. + +**Arguments**: + +- `profit` _float_ - The current profit of one or more open positions. Can be positive or negative. +- `margin` _float_ - The margin set aside for a trade. It is released when the trade is closed. +- `gain` _gain_ - The gain realized when the trade is closed. + + + +#### deposit + +```python +def deposit(*, amount: float) +``` + +Make deposit to the trading account + + + +#### withdraw + +```python +def withdraw(*, amount: float) +``` + +Make a withdrawal from the trading account. You can not withdraw more than what you have + + + +#### setup\_account + +```python +@error_handler +async def setup_account(**kwargs) +``` + +Setup the trading account before the begining of a backtesting session. + +**Arguments**: + + (**kwargs, Any): Attributes for the backetest account object can be set here. + + + +#### setup\_account\_sync + +```python +@error_handler_sync +def setup_account_sync(**kwargs) +``` + +Set up the backtesting account in sync mode + + + +#### prices + +```python +@cached_property +def prices() -> dict[str, DataFrame] +``` + +Get the prices for instruments used in the backtesting. This class is called when the use_terminal option +is set to False and trading data is provided in the data attribute. It makes sure that there is a price for each +symbol for every second covered in the backtesting range, by reindexing the price ticks using the backtesting +time span and filling up missing data using the nearest method. +This method returns a dictionaries of dataframe containing the prices for each symbol. +It's cached and there computed only once per backtesting session. + +**Returns**: + + dict[str, DataFrame]: A dictionary mapping dataframe of prices to symbols. + + + +#### ticks + +```python +@cached_property +def ticks() -> dict[str, DataFrame] +``` + +Similar to prices above, but returns prices exactly as they are without reindexing and filling up. + +**Returns**: + + dict[str, DataFrame]: A dictionary mapping symbols to dataframes of ticks. + + + +#### rates + +```python +@cached_property +def rates() -> dict[str, dict[int, DataFrame]] +``` + +This property is useful when backtesting with the use_terminal option set to false. It returns a nested dict +that maps symbols to a dict mapping timeframes to rates. The timeframes are mapped using their integer values. + +**Returns**: + + dict[str, dict[int, DataFrame]]: A dictionary containing the symbol rates. + + + +#### symbols + +```python +@cached_property +def symbols() -> dict[str, SymbolInfo] +``` + +A dictionary of symbols and SymbolInfo object. Used when use_terminal is set to false. + +**Returns**: + + dict[str, SymbolInfo] + + + +#### order\_send + +```python +@error_handler +async def order_send(*, request: dict, use_terminal=False) -> OrderSendResult +``` + +Simulates the sending of an order to the broker. An OrderSendResult is object is created at the end of this +operation as would be created if it was done in live trading. When an order is successful a positions object is +created, an order and deal object is created as well. When use_terminal is set to true the margin and profit +are calculated by sending to the broker. This increases accuracy but slows down the backtester. Check order is +called to make sure the order is valid and would go through if it was a live trade. + +**Arguments**: + +- `request` _dict_ - The order request as a dict. +- `use_terminal` _bool_ - A flag to override the use_terminal attribute. If true, the terminal will + be used even if the use_terminal attribute is True. + + +**Returns**: + +- `OrderSendResult` - An object containing the result of the order send operation. + + + +#### order\_check + +```python +@error_handler +async def order_check(*, + request: dict, + use_terminal: bool = False) -> OrderCheckResult +``` + +Checks the order before placing it. If use_terminal, the order is checked with the broker, but the entire result +is not used. Details such as balance, profit, equity, margin, and margin level are calculated by the backtester. + +**Arguments**: + +- `request` _dict_ - The order request as a dict. +- `use_terminal` _bool_ - A flag to override the use_terminal attribute. If true, the terminal will used. + + +**Returns**: + +- `OrderCheckResult` - The result of the order check. + + + +#### get\_terminal\_info + +```python +@error_handler +async def get_terminal_info() -> TerminalInfo +``` + +Get the terminal information + +**Returns**: + +- `TerminalInfo` - The terminal information + + + +#### get\_version + +```python +@error_handler +async def get_version() -> tuple[int, int, str] +``` + +Get the version of the terminal. + +**Returns**: + + tuple[int, int, str]: The version of the terminal + + + +#### get\_symbols\_total + +```python +@error_handler +async def get_symbols_total() -> int +``` + +Get the total number of symbols available in the terminal. + +**Returns**: + +- `int` - The total number of symbols available. + + + +#### get\_symbols + +```python +@error_handler +async def get_symbols(*, group: str = "") -> tuple[SymbolInfo, ...] +``` + +Get the symbols available in the terminal. Filter by group if provided. + +**Arguments**: + +- `group` _str_ - The group to filter by (default is "") + + +**Returns**: + + tuple[SymbolInfo, ...]: A tuple of symbol information + + + +#### get\_account\_info + +```python +@error_handler_sync +def get_account_info() -> AccountInfo +``` + +Get the account information + +**Returns**: + +- `AccountInfo` - The account information + + + +#### get\_symbol\_info\_tick + +```python +@error_handler +async def get_symbol_info_tick(*, symbol: str) -> Tick +``` + +Get the price tick for a symbol at the current time + +**Arguments**: + +- `symbol` _str_ - The symbol + + +**Returns**: + +- `Tick` - The price tick + + + +#### get\_symbol\_info + +```python +@error_handler +async def get_symbol_info(*, symbol: str) -> SymbolInfo +``` + +Get the symbol information + +**Arguments**: + +- `symbol` _str_ - The symbol to get information for + + +**Returns**: + +- `SymbolInfo` - The symbol information + + + +#### get\_rates\_from + +```python +@error_handler +async def get_rates_from(*, symbol: str, timeframe: TimeFrame, + date_from: datetime | float, + count: int) -> np.ndarray +``` + +Get rates from a specific date to the current date. Used by the backtester to get rates for a symbol + +**Arguments**: + +- `symbol` _str_ - The symbol to get rates for +- `timeframe` _TimeFrame_ - The timeframe of the rates +- `date_from` _datetime | float_ - The date from which to get the rates +- `count` _int_ - The number of rates to get + + +**Returns**: + +- `np.ndarray` - An array of rates + + + +#### get\_rates\_from\_pos + +```python +@error_handler +async def get_rates_from_pos(*, symbol: str, timeframe: TimeFrame, + start_pos: int, count: int) -> np.ndarray +``` + +Get a number of rates counting from a specific position. With position zero being the current time. + +**Arguments**: + +- `symbol` _str_ - The symbol to get rates for +- `timeframe` _TimeFrame_ - The timeframe of the rates +- `start_pos` _int_ - The position to start from +- `count` _int_ - The number of rates to get + + +**Returns**: + +- `np.ndarray` - An array of rates + + + +#### get\_rates\_range + +```python +@error_handler +async def get_rates_range(*, symbol: str, timeframe: TimeFrame, + date_from: datetime | float, + date_to: datetime | float) -> np.ndarray +``` + +Get rates within a specific date range. Used by the backtester to get rates for a symbol + +**Arguments**: + +- `symbol` _str_ - The symbol to get rates for +- `timeframe` _TimeFrame_ - The timeframe of the rates +- `date_from` _datetime | float_ - The date from which to get the rates +- `date_to` _datetime | float_ - The date to which to get the rates + + +**Returns**: + +- `np.ndarray` - An array of rates + + + +#### get\_ticks\_from + +```python +@error_handler +async def get_ticks_from(*, + symbol: str, + date_from: datetime | float, + count: int, + flags: CopyTicks = CopyTicks.ALL) -> np.ndarray +``` + +Get a specified number of ticks counting from a specific date. + +**Arguments**: + +- `symbol` _str_ - The symbol to get ticks for +- `date_from` _datetime | float_ - The date from which to get the ticks +- `count` _int_ - The number of ticks to get +- `flags` _CopyTicks_ - The flags to use when getting the ticks + + +**Returns**: + +- `np.ndarray` - An array of ticks + + + +#### get\_ticks\_range + +```python +@error_handler +async def get_ticks_range(*, + symbol: str, + date_from: datetime | float, + date_to: datetime | float, + flags: CopyTicks = CopyTicks.ALL) -> np.ndarray +``` + +Get ticks within a specific date range. + +**Arguments**: + +- `symbol` _str_ - The symbol to get ticks for +- `date_from` _datetime | float_ - The date from which to get the ticks +- `date_to` _datetime | float_ - The date to which to get the ticks +- `flags` _CopyTicks_ - The flags to use when getting the ticks + + +**Returns**: + +- `np.ndarray` - An array of ticks + + + +#### order\_calc\_margin + +```python +@error_handler +async def order_calc_margin(*, + action: Literal[OrderType.BUY, OrderType.SELL], + symbol: str, + volume: float, + price: float, + use_terminal: bool = None) +``` + +Calculate the margin required for a trade. + +**Arguments**: + +- `action` _Literal[OrderType.BUY, OrderType.SELL]_ - Type of order +- `symbol` _str_ - Symbol name +- `volume` _float_ - Volume of the trade +- `price` _float_ - The price at which the trade is opened +- `use_terminal` _bool_ - A flag to override the use_terminal attribute. If true, the terminal will be used + even if the use_terminal attribute is True. + + +**Returns**: + +- `float` - The margin required for the trade + + + +#### order\_calc\_profit + +```python +@error_handler +async def order_calc_profit(*, + action: Literal[OrderType.BUY, OrderType.SELL], + symbol: str, + volume: float, + price_open: float, + price_close: float, + use_terminal=None) +``` + +Calculate the profit for a trade. + +**Arguments**: + +- `action` _Literal[OrderType.BUY, OrderType.SELL]_ - Type of order +- `symbol` _str_ - Symbol name +- `volume` _float_ - Volume of the trade +- `price_open` _float_ - The price at which the trade is opened +- `price_close` _float_ - The price at which the trade is closed +- `use_terminal` _bool_ - A flag to override the use_terminal attribute. If true, the terminal will be used + even if the use_terminal attribute is True. + + +**Returns**: + +- `float` - The profit of the trade + + + +#### get\_orders\_total + +```python +@error_handler_sync +def get_orders_total() -> int +``` + +Get the total number of pending orders. + +**Returns**: + +- `int` - Total number of pending orders + + + +#### get\_orders + +```python +@error_handler_sync +def get_orders(*, + symbol: str = "", + group: str = "", + ticket: int = None) -> tuple[TradeOrder, ...] +``` + +Get pending orders from the terminal history. This has to do with pending orders, which this backtester +doesn't support yet. + +**Arguments**: + +- `symbol` - Symbol name +- `group` - Group name +- `ticket` - Order ticket + + +**Returns**: + + tuple[TradeOrder, ...]: Pending orders + + + +#### get\_positions\_total + +```python +@error_handler_sync +def get_positions_total() -> int +``` + +Get the total number of open positions. + +**Returns**: + +- `int` - Total number of open positions + + + +#### get\_positions + +```python +@error_handler_sync +def get_positions(*, + symbol: str = None, + group: str = None, + ticket: int = None) -> tuple[TradePosition, ...] +``` + +Get open positions from the terminal history. + +**Arguments**: + +- `symbol` - The symbol name +- `group` - Group argument to filter by +- `ticket` - Position ticket + + +**Returns**: + + tuple[TradePosition, ...]: Open positions + + + +#### get\_history\_orders\_total + +```python +@error_handler_sync +def get_history_orders_total(*, date_from: datetime | float, + date_to: datetime | float) -> int +``` + +Get the total number of orders in the terminal history. + +**Arguments**: + +- `date_from` - The start date of the history + +- `date_to` - The end date of the history + + +**Returns**: + +- `int` - Total number of orders in the history + + + +#### get\_history\_orders + +```python +@error_handler_sync +def get_history_orders(*, + date_from: datetime | float = None, + date_to: datetime | float = None, + group: str = "", + ticket: int = None, + position: int = None) -> tuple[TradeOrder, ...] +``` + +Get orders from the terminal history. + +**Arguments**: + +- `date_from` - Date from which to start the history +- `date_to` - Date to which to end the history +- `group` - group keyword to filter by +- `ticket` - ticket id to filter by +- `position` - position id to filter by + + +**Returns**: + + tuple[TradeOrder, ...]: Orders in the history + + + +#### get\_history\_deals\_total + +```python +@error_handler_sync +def get_history_deals_total(*, date_from: datetime | float, + date_to: datetime | float) -> int +``` + +Get the total number of deals in the terminal history. + +**Arguments**: + +- `date_from` - Date from which to start the history +- `date_to` - Date to which to end the history + + +**Returns**: + +- `int` - Total number of deals in the history + + + +#### get\_history\_deals + +```python +@error_handler_sync +def get_history_deals(*, + date_from: datetime | float = None, + date_to: datetime | float = None, + group: str = None, + position: int = None, + ticket: int = None) -> tuple[TradeDeal, ...] +``` + +Get deals from the terminal history. + +**Arguments**: + +- `date_from` - Date from which to start the history +- `date_to` - Date to which to end the history +- `group` - group keyword to filter by +- `position` - position id to filter by +- `ticket` - ticket id to filter by + + +**Returns**: + + tuple[TradeDeal, ...]: Deals in the history + diff --git a/docs/core/backtesting/get_data.md b/docs/core/backtesting/get_data.md new file mode 100644 index 0000000..11f6d3b --- /dev/null +++ b/docs/core/backtesting/get_data.md @@ -0,0 +1,185 @@ +# Table of Contents + +* [get\_data](#get_data) + * [Cursor](#get_data.Cursor) + * [BackTestData](#get_data.BackTestData) + * [set\_attrs](#get_data.BackTestData.set_attrs) + * [fields](#get_data.BackTestData.fields) + * [GetData](#get_data.GetData) + * [\_\_init\_\_](#get_data.GetData.__init__) + * [pickle\_data](#get_data.GetData.pickle_data) + * [load\_data](#get_data.GetData.load_data) + * [save\_data](#get_data.GetData.save_data) + * [get\_data](#get_data.GetData.get_data) + + + +# get\_data + + + +## Cursor Objects + +```python +class Cursor(NamedTuple) +``` + +A cursor to iterate over the data. Marks the current position. + + + +## BackTestData Objects + +```python +@dataclass +class BackTestData() +``` + +The data class to store the backtesting data. + +**Attributes**: + +- `name` _str_ - The name of the backtest data. +- `terminal` _dict_ - The terminal information. +- `version` _tuple_ - The version of the terminal. +- `account` _dict_ - The account information. +- `symbols` _dict_ - The symbols information. +- `ticks` _dict_ - The ticks data. +- `rates` _dict_ - The rates data. +- `span` _range_ - The range of the data. +- `range` _range_ - The range of the data. +- `orders` _dict_ - The orders data. +- `deals` _dict_ - The deals data. +- `positions` _dict_ - The positions data. +- `open_positions` _set_ - The open positions. +- `cursor` _Cursor_ - The cursor to iterate over the data. +- `margins` _dict_ - The margins data. +- `fully_loaded` _bool_ - A flag to indicate if the data is fully loaded + + + +#### set\_attrs + +```python +def set_attrs(**kwargs) +``` + +Set the attributes of the class on the instance. + + + +#### fields + +```python +@property +def fields() +``` + +A list of the fields of the class. + + + +## GetData Objects + +```python +class GetData() +``` + +A class to get the backtesting data from the MetaTrader5 terminal. + +**Attributes**: + +- `start` _datetime_ - The start date of the data. +- `end` _datetime_ - The end date of the data. +- `symbols` _Sequence[str]_ - The symbols to get the data for. +- `timeframes` _Sequence[TimeFrame]_ - The timeframes to get the data for. +- `name` _str_ - The name of the backtest data. +- `range` _range_ - The range of the data. +- `span` _range_ - The span of the data. +- `data` _BackTestData_ - The backtesting data. +- `mt5` _MetaTrader_ - The MetaTrader5 instance. +- `task_queue` _TaskQueue_ - The task queue to handle the requests. + + + +#### \_\_init\_\_ + +```python +def __init__(*, + start: datetime, + end: datetime, + symbols: Sequence[str], + timeframes: Sequence[TimeFrame], + name: str = "") +``` + +Get the backtesting data from the MetaTrader5 terminal. + +**Arguments**: + +- `start` _datetime_ - The start date of the data. +- `end` _datetime_ - The end date of the data. +- `symbols` _Sequence[str]_ - The symbols to get the data for. +- `timeframes` _Sequence[TimeFrame]_ - The timeframes to get the data for. +- `name` _str_ - The name of the backtest data. + + + +#### pickle\_data + +```python +@classmethod +def pickle_data(cls, *, data: BackTestData, name: str | Path) +``` + +Pickle the data to a file. + +**Arguments**: + +- `data` _BackTestData_ - The data to pickle. +- `name` _str | Path_ - The name of the file to pickle the data to. + + + +#### load\_data + +```python +@classmethod +def load_data(cls, *, name: str | Path) -> BackTestData +``` + +Load the data from a file. + +**Arguments**: + +- `name` _str | Path_ - The name of the file to load the data from. + + + +#### save\_data + +```python +def save_data(*, name: str | Path = "") +``` + +Save the data to a file. + +**Arguments**: + +- `name` _str | Path_ - The name of the file to save the data to. If not provided, the name of the data is used. + + + +#### get\_data + +```python +async def get_data(workers: int = None) +``` + +Use the task queue to get the data from the MetaTrader5 terminal. + +**Arguments**: + +- `workers` _int_ - The number of workers to use in the task queue. If not provided, the default number of workers + is used. + diff --git a/docs/core/backtesting/trades_manager.md b/docs/core/backtesting/trades_manager.md new file mode 100644 index 0000000..46b8e1f --- /dev/null +++ b/docs/core/backtesting/trades_manager.md @@ -0,0 +1,447 @@ +# Table of Contents + +* [trades\_manager](#trades_manager) + * [TradeManager](#trades_manager.TradeManager) + * [update](#trades_manager.TradeManager.update) + * [values](#trades_manager.TradeManager.values) + * [keys](#trades_manager.TradeManager.keys) + * [items](#trades_manager.TradeManager.items) + * [to\_dict](#trades_manager.TradeManager.to_dict) + * [PositionsManager](#trades_manager.PositionsManager) + * [\_\_init\_\_](#trades_manager.PositionsManager.__init__) + * [margin](#trades_manager.PositionsManager.margin) + * [close](#trades_manager.PositionsManager.close) + * [get\_margin](#trades_manager.PositionsManager.get_margin) + * [delete\_margin](#trades_manager.PositionsManager.delete_margin) + * [set\_margin](#trades_manager.PositionsManager.set_margin) + * [positions\_get](#trades_manager.PositionsManager.positions_get) + * [positions\_total](#trades_manager.PositionsManager.positions_total) + * [open\_positions](#trades_manager.PositionsManager.open_positions) + * [OrdersManager](#trades_manager.OrdersManager) + * [get\_orders\_range](#trades_manager.OrdersManager.get_orders_range) + * [history\_orders\_get](#trades_manager.OrdersManager.history_orders_get) + * [history\_orders\_total](#trades_manager.OrdersManager.history_orders_total) + * [DealsManager](#trades_manager.DealsManager) + * [get\_deals\_range](#trades_manager.DealsManager.get_deals_range) + * [history\_deals\_get](#trades_manager.DealsManager.history_deals_get) + * [history\_deals\_total](#trades_manager.DealsManager.history_deals_total) + + + +# trades\_manager + + + +## TradeManager Objects + +```python +class TradeManager(Generic[TradeData]) +``` + +A generic class to manage trades data during a backtest. It is the parent class of the +PositionsManager, OrdersManager, and DealsManager. It implements some dict-like methods to manage the data. +It has a private attribute _data to store the data. It exposes the data through the values, keys, and items methods. +It also has a to_dict method to convert the data to a dictionary. + +**Attributes**: + +- `_data` _dict[int, TradeData]_ - The data to store the trades. + + +**Examples**: + + >>> manager = TradeManager() + >>> manager[123456] = TradePosition(ticket=123456, symbol="EURUSD", volume=0.1) + >>> manager.update(ticket=123456, symbol="EURUSD", volume=0.1) + >>> manager[123456] + TradePosition(ticket=123456, symbol='EURUSD', volume=0.1) + >>> manager.values() + (TradePosition(ticket=123456, symbol='EURUSD', volume=0.1),) + >>> manager.keys() + (123456,) + >>> manager.items() + ((123456, TradePosition(ticket=123456, symbol='EURUSD', volume=0.1)),) + >>> manager.to_dict() +- `{123456` - {'ticket': 123456, 'symbol': 'EURUSD', 'volume': 0.1}} + >>> pos = manager.get(123456) + >>> pos + TradePosition(ticket=123456, symbol='EURUSD', volume=0.1) + >>> pos in manager + True + >>> len(manager) + 1 + >>> pos in manager + False + + + +#### update + +```python +def update(*, ticket: int, **kwargs) +``` + +Update the data of a trade. Given the ticket of the trade and the new data to update. + +**Arguments**: + +- `ticket` _int_ - The ticket of the trade to update. +- `**kwargs` - The new data to update. + + + +#### values + +```python +def values() -> tuple[TradeData, ...] +``` + +Returns the values of the data. + + + +#### keys + +```python +def keys() -> tuple[int, ...] +``` + +Returns the keys of the data. + + + +#### items + +```python +def items() -> tuple[tuple[int, TradeData], ...] +``` + +Returns the items of the data. + + + +#### to\_dict + +```python +def to_dict() +``` + +Convert the data to a dictionary. + + + +## PositionsManager Objects + +```python +class PositionsManager(TradeManager) +``` + +A class to manage the open positions during a backtest. It is a subclass of TradeManager. It has an additional +attribute _open_positions to store the open positions. It also has a margins attribute to store the margins of the +open positions. It overrides some methods of the TradeManager class to manage the open positions. + +**Attributes**: + +- `_open_positions` _set[int]_ - The open positions. +- `margins` _dict[int, float]_ - The margins of the open positions. + + + +#### \_\_init\_\_ + +```python +def __init__(*, + data: dict = None, + open_positions: set[int] = None, + margins: dict = None) +``` + +Positions manager manages the open positions during a backtest. It is a subclass of TradeManager. It has an +additional attribute _open_positions to store the open positions. It also has a margins attribute to store the +margins of the open positions. It overrides some methods of the TradeManager class to manage the open positions. + +**Arguments**: + +- `data` _dict, optional_ - The data to store the trades. This used for continuation of the backtesting, if it + was stopped with some open positions. + +- `open_positions` _set, optional_ - The open positions. Defaults to None. + +- `margins` _dict, optional_ - The margins of the open positions. Defaults to None. + + + +#### margin + +```python +@property +def margin() +``` + +Returns the total margin of all open positions + + + +#### close + +```python +def close(*, ticket: int) -> bool +``` + +Close a position. Given the ticket of the position to close. + +**Arguments**: + +- `ticket` _int_ - The ticket of the position to close. + + + +#### get\_margin + +```python +def get_margin(*, ticket: int) -> float +``` + +Get the margin of a position. Given the ticket of the position. + +**Arguments**: + +- `ticket` _int_ - The ticket of the position. + + +**Returns**: + +- `float` - The margin of the position. + + + +#### delete\_margin + +```python +def delete_margin(*, ticket: int) +``` + +Delete the margin of a position. Given the ticket of the position. + +**Arguments**: + +- `ticket` _int_ - The ticket of the position. + + + +#### set\_margin + +```python +def set_margin(*, ticket: int, margin: float) +``` + +Set the margin of a position. Given the ticket of the position and the margin. + +**Arguments**: + +- `ticket` _int_ - The ticket of the position. +- `margin` _float_ - The margin of the position + + + +#### positions\_get + +```python +def positions_get(*, + ticket: int = None, + symbol: str = None, + group: None = None) -> tuple[TradePosition, ...] +``` + +Get positions. Given the ticket, symbol, or group of the positions. + +**Arguments**: + +- `ticket` _int_ - The ticket of the position. +- `symbol` _str_ - The symbol of the position. +- `group` _str_ - The group + + +**Returns**: + + tuple[TradePosition, ...]: The positions + + + +#### positions\_total + +```python +def positions_total() -> int +``` + +Get the total number of open positions. + +**Returns**: + +- `int` - The total number of open positions. + + + +#### open\_positions + +```python +@property +def open_positions() -> tuple[TradePosition, ...] +``` + +Returns the open positions. + +**Arguments**: + + tuple[TradePosition, ...]: The open positions. + + + +## OrdersManager Objects + +```python +class OrdersManager(TradeManager) +``` + +Managers orders data during a backtest. It is a subclass of TradeManager. It manages access to the historical +orders data + + + +#### get\_orders\_range + +```python +def get_orders_range(*, date_from: float, + date_to: float) -> tuple[TradeData, ...] +``` + +Get orders within a date range. Given the start and end date of the range. + +**Arguments**: + +- `date_from` _float_ - The start date of the range. +- `date_to` _float_ - The end date of the range. + + +**Returns**: + + tuple[TradeData, ...]: The orders within the date range. + + + +#### history\_orders\_get + +```python +def history_orders_get(*, + date_from: float | datetime = None, + date_to: float | datetime = None, + group: str = "", + ticket: int = None, + position: int = None) -> tuple[TradeOrder, ...] +``` + +Get historical orders. Given the start and end date of the range, the group, ticket, or position of the +orders. + +**Arguments**: + +- `date_from` _float, datetime_ - The start date of the range. +- `date_to` _float, datetime_ - The end date of the range. +- `group` _str_ - The group of the orders. +- `ticket` _int_ - The ticket of the order. +- `position` _int_ - The position of the order. + + +**Returns**: + + tuple[TradeOrder, ...]: The historical orders. + + + +#### history\_orders\_total + +```python +def history_orders_total(*, date_from: datetime | float, + date_to: datetime | float) -> int +``` + +Get the total number of historical orders. Given the start and end date of the range. + +**Arguments**: + +- `date_from` _datetime, float_ - The start date of the range. +- `date_to` _datetime, float_ - The end date of the range. + + + +## DealsManager Objects + +```python +class DealsManager(TradeManager) +``` + + + +#### get\_deals\_range + +```python +def get_deals_range(*, date_from: float, + date_to: float) -> tuple[TradeData, ...] +``` + +Get deals within a date range. Given the start and end date of the range. + +**Arguments**: + +- `date_from` _float_ - The start date of the range. +- `date_to` _float_ - The end date of the range. + + +**Returns**: + + tuple[TradeData, ...]: The deals within the date range. + + + +#### history\_deals\_get + +```python +def history_deals_get(*, + date_from: float | datetime = None, + date_to: float | datetime = None, + group: str = "", + ticket: int = None, + position: int = None) -> tuple[TradeDeal, ...] +``` + +History deals get. Given the start and end date of the range, the group, ticket, or position of the deals. + +**Arguments**: + +- `date_from` _float, datetime_ - The start date of the range. +- `date_to` _float, datetime_ - The end date of the range. +- `group` _str_ - The group of the deals. +- `ticket` _int_ - The ticket of the deal. +- `position` _int_ - The position of the deal. + + + +#### history\_deals\_total + +```python +def history_deals_total(*, date_from: datetime | float, + date_to: datetime | float) -> int +``` + +Get the total number of historical deals. Given the start and end date of the range + +**Arguments**: + +- `date_from` _datetime, float_ - The start date of the range. +- `date_to` _datetime, float_ - The end date of the range. + + +**Returns**: + +- `int` - The total number of historical deals. + diff --git a/docs/core/base.md b/docs/core/base.md index 7f6e34c..afca628 100644 --- a/docs/core/base.md +++ b/docs/core/base.md @@ -1,14 +1,17 @@ -# Base Class +# Base ## Table of Contents -- [Base](#base) -- [set\_attributes](#base.set_attributes) -- [annotations](#base.annotations) -- [get\_dict](#base.get_dict) -- [class\_vars](#base.class_vars) -- [dict](#base.dict) +- [Base](#base.base) + - [set_attributes](#base.set_attributes) + - [annotations](#base.annotations) + - [get_dict](#base.get_dict) + - [class_vars](#base.class_vars) + - [dict](#base.dict) - +- [_Base](#_base._base) + + + ### Base ```python class Base @@ -16,11 +19,12 @@ class Base A base class for all data model classes in the aiomql package. This class provides a set of common methods and attributes for all data model classes. -#### Class Attributes -| Name | Type | Description | Default | -|----------|--------------|-------------------------------------|---------| -| `mt5` | `MetaTrader` | An instance of the MetaTrader class | | -| `config` | `Config` | An instance of the Config class | | +#### Attributes: +| Name | Type | Description | +|-----------|-------|------------------------------------------------------------------------------------------------------| +| `exclude` | `set` | A set of attributes to be excluded when retrieving attributes using the *get_dict* and *dict* method | +| `include` | `set` | A set of attributes to be included when retrieving attributes using the *get_dict* and *dict* method | + ### __init__ @@ -32,25 +36,28 @@ def __init__(**kwargs) |----------|-------|---------------------------------------------------| | `kwargs` | `Any` | Object attributes and values as keyword arguments | + ### set_attributes ```python def set_attributes(**kwargs) ``` Set keyword arguments as object attributes. Only sets attributes that have been annotated on the class body. -#### Parameters + +#### Parameters: | Name | Type | Description | |----------|-------|---------------------------------------------------| | `kwargs` | `Any` | Object attributes and values as keyword arguments | -#### Raises +#### Raises: | Exception | Description | |------------------|-----------------------------------------------------------------------------------| | `AttributeError` | When assigning an attribute that does not belong to the class or any parent class | -#### Notes +#### Notes: Only sets attributes that have been annotated on the class body. + ### annotations ```python @@ -59,43 +66,49 @@ Only sets attributes that have been annotated on the class body. def annotations() -> dict ``` Class annotations from all ancestor classes and the current class. -#### Returns -| Type | Description | -|--------|-----------------------------------| -| `dict` | A dictionary of class annotations | +#### Returns: +| Type | Description | +|------------------|-----------------------------------| +| `dict[str, Any]` | A dictionary of class annotations | + -#### get\_dict +#### get_dict ```python def get_dict(exclude: set = None, include: set = None) -> dict ``` Returns class attributes as a dict, with the ability to filter -#### Parameters + +#### Parameters: | Name | Type | Description | |-----------|-------|------------------------------------| | `exclude` | `set` | A set of attributes to be excluded | | `include` | `set` | Specific attributes to be returned | -#### Returns + +#### Returns: | Type | Description | |--------|--------------------------------------------| | `dict` | A dictionary of specified class attributes | -#### Notes +#### Notes: You can only set either of include or exclude. If you set both, include will take precedence + -### class\_vars +### class_vars ```python @property @cache def class_vars() ``` Annotated class attributes -#### Returns + +#### Returns: | Type | Description | |--------|-------------------------------------------------------------------------------------------| | `dict` | A dictionary of available class attributes in all ancestor classes and the current class. | + ### dict ```python @@ -103,7 +116,20 @@ Annotated class attributes def dict() -> dict ``` All instance and class attributes as a dictionary, except those excluded in the Meta class. -#### Returns + +#### Returns: | Type | Description | |--------|-----------------------------------------------| | `dict` | A dictionary of instance and class attributes | + + + ### MetaTrader @@ -51,12 +53,13 @@ It provides methods for connecting to the MetaTrader terminal and retrieving dat |-------|-------|--------------------------------------------------------|------------------------| | error | Error | The last error encountered by the MetaTrader terminal. | Error(1, 'Successful') | -#### Notes +#### Notes: All the attributes, enums and constants of the MetaTrader5 class are also available here. Although, they are more easily accessible and used via the various enums and models defined in the module. - -#### \_\_aenter\_\_ + + +### \__aenter\__ ```python async def __aenter__() -> 'MetaTrader' ``` @@ -68,405 +71,478 @@ Initializes the connection to the MetaTrader terminal. |--------------|-------------------------------------| | `MetaTrader` | An instance of the MetaTrader class | - -#### \_\_aexit\_\_ + + +### \__aexit\__ ```python async def __aexit__(exc_type, exc_val, exc_tb) ``` Async context manager exit point. Closes the connection to the MetaTrader terminal. - -#### login + + +### login ```python -async def login(login: int, - password: str, - server: str, - timeout: int = 60000) -> bool +async def login(*, login: int, password: str, server: str, timeout: int = 60000) -> bool ``` Connects to the MetaTrader terminal using the specified login, password and server. -#### Parameters + +#### Parameters: | Name | Type | Description | |------------|-------|--------------------------------------------| | `login` | `int` | The trading account number. | | `password` | `str` | The trading account password. | | `server` | `str` | The trading server name. | | `timeout` | `int` | The timeout for the connection in seconds. | + #### Returns: | Type | Description | |--------|--------------------------------------| | `bool` | True if successful, False otherwise. | - -#### initialize + + +#### login_sync ```python -async def initialize(path: str = "", - login: int = 0, - password: str = "", - server: str = "", - timeout: int | None = None, - portable=False) -> bool +async def login_sync(*, login: int, password: str, server: str, timeout: int = 60000) -> bool ``` -Initializes the connection to the MetaTrader terminal. All parameters are optional. -#### Parameters -| Name | Type | Description | -|------------|-----------------|----------------------------------------------------------| -| `path` | `str` | The path to the MetaTrader terminal executable. | -| `login` | `int` | The trading account number. | -| `password` | `str` | The trading account password. | -| `server` | `str` | The trading server name. | -| `timeout` | `int` or `None` | The timeout for the connection in seconds. | -| `portable` | `bool` | If True, the terminal will be launched in portable mode. | -#### Returns +A synchronous version of the login method. +Connects to the MetaTrader terminal using the specified login, password and server. + +#### Parameters: +| Name | Type | Description | +|------------|-------|--------------------------------------------| +| `login` | `int` | The trading account number. | +| `password` | `str` | The trading account password. | +| `server` | `str` | The trading server name. | +| `timeout` | `int` | The timeout for the connection in seconds. | + +#### Returns: | Type | Description | |--------|--------------------------------------| | `bool` | True if successful, False otherwise. | - -#### shutdown + + +### initialize +```python +async def initialize(path: str = "", login: int = 0, password: str = "", server: str = "", + timeout: int | None = None, portable=False) -> bool +``` +Initializes the connection to the MetaTrader terminal. All parameters are optional. + +#### Parameters: +| Name | Type | Description | +|------------|---------------|----------------------------------------------------------| +| `path` | `str` | The path to the MetaTrader terminal executable. | +| `login` | `int` | The trading account number. | +| `password` | `str` | The trading account password. | +| `server` | `str` | The trading server name. | +| `timeout` | `int \| None` | The timeout for the connection in seconds. | +| `portable` | `bool` | If True, the terminal will be launched in portable mode. | + +#### Returns: +| Type | Description | +|--------|--------------------------------------| +| `bool` | True if successful, False otherwise. | + + + +### initialize_sync +```python +async def initialize_sync(path: str = "", login: int = 0, password: str = "", server: str = "", + timeout: int | None = None, portable=False) -> bool +``` +Initializes the connection to the MetaTrader terminal. All parameters are optional. + +#### Parameters: +| Name | Type | Description | +|------------|---------------|----------------------------------------------------------| +| `path` | `str` | The path to the MetaTrader terminal executable. | +| `login` | `int` | The trading account number. | +| `password` | `str` | The trading account password. | +| `server` | `str` | The trading server name. | +| `timeout` | `int \| None` | The timeout for the connection in seconds. | +| `portable` | `bool` | If True, the terminal will be launched in portable mode. | + +#### Returns: +| Type | Description | +|--------|--------------------------------------| +| `bool` | True if successful, False otherwise. | + + + +### shutdown ```python async def shutdown() -> None ``` Closes the connection to the MetaTrader terminal. - -#### version + + +### version ```python async def version() -> tuple[int, int, str] | None ``` Returns the version of the MetaTrader terminal. -#### Returns + +#### Returns: | Type | Description | |------------------------|-----------------------------------------------------------------------------------------------| | `tuple[int, int, str]` | A tuple of the MetaTrader terminal version. `Terminal Version`, `Build`, `Build Release Date` | - -#### account\_info + + +### account_info ```python async def account_info() -> AccountInfo | None ``` Returns the account information for the connected account. -#### Returns + +#### Returns: | Type | Description | |---------------|--------------------------------------| | `AccountInfo` | An instance of the AccountInfo class | - -#### terminal\_info + + +### terminal_info ```python async def terminal_info() -> TerminalInfo | None ``` + Returns the terminal information for the connected terminal. -#### Returns +### Returns | Type | Description | |----------------|------------------------------------------------| | `TerminalInfo` | An instance of the TerminalInfo class. A tuple | - -#### last\_error + + +### last_error ```python async def last_error() -> tuple[int, str] ``` Returns the last error code and description. -#### Returns + +#### Returns: | Type | Description | |-------------------|-------------------------------------------------| | `tuple[int, str]` | A tuple of the last error code and description. | - -#### symbols\_total + + +### symbols_total ```python async def symbols_total() -> int ``` Returns the total number of symbols. -#### Returns + +#### Returns: | Type | Description | |-------|------------------------------| | `int` | The total number of symbols. | - -#### symbols\_get + + +### symbols_get ```python async def symbols_get(group: str = "") -> tuple[SymbolInfo] | None ``` Returns the symbol information for all symbols or for a specified group. -#### Parameters + +#### Parameters: | Name | Type | Description | |---------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------| | `group` | `str` | The group name. Optional named parameter. If the group is specified, the function returns only symbols meeting a specified criteria for a symbol name. | -#### Returns + +#### Returns: | Type | Description | |---------------------|--------------------------------| | `tuple[SymbolInfo]` | A tuple of SymbolInfo objects. | - -#### symbol\_info + +### symbol_info ```python async def symbol_info(symbol: str) -> SymbolInfo | None ``` Returns the symbol information for the specified symbol. -#### Parameters + +#### Parameters: | Name | Type | Description | |----------|-------|------------------| | `symbol` | `str` | The symbol name. | + #### Returns: | Type | Description | |--------------|--------------------------------------| | `SymbolInfo` | An instance of the SymbolInfo class. | - -#### symbol\_info\_tick + + +### symbol_info_tick ```python async def symbol_info_tick(symbol: str) -> Tick | None ``` Returns the latest tick for the specified symbol. -#### Parameters + +#### Parameters: | Name | Type | Description | |----------|-------|------------------| | `symbol` | `str` | The symbol name. | -#### Returns + +#### Returns: | Type | Description | |--------|--------------------------------| | `Tick` | An instance of the Tick class. | - -#### symbol\_select + + +### symbol_select ```python async def symbol_select(symbol: str, enable: bool) -> bool ``` Selects or unselects the specified symbol in the Market Watch window. -#### Parameters + +#### Parameters: | Name | Type | Description | |----------|--------|--------------------------------------------------------------------------------| | `symbol` | `str` | The symbol name. | | `enable` | `bool` | If True, the symbol will be selected. If False, the symbol will be unselected. | -#### Returns + +#### Returns: | Type | Description | |--------|--------------------------------------| | `bool` | True if successful, False otherwise. | - -#### market\_book\_add + + +### market_book_add ```python async def market_book_add(symbol: str) -> bool ``` Adds the specified symbol to the market book. -#### Parameters + +#### Parameters: | Name | Type | Description | |----------|-------|------------------| | `symbol` | `str` | The symbol name. | -#### Returns + +#### Returns: | Type | Description | |--------|--------------------------------------| | `bool` | True if successful, False otherwise. | - -#### market\_book\_get + +### market_book_get ```python async def market_book_get(symbol: str) -> tuple[BookInfo] | None ``` Returns the market depth for the specified symbol. -#### Parameters + +#### Parameters: | Name | Type | Description | |----------|-------|------------------| | `symbol` | `str` | The symbol name. | -#### Returns + +#### Returns: | Type | Description | |-------------------|------------------------------| | `tuple[BookInfo]` | A tuple of BookInfo objects. | - -#### market\_book\_release + + +### market_book_release ```python async def market_book_release(symbol: str) -> bool ``` Removes the specified symbol from the market book. -#### Parameters + +#### Parameters: | Name | Type | Description | |----------|-------|------------------| | `symbol` | `str` | The symbol name. | -#### Returns + +#### Returns: | Type | Description | |--------|--------------------------------------| | `bool` | True if successful, False otherwise. | - -#### copy\_rates\_from + +### copy_rates_from ```python -import numpy - - -async def copy_rates_from(symbol: str, - timeframe: TimeFrame, - date_from: datetime | int, +async def copy_rates_from(symbol: str, timeframe: TimeFrame, date_from: datetime | int, count: int) -> numpy.ndarray | None ``` Returns the OHLCV rates for the specified symbol and timeframe starting from the specified date. -#### Parameters + +#### Parameters: | Name | Type | Description | |-------------|---------------------|--------------------------------| | `symbol` | `str` | The symbol name. | | `timeframe` | `TimeFrame` | The timeframe. | | `date_from` | `datetime` or `int` | The date to start from. | | `count` | `int` | The number of rates to return. | -#### Returns + +#### Returns: | Type | Description | |-----------------|-------------------------------| | `numpy.ndarray` | A numpy array of OHLCV rates. | - -#### copy\_rates\_from\_pos + + +### copy_rates_from_pos ```python -async def copy_rates_from_pos(symbol: str, - timeframe: TimeFrame, - start_pos: int, - count: int) -> numpy.ndarray | None +async def copy_rates_from_pos(symbol: str, timeframe: TimeFrame, start_pos: int, count: int) -> numpy.ndarray | None ``` Returns the OHLCV rates for the specified symbol and timeframe starting from the specified position. -#### Parameters + +#### Parameters: | Name | Type | Description | |-------------|-------------|--------------------------------| | `symbol` | `str` | The symbol name. | | `timeframe` | `TimeFrame` | The timeframe. | | `start_pos` | `int` | The position to start from. | | `count` | `int` | The number of rates to return. | -#### Returns + +#### Returns: | Type | Description | |-----------------|-------------------------------| | `numpy.ndarray` | A numpy array of OHLCV rates. | - -#### copy\_rates\_range + + +### copy_rates_range ```python -async def copy_rates_range(symbol: str, - timeframe: TimeFrame, - date_from: datetime | int, +async def copy_rates_range(symbol: str, timeframe: TimeFrame, date_from: datetime | int, date_to: datetime | int) -> numpy.ndarray | None ``` Returns the OHLCV rates for the specified symbol and timeframe between the specified dates. -#### Parameters + +#### Parameters: | Name | Type | Description | |-------------|---------------------|------------------| | `symbol` | `str` | The symbol name. | | `timeframe` | `TimeFrame` | The timeframe. | | `date_from` | `datetime` or `int` | The start date. | | `date_to` | `datetime` or `int` | The end date. | + #### Returns: | Type | Description | |-----------------|-------------------------------| | `numpy.ndarray` | A numpy array of OHLCV rates. | - -#### copy\_ticks\_from + + +### copy_ticks_from ```python -async def copy_ticks_from(symbol: str, - date_from: datetime | int, - count: int, - flags: CopyTicks) -> tuple[Tick] | None +async def copy_ticks_from(symbol: str, date_from: datetime | int, count: int, flags: CopyTicks) -> tuple[Tick] | None ``` Returns the ticks for the specified symbol starting from the specified date. -#### Parameters + +#### Parameters: | Name | Type | Description | |-------------|---------------------|--------------------------------| | `symbol` | `str` | The symbol name. | | `date_from` | `datetime` or `int` | The date to start from. | | `count` | `int` | The number of ticks to return. | | `flags` | `CopyTicks` | The CopyTicks flags. | -#### Returns + +#### Returns: | Type | Description | |---------------|--------------------------| | `tuple[Tick]` | A tuple of Tick objects. | - -#### copy\_ticks\_range + + +### copy_ticks_range ```python -async def copy_ticks_range(symbol: str, - date_from: datetime | int, - date_to: datetime | int, +async def copy_ticks_range(symbol: str, date_from: datetime | int, date_to: datetime | int, flags: CopyTicks) -> tuple[Tick] | None ``` Returns the ticks for the specified symbol between the specified dates. -#### Parameters + +#### Parameters: | Name | Type | Description | |-------------|---------------------|----------------------| | `symbol` | `str` | The symbol name. | | `date_from` | `datetime` or `int` | The start date. | | `date_to` | `datetime` or `int` | The end date. | | `flags` | `CopyTicks` | The CopyTicks flags. | -#### Returns + +#### Returns: | Type | Description | |---------------|--------------------------| | `tuple[Tick]` | A tuple of Tick objects. | - -#### orders\_total + + +### orders_total ```python async def orders_total() -> int ``` Returns the total number of active orders. -#### Returns + +#### Returns: | Type | Description | |-------|------------------------------------| | `int` | The total number of active orders. | - -#### orders\_get + + +### orders_get ```python -async def orders_get(group: str = "", - ticket: int = 0, - symbol: str = "") -> tuple[TradeOrder] | None +async def orders_get(group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder, ...] | None ``` Get active orders with the ability to filter by symbol or ticket. There are three call options. Call without parameters. Return active orders on all symbols -#### Parameters + +#### Parameters: | Name | Type | Description | |----------|-------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `group` | `str` | The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only active orders meeting a specified criteria for a symbol name. | | `ticket` | `int` | Order ticket (ORDER_TICKET). Optional named parameter. | | `symbol` | `str` | Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored. | -#### Returns -| Type | Description | -|---------------------|------------------------------------------------------| -| `tuple[TradeOrder]` | A tuple of active trade orders as TradeOrder objects | -#### Returns -| Type | Description | -|---------------------|------------------------------------------------------| -| `tuple[TradeOrder]` | A tuple of active trade orders as TradeOrder objects | - -#### order\_calc\_margin +#### Returns: +| Type | Description | +|--------------------------|------------------------------------------------------| +| `tuple[TradeOrder, ...]` | A tuple of active trade orders as TradeOrder objects | + + + +### order_calc_margin ```python -async def order_calc_margin(action: OrderType, - symbol: str, - volume: float, - price: float) -> float | None +async def order_calc_margin(action: OrderType, symbol: str, volume: float, price: float) -> float | None ``` Calculates the margin required to open a trade. -#### Parameters + +#### Parameters: | Name | Type | Description | |----------|-------------|-------------------| | `action` | `OrderType` | The order type. | | `symbol` | `str` | The symbol name. | | `volume` | `float` | The order volume. | | `price` | `float` | The order price. | -#### Returns + +#### Returns: | Type | Description | |---------|--------------------------------------| | `float` | The margin required to open a trade. | - -#### order\_calc\_profit + + +### order_calc_profit ```python -async def order_calc_profit(action: OrderType, - symbol: str, - volume: float, - price_open: float, +async def order_calc_profit(action: OrderType, symbol: str, volume: float, price_open: float, price_close: float) -> float | None ``` Calculates the profit for a closed trade. -#### Parameters + +#### Parameters: | Name | Type | Description | |---------------|-------------|------------------------| | `action` | `OrderType` | The order type. | @@ -474,103 +550,112 @@ Calculates the profit for a closed trade. | `volume` | `float` | The order volume. | | `price_open` | `float` | The order open price. | | `price_close` | `float` | The order close price. | -#### Returns + +#### Returns: | Type | Description | |---------|--------------------------------| | `float` | The profit for a closed trade. | - -#### order\_check + + +### order_check ```python async def order_check(request: dict) -> OrderCheckResult ``` Checks the specified order for validity. -#### Parameters + +#### Parameters: | Name | Type | Description | |-----------|--------|--------------------| | `request` | `dict` | The order request. | -#### Returns + +#### Returns: | Type | Description | |--------------------|--------------------------------------------| | `OrderCheckResult` | An instance of the OrderCheckResult class. | - -#### order\_send + + +### order_send ```python async def order_send(request: dict) -> OrderSendResult ``` Sends the specified order request to the MetaTrader terminal. -#### Parameters + +#### Parameters: | Name | Type | Description | |-----------|--------|--------------------| | `request` | `dict` | The order request. | -#### Returns + +#### Returns: | Type | Description | |-------------------|-------------------------------------------| | `OrderSendResult` | An instance of the OrderSendResult class. | - -#### positions\_total + + +### positions_total ```python async def positions_total() -> int ``` Returns the total number of open positions. -#### Returns + +#### Returns: | Type | Description | |-------|-------------------------------------| | `int` | The total number of open positions. | - -#### positions\_get + +### positions_get ```python -async def positions_get(group: str = "", - ticket: int = 0, - symbol: str = "") -> tuple[TradePosition] | None +async def positions_get(group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradePosition, ...] | None ``` Returns the open positions with the ability to filter by symbol or ticket. There are three call options. Call without parameters. Return open positions on all symbols -#### Parameters + +#### Parameters: | Name | Type | Description | |----------|-------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `group` | `str` | The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only open positions meeting a specified criteria for a symbol name. | | `ticket` | `int` | Position ticket (POSITION_TICKET). Optional named parameter. | | `symbol` | `str` | Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored. | -#### Returns -| Type | Description | -|------------------------|----------------------------------------------------------| -| `tuple[TradePosition]` | A tuple of open trade positions as TradePosition objects | - -#### history\_orders\_total +#### Returns: +| Type | Description | +|-----------------------------|----------------------------------------------------------| +| `tuple[TradePosition, ...]` | A tuple of open trade positions as TradePosition objects | + + + +### history_orders_total ```python -async def history_orders_total(date_from: datetime | int, - date_to: datetime | int) -> int +async def history_orders_total(date_from: datetime | int, date_to: datetime | int) -> int ``` Returns the total number of closed orders for the specified period. -#### Parameters + +#### Parameters: | Name | Type | Description | |-------------|---------------------|-----------------| | `date_from` | `datetime` or `int` | The start date. | | `date_to` | `datetime` or `int` | The end date. | -#### Returns + +#### Returns: | Type | Description | |-------|-------------------------------------------------------------| | `int` | The total number of closed orders for the specified period. | - -#### history\_orders\_get + +### history_orders_get ```python -async def history_orders_get(date_from: datetime | int = None, - date_to: datetime | int = None, - group: str = "", - ticket: int = 0, - position: int = 0) -> tuple[TradeOrder] | None +async def history_orders_get(date_from: datetime | int = None, date_to: datetime | int = None, group: str = "", + ticket: int = 0, position: int = 0) -> tuple[TradeOrder, ...] | None ``` Returns the closed orders for the specified period with the ability to filter by symbol or ticket. There are three call options. Call without parameters. Return closed orders on all symbols -#### Parameters + +#### Parameters: | Name | Type | Description | |-------------|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `date_from` | `datetime` or `int` | The start date. Optional named parameter. | @@ -578,40 +663,41 @@ Call without parameters. Return closed orders on all symbols | `group` | `str` | The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only closed orders meeting a specified criteria for a symbol name. | | `ticket` | `int` | Order ticket (ORDER_TICKET). Optional named parameter. | | `position` | `int` | Position ticket (POSITION_TICKET). Optional named parameter. | -#### Returns -| Type | Description | -|---------------------|------------------------------------------------------| -| `tuple[TradeOrder]` | A tuple of closed trade orders as TradeOrder objects | - -#### history\_deals\_total +#### Returns: +| Type | Description | +|--------------------------|------------------------------------------------------| +| `tuple[TradeOrder, ...]` | A tuple of closed trade orders as TradeOrder objects | + + + +### history_deals_total ```python -async def history_deals_total(date_from: datetime | int, - date_to: datetime | int) -> int +async def history_deals_total(date_from: datetime | int, date_to: datetime | int) -> int ``` Returns the total number of closed deals for the specified period. -#### Parameters + +#### Parameters: | Name | Type | Description | |-------------|---------------------|-----------------| | `date_from` | `datetime` or `int` | The start date. | | `date_to` | `datetime` or `int` | The end date. | -#### Returns + +#### Returns: | Type | Description | |-------|------------------------------------------------------------| | `int` | The total number of closed deals for the specified period. | - -#### history\_deals\_get + +### history_deals_get ```python -async def history_deals_get(date_from: datetime | int = None, - date_to: datetime | int = None, - group: str = "", - ticket: int = 0, - position: int = 0) -> tuple[TradeDeal] | None +async def history_deals_get(date_from: datetime | int = None, date_to: datetime | int = None, group: str = "", + ticket: int = 0,position: int = 0) -> tuple[TradeDeal, ...] | None ``` Returns the closed deals for the specified period with the ability to filter by symbol or ticket. There are three call options. Call without parameters. Return closed deals on all symbols -#### Parameters + +#### Parameters: | Name | Type | Description | |-------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `date_from` | `datetime` or `int` | The start date. Optional named parameter. | @@ -619,7 +705,8 @@ Call without parameters. Return closed deals on all symbols | `group` | `str` | The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only closed deals meeting a specified criteria for a symbol name. | | `ticket` | `int` | Order ticket (ORDER_TICKET). Optional named parameter. | | `position` | `int` | Position ticket (POSITION_TICKET). Optional named parameter. | -#### Returns -| Type | Description | -|--------------------|----------------------------------------------------| -| `tuple[TradeDeal]` | A tuple of closed trade deals as TradeDeal objects | + +#### Returns: +| Type | Description | +|-------------------------|----------------------------------------------------| +| `tuple[TradeDeal, ...]` | A tuple of closed trade deals as TradeDeal objects | diff --git a/docs/core/task_queue.md b/docs/core/task_queue.md index 91e8323..9240be6 100644 --- a/docs/core/task_queue.md +++ b/docs/core/task_queue.md @@ -1,69 +1,126 @@ # TaskQueue and QueueItem ## Table of Contents -- [QueueItem](#queue_item) - - [run](#run) +- [QueueItem](#queue_item.queue_item) + - [\__init\__](#queue_item.__init__) + - [run](#queue_item.run) -- [TaskQueue](#task_queue) - - [TaskQueue.add](#task_queue.add) - - [TaskQueue.add_task](#task_queue.add_task) - - [TaskQueue.worker](#task_queue.worker) - - [TaskQueue.start](#task_queue.start) +- [TaskQueue](#task_queue.task_queue) + - [\__init\__](#task_queue.__init__) + - [add](#task_queue.add) + - [add_task](#task_queue.add_task) + - [worker](#task_queue.worker) + - [run](#task_queue.run) + - [stop_queue](#task_queue.stop_queue) + - [clean_up](#task_queue.clean_up) + - [cancel](#task_queue.cancel) - + ### QueueItem ```python -class QueueItem: - def __init__(self, task: Callable | Awaitable, *args, **kwargs): +class QueueItem ``` -A task to be executed by the `TaskQueue`. The task can be a callable or an awaitable. The task is wrapped as a +A task to be executed by the `TaskQueue`. The task can be any coroutine callable. The task is wrapped as a `QueueItem` object, which is then added to the `TaskQueue` for execution. The arguments and keyword arguments are -passed to the task when it is executed. All parameters are created as attributes of the `QueueItem` object. +passed to the task when it is executed. + +#### Attributes: +| Name | Type | Description | +|-----------------|---------------------------|-----------------------------------------------------------------------| +| `task_item` | `Callable` \| `Coroutine` | A coroutine function to be executed by the `TaskQueue` | +| `args` | `tuple[Any, ...]` | Positional arguments to be passed to the task_item | +| `kwargs` | `dict[str, Any]` | Keyword arguments to be passed to the task_item | +| `must_complete` | `bool` | If True, the item must be completed even if the queue is stopped. | +| `time` | `float` | The time the item was added to the queue. For sorting priority queues | + + + +### \__init\__ +```python +def __init__(self, task: Callable | Coroutine, *args, **kwargs): +``` #### Parameters: | Name | Type | Description | |----------|---------------------------|-------------------------------------------------------------------| -| `task` | `Callable` \| `Awaitable` | A callable or awaitable task to be executed by the `TaskQueue` | +| `task` | `Callable` \| `Coroutine` | A coroutine to be executed by the `TaskQueue` | | `args` | `Any` | Positional arguments to be passed to the task when it is executed | | `kwargs` | `Any` | Keyword arguments to be passed to the task when it is executed | - + + ### run ```python -def run(self) -> Any +def run(self) ``` Run the task. If the task is a coroutine, it is awaited. If the task is a callable, it is called. + ### TaskQueue ```python -class TaskQueue: - def __init__(self): +class TaskQueue ``` +A perpetual task queue that processes `QueueItem` objects. The `TaskQueue` runs indefinitely, processing `QueueItem` +objects as they are added to the queue. The `TaskQueue` is a wrapper around an `asyncio.Queue` that can be passed in as +an argument or defaults to an `asyncio.PriorityQueue`. It is added to the bot executor of the `Bot` class on a +separate thread. + #### Attributes: -| Name | Type | Description | -|---------------|-----------------|---------------------------------------------------------------------------------| -| `queue` | `asyncio.Queue` | An asyncio.Queue queue of `QueueItem` objects to be executed by the `TaskQueue` | +| Name | Type | Description | +|------------------|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `queue` | `asyncio.Queue` | An `asyncio.Queue` queue of `QueueItem` objects to be executed by the `TaskQueue`. If not provided during instantiation, an `asyncio.PriorityQueue` is used | +| `stop` | `bool` | A flag to stop the task_queue instance. | +| `workers` | `int` | The number of workers to process the queue items. Defaults to 10. | +| `timeout` | `int` | The maximum time to wait for the queue to complete. Default is None. If timeout is provided the queue is joined using `asyncio.wait_for` with the timeout | +| `on_exit` | `Literal["cancel", "complete_priority"]` | The action to take when the queue is stopped. If "cancel" the queue is cancelled and the remaining items are not processed. If "complete_priority" the queue is completed with the priority items. Default is "cancel" | +| `mode` | `Literal["finite", "infinite"]` | The mode of the queue. If `finite` the queue will stop when all tasks are completed. If `infinite` the queue will continue to run until stopped. | +| `worker_timeout` | `int` | The time to wait for a task to be added to the queue before stopping the worker or adding a dummy sleep task to the queue. | +| `tasks` | `List[Task]` | A list of the worker tasks running concurrently, including the main task that joins the queue. | +| `priority_tasks` | `set[QueueItem]` | A set to store the `QueueItems` that must complete before the queue stops | + + + +### \__init\__ +```python +def __init__(self, queue: asyncio.Queue = None, workers: int = 10, timeout: int = None, size: int = None, + on_exit: Literal["cancel", "complete_priority"] = "cancel", + mode: Literal["finite", "infinite"] = "infinite", worker_timeout: int = 60) +``` +Create a new `TaskQueue` instance. + +#### Parameters: +| Name | Type | Description | Default | +|------------------|------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|---------------------| +| `queue` | `asyncio.Queue` | An `asyncio.Queue` queue instance | None | +| `workers` | `int` | The number of workers to process the queue items. | 10 | +| `timeout` | `int` | The maximum time to wait for the queue to complete. | None | +| `size` | `int` | The maximum size of the queue. | None | +| `on_exit` | `Literal["cancel", "complete_priority"]` | The action to take when the queue is stopped. | "complete_priority" | +| `mode` | `Literal["finite", "infinite"]` | The mode of the queue. | "infinite" | +| `worker_timeout` | `int` | The time to wait for a task to be added to the queue before stopping the worker or adding a dummy sleep task to the queue. | 60 | ### add ```python -def add(self, item: QueueItem, *args, **kwargs) -> None +def add(*, item: QueueItem, priority: int = 3, must_complete_false: bool = False) -> None ``` Add a `QueueItem` to the `TaskQueue` queue. #### Parameters: -| Name | Type | Description | -|--------|-------------|----------------------------------------| -| `item` | `QueueItem` | A `QueueItem` to be added to the queue | +| Name | Type | Description | +|-----------------------|-------------|----------------------------------------------------------------------------------------| +| `item` | `QueueItem` | A `QueueItem` to be added to the queue | +| `priority` | `int` | The priority of the item. The lower the number, the higher the priority. Default is 3. | +| `must_complete_false` | `bool` | If True, the item must be completed even if the queue is stopped. Default is False. | ### add_task ```python -def add_task(self, task: Callable | Awaitable, *args, **kwargs) -> None +def add_task(self, task: Callable | Awaitable, *args, **kwargs) ``` Create a QueueItem from the task and add it to the `TaskQueue` queue. The task can be a callable or an awaitable. The arguments and keyword arguments are passed to the QueueItem. @@ -78,14 +135,47 @@ The arguments and keyword arguments are passed to the QueueItem. ### worker ```python -async def worker(self) -> None +async def worker() ``` -A worker that processes the `QueueItem` objects in the `TaskQueue` queue. The worker runs indefinitely, processing -`QueueItem` objects as they are added to the queue. +A worker that processes the `QueueItem` objects in the `TaskQueue` queue. - -### start + + +### run ```python -def start(self) -> None +async def run(timeout: int = None) ``` -Start the worker that processes the `QueueItem` objects in the `TaskQueue` queue. +Start the `TaskQueue` instance. If a timeout is provided, the queue is joined using `asyncio.wait_for` with the timeout. +This is the main entry point for the `TaskQueue` instance. It is added to the bot executor of the `Bot` class on a +separate thread. + +#### Parameters: +| Name | Type | Description | +|-----------|-------|----------------------------------------------------------------------| +| `timeout` | `int` | The maximum time to wait for the queue to complete. Default is None. | + + + +### stop_queue +```python +def stop_queue() +``` +Stop the `TaskQueue` instance. This sets the `stop` attribute to True, changes the `on_exit` attribute to "cancel", +and cancels the queue. + + + +### clean_up +```python +async def clean_up() +``` +Clean up the `TaskQueue` instance. This is called when the queue is stopped. It cancels the queue and processes the +remaining priority items based on the `on_exit` attribute. + + + +### cancel +```python +def cancel() +``` +Cancel all remaining tasks. diff --git a/pyproject.toml b/pyproject.toml index 8e114f9..2a3c7d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "aiomql" -version = "3.23" +version = "4.0.0" readme = "README.md" requires-python = ">=3.11" classifiers = [ @@ -16,7 +16,7 @@ classifiers = [ "Operating System :: OS Independent", ] keywords = ['MetaTrader5', 'Asynchronous', 'Algorithmic Trading', 'Trading Bot'] -dependencies = ["MetaTrader5>=5.0.37", "pandas>=1.5.0", "pandas-ta>=0.3.14b0", "matplotlib>=3.8.4", "mplfinance>=0.12.10b0"] +dependencies = ["MetaTrader5>=5.0.37", "pandas>=1.5.0", "pandas-ta>=0.3.14b0"] authors = [{name = "Ichinga Samuel", email = "ichingasamuel@gmail.com"}] description = "Asynchronous MetaTrader5 library and Algorithmic Trading Framework" diff --git a/src/aiomql/contrib/candle_patterns/fractals.py b/src/aiomql/contrib/candle_patterns/fractals.py index 013c3d4..373591a 100644 --- a/src/aiomql/contrib/candle_patterns/fractals.py +++ b/src/aiomql/contrib/candle_patterns/fractals.py @@ -2,12 +2,14 @@ from ...lib.candle import Candle, Candles def find_bearish_fractal(candles: Candles) -> Candle | None: + """Given a candles object, find the most recent bearish fractal.""" for i in range(len(candles) - 3, 1, -1): if candles[i].high > max(candles[i - 1].high, candles[i + 1].high, candles[i - 2].high, candles[i + 2].high): return candles[i] def find_bullish_fractal(candles: Candles) -> Candle | None: + """Given a candles object, find the most recent bullish fractal.""" for i in range(len(candles) - 3, 1, -1): if candles[i].low < min(candles[i - 1].low, candles[i + 1].low, candles[i - 2].low, candles[i + 2].low): return candles[i] diff --git a/src/aiomql/core/backtesting/backtest_engine.py b/src/aiomql/core/backtesting/backtest_engine.py index 6ee47ff..c4ad453 100644 --- a/src/aiomql/core/backtesting/backtest_engine.py +++ b/src/aiomql/core/backtesting/backtest_engine.py @@ -36,6 +36,7 @@ from .trades_manager import PositionsManager, OrdersManager, DealsManager logger = getLogger(__name__) +# noinspection PyUnresolvedReferences class BackTestEngine: mt5: MetaTrader span: range @@ -77,43 +78,51 @@ class BackTestEngine: account_info: dict = None, ): """The BackTestEngine class is used to simulate trading strategies on historical data. - It can accept already saved data or create new data for backtesting on the fly. Ideally only one instance of this class should be created per session. - By default it is automatically assigned to the global config instance during instantiation, - replacing any existing backtest engine instance. But this is a configurable behaviour. - The start and end time can still be specified even when test data is provided. In that case it will be used to set the range of the backtest. + It can accept already saved data or create new data for backtesting on the fly. Ideally only one instance of + this class should be created per session. By default it is automatically assigned to the global config instance + during instantiation, replacing any existing backtest engine instance. But this is a configurable behaviour. + The start and end time can still be specified even when test data is provided. In that case it will be used + to set the range of the backtest. Args: data (BackTestData, optional): The data to use for backtesting. Defaults to None. speed (int, optional): The speed of the backtest. Defaults to 60 seconds. - start (float | datetime, optional): The start time of the backtest. Defaults to 0. If a float is passed, it is assumed to be a timestamp. + start (float | datetime, optional): The start time of the backtest. Defaults to 0. If a float is passed, + it is assumed to be a timestamp. - end (float | datetime, optional): The end time of the backtest. Defaults to 0. If a float is passed, it is assumed to be a timestamp. + end (float | datetime, optional): The end time of the backtest. Defaults to 0. If a float is passed, + it is assumed to be a timestamp. - restart (bool, optional): Whether to restart the backtest from the beginning. Defaults to True. This is useful when resuming a backtest - using a saved BackTestData instance. + restart (bool, optional): Whether to restart the backtest from the beginning. Defaults to True. + This is useful when resuming a backtest using a saved BackTestData instance. - use_terminal (bool, optional): Whether to use the terminal for backtesting. Defaults to None. If None, it uses the global config setting. - If use terminal is true, the backtest engine will use the terminal to get price data, compute margins, profit and check order viability. - If false, it will use the data provided in the BackTestData instance and default algorithm for the calculations + use_terminal (bool, optional): Whether to use the terminal for backtesting. Defaults to None. If None, + it uses the global config setting. If use terminal is true, the backtest engine will use the terminal to + get price data, compute margins, profit and check order viability. If false, it will use the data + provided in the BackTestData instance and default algorithm for the calculations - name (str, optional): The name of the backtest. Defaults to "". If not provided, it is generated from the start and end times. + name (str, optional): The name of the backtest. Defaults to "". If not provided, + it is generated from the start and end times. - stop_time (float | datetime, optional): The time to stop the backtest. Defaults to None. If a float is passed, it is assumed to be a timestamp. - If not given it is asummed to be the end of the backtest range. + stop_time (float | datetime, optional): The time to stop the backtest. Defaults to None. + If a float is passed, it is assumed to be a timestamp. If not given it is assumed to be the end of the backtest range. - close_open_positions_on_exit (bool, optional): Whether to close all open positions when the backtest is stopped. Defaults to True. + close_open_positions_on_exit (bool, optional): Whether to close all open positions when the backtest + is stopped. Defaults to True. preload (bool, optional): Whether to preload the ticks for the backtest. Defaults to True. - assign_to_config (bool, optional): Whether to assign the backtest engine to the global config instance. Defaults to True. + assign_to_config (bool, optional): Whether to assign the backtest engine to the global config instance. + Defaults to True. account_info (dict, optional): A dictionary of account information to use for the backtest. Defaults to None. Use this to set the account information for the backtest. Attributes: - _data (BackTestData): The data used for backtesting. This is the data that is saved to disk when the backtest is stopped. + _data (BackTestData): The data used for backtesting. This is the data that is saved to disk when the + backtest is stopped. mt5 (MetaTrader): The MetaTrader instance for the backtest engine. @@ -174,14 +183,22 @@ class BackTestEngine: def __repr__(self): return f"{self.__class__.__name__}()" - def setup_test_range(self, *, start: float | datetime = None, end: float | datetime = None, speed: int = 60, restart: bool = True): - """Setup the test range for the backtest engine. This is used to set the range of the backtest and the speed at which it runs. + def setup_test_range(self, *, start: float | datetime = None, end: float | datetime = None, speed: + int = 60, restart: bool = True): + """Setup the test range for the backtest engine. This is used to set the range of the backtest and the speed + at which it runs. Args: - start (float | datetime, optional): The start time of the backtest. Defaults to None. If a float is passed, it is assumed to be a timestamp. - end (float | datetime, optional): The end time of the backtest. Defaults to None. If a float is passed, it is assumed to be a timestamp. + start (float | datetime, optional): The start time of the backtest. Defaults to None. If a float is passed, + it is assumed to be a timestamp. + + end (float | datetime, optional): The end time of the backtest. Defaults to None. If a float is passed, + it is assumed to be a timestamp. + speed (int, optional): The speed of the backtest. Defaults to 60. - restart (bool, optional): Whether to restart the backtest. Defaults to True. This is useful when resuming a backtest using a saved BackTestData. + + restart (bool, optional): Whether to restart the backtest. Defaults to True. + This is useful when resuming a backtest using a saved BackTestData. """ if self._data.span and self._data.range: start = start or self._data.span[0] @@ -206,6 +223,13 @@ class BackTestEngine: self.cursor = Cursor(index=self.range.start, time=self.span.start) def setup_data(self, *, restart: bool = True): + """Sets up the data for the backtest engine. This includes the orders, positions, deals and account + information. This data is handled by specialized classes such as the BackTestAccount and the TradeManager + classes. + + Args: + restart (bool, optional): Whether to restart the data. Defaults to True. + """ if restart is True: self.orders = OrdersManager() self.positions = PositionsManager() @@ -231,19 +255,26 @@ class BackTestEngine: self._account = BackTestAccount(**self._data.account) def next(self) -> Cursor: + """Move the cursor to the next time step in the backtest range.""" return next(self) @property def data(self): + """The BackTestData instance used for the backtest. If not provided, a new instance is created, + and the data is made persistent when the backtest is stopped.""" return self._data def reset(self, clear_data: bool = False): + """Reset the backtest engine. This is useful when restarting the backtest from the beginning.""" self.iter = zip(self.range, self.span) self.cursor = Cursor(index=self.range.start, time=self.span.start) if clear_data: self.setup_data(restart=True) def go_to(self, *, time: datetime | float): + """Move the cursor to a specific time in the backtest range. You can pass a datetime object or a timestamp. + You can't go back in time or beyond the limits of the range. + """ time = time.astimezone(tz=UTC) if isinstance(time, datetime) else datetime.fromtimestamp(time, tz=UTC) time = int(time.timestamp()) steps = time - self.cursor.time @@ -255,6 +286,7 @@ class BackTestEngine: raise ValueError("Can't go back in time or beyond the limits of the range") def fast_forward(self, *, steps: int): + """Fast-forward the backtester by the given steps.""" for _ in range(steps): self.next() @@ -263,6 +295,7 @@ class BackTestEngine: return [(c, t) for c, t in zip(df.columns, df.dtypes)] async def tracker(self): + """The tracker monitors and updates open positions on every iteration. It is called by the controller.""" try: pos_tasks = [self.check_position(ticket=ticket) for ticket in self.positions._open_positions] await asyncio.gather(*pos_tasks) @@ -274,6 +307,7 @@ class BackTestEngine: @error_handler_sync def save_result_to_json(self): + """Saves the result to a json file at the end of testing.""" data = self._account.get_dict(include={"balance", "profit", "equity", "margin", "margin_free", "margin_level"}) wins = [position for ticket in self.positions if (position := self.positions.get(ticket)).profit > 0] losses = [position for ticket in self.positions if (position := self.positions.get(ticket)).profit <= 0] @@ -303,6 +337,7 @@ class BackTestEngine: json.dump(data, file, indent=4) async def close_all_open(self): + """Closes all open position at the end of testing""" tasks = [self.check_position(ticket=position.ticket) for position in self.positions.open_positions] await asyncio.gather(*tasks) for position in self.positions.open_positions: @@ -310,6 +345,8 @@ class BackTestEngine: @error_handler async def wrap_up(self): + """Wraps up the backtest. This is called at the end of testing to save the results and close all open + positions.""" if self.close_open_positions_on_exit: await self.close_all_open() self.save_result_to_json() @@ -329,7 +366,11 @@ class BackTestEngine: GetData.pickle_data(data=self._data, name=path) async def preload_ticks(self, *, symbol: str): - """Pull a month data on ticks from the terminal. Starting from the current time""" + """Pull a month data on ticks from the terminal. Starting from the current time. + + Args: + symbol (str): The symbol to preload ticks for. + """ try: start = self.cursor.time end = start + (30 * 24 * 60 * 60) @@ -348,6 +389,13 @@ class BackTestEngine: @async_cache async def get_price_tick(self, *, symbol: str, time: int) -> Tick | None: + """Get the price tick for a symbol at a given time. If the preload option is set to True, + it will use the preloaded ticks when available. + + Args: + symbol (str): The symbol to get the price tick for. + time (int): The time to get the price tick. + """ try: if self.use_terminal and self.preload: if (ticks := self.preloaded_ticks.get(symbol)) is not None and time in ticks.index: @@ -420,6 +468,7 @@ class BackTestEngine: ... def check_account(self): + """Checks an account status. This method is called at each iteration to check if the account has burned out.""" account = self._account level = account.margin_level if account.margin_so_mode == AccountStopOutMode.PERCENT else account.margin_so_call if level < account.margin_so_call and level != 0 and account.equity < 0: @@ -428,7 +477,8 @@ class BackTestEngine: async def check_position(self, *, ticket: int): """ - Update the profit of an open position based on the current price of the symbol. + Update the profit of an open position based on the current price of the symbol. It is called by the + tracker to update the profit of open positions. Args: ticket (int): Position ticket @@ -445,6 +495,7 @@ class BackTestEngine: @error_handler_sync async def close_position_manually(self, *, ticket: int): + """Close a position manually without. Usually at the end of testing.""" res = await self.close_position(ticket=ticket) if not res: return @@ -524,9 +575,9 @@ class BackTestEngine: Modify the stop loss and take profit levels of an open position. Args: - ticket: Position ticket - sl: stop loss level - tp: Take profit level + ticket (int): Position ticket + sl (int): stop loss level + tp (int): Take profit level Returns: bool: True if the stops are modified successfully, False otherwise @@ -535,6 +586,14 @@ class BackTestEngine: return True def update_account(self, *, profit: float = None, margin: float = 0, gain: float = 0): + """ + Update the account. This method is protected by thread lock. + + Args: + profit (float): The current profit of one or more open positions. Can be positive or negative. + margin (float): The margin set aside for a trade. It is released when the trade is closed. + gain (gain): The gain realized when the trade is closed. + """ self.account_lock.acquire() try: self._account.balance += round(gain, self._account.currency_digits) @@ -561,14 +620,21 @@ class BackTestEngine: self.account_lock.release() def deposit(self, *, amount: float): + """Make deposit to the trading account""" self.update_account(gain=amount) def withdraw(self, *, amount: float): + """Make a withdrawal from the trading account. You can not withdraw more than what you have""" assert amount <= self._account.balance, "Insufficient funds" self.update_account(gain=-amount) @error_handler async def setup_account(self, **kwargs): + """Setup the trading account before the begining of a backtesting session. + + Args: + (**kwargs, Any): Attributes for the backetest account object can be set here. + """ kwargs = {**self.account_info, **kwargs} default = { "profit": self._account.profit, @@ -589,6 +655,7 @@ class BackTestEngine: @error_handler_sync def setup_account_sync(self, **kwargs): + """Set up the backtesting account in sync mode""" kwargs = {**self.account_info, **kwargs} default = { "profit": self._account.profit, @@ -609,6 +676,16 @@ class BackTestEngine: @cached_property def prices(self) -> dict[str, DataFrame]: + """Get the prices for instruments used in the backtesting. This class is called when the use_terminal option + is set to False and trading data is provided in the data attribute. It makes sure that there is a price for each + symbol for every second covered in the backtesting range, by reindexing the price ticks using the backtesting + time span and filling up missing data using the nearest method. + This method returns a dictionaries of dataframe containing the prices for each symbol. + It's cached and there computed only once per backtesting session. + + Returns: + dict[str, DataFrame]: A dictionary mapping dataframe of prices to symbols. + """ prices = {} for symbol in self._data.ticks.keys(): res = self._data.ticks[symbol] @@ -621,6 +698,11 @@ class BackTestEngine: @cached_property def ticks(self) -> dict[str, DataFrame]: + """Similar to prices above, but returns prices exactly as they are without reindexing and filling up. + + Returns: + dict[str, DataFrame]: A dictionary mapping symbols to dataframes of ticks. + """ ticks = {} for symbol in self._data.ticks.keys(): res = self._data.ticks[symbol] @@ -630,6 +712,12 @@ class BackTestEngine: @cached_property def rates(self) -> dict[str, dict[int, DataFrame]]: + """This property is useful when backtesting with the use_terminal option set to false. It returns a nested dict + that maps symbols to a dict mapping timeframes to rates. The timeframes are mapped using their integer values. + + Returns: + dict[str, dict[int, DataFrame]]: A dictionary containing the symbol rates. + """ rates = {} for symbol in self._data.rates.keys(): for timeframe in self._data.rates[symbol].keys(): @@ -640,6 +728,11 @@ class BackTestEngine: @cached_property def symbols(self) -> dict[str, SymbolInfo]: + """A dictionary of symbols and SymbolInfo object. Used when use_terminal is set to false. + + Returns: + dict[str, SymbolInfo] + """ symbols = {} for symbol, info in self._data.symbols.items(): symbols[symbol] = SymbolInfo((info.get(key) for key in SymbolInfo.__match_args__)) @@ -647,6 +740,20 @@ class BackTestEngine: @error_handler async def order_send(self, *, request: dict, use_terminal=False) -> OrderSendResult: + """Simulates the sending of an order to the broker. An OrderSendResult is object is created at the end of this + operation as would be created if it was done in live trading. When an order is successful a positions object is + created, an order and deal object is created as well. When use_terminal is set to true the margin and profit + are calculated by sending to the broker. This increases accuracy but slows down the backtester. Check order is + called to make sure the order is valid and would go through if it was a live trade. + + Args: + request (dict): The order request as a dict. + use_terminal (bool): A flag to override the use_terminal attribute. If true, the terminal will + be used even if the use_terminal attribute is True. + + Returns: + OrderSendResult: An object containing the result of the order send operation. + """ use_terminal = self.use_terminal or use_terminal osr = { "retcode": 10013, @@ -814,6 +921,16 @@ class BackTestEngine: @error_handler async def order_check(self, *, request: dict, use_terminal: bool = False) -> OrderCheckResult: + """Checks the order before placing it. If use_terminal, the order is checked with the broker, but the entire result + is not used. Details such as balance, profit, equity, margin, and margin level are calculated by the backtester. + + Args: + request (dict): The order request as a dict. + use_terminal (bool): A flag to override the use_terminal attribute. If true, the terminal will used. + + Returns: + OrderCheckResult: The result of the order check. + """ use_terminal = self.use_terminal or use_terminal ocr = { "retcode": 10013, @@ -892,6 +1009,11 @@ class BackTestEngine: @error_handler async def get_terminal_info(self) -> TerminalInfo: + """Get the terminal information + + Returns: + TerminalInfo: The terminal information + """ if self.use_terminal: res = await self.mt5.terminal_info() return res @@ -899,6 +1021,11 @@ class BackTestEngine: @error_handler async def get_version(self) -> tuple[int, int, str]: + """Get the version of the terminal. + + Returns: + tuple[int, int, str]: The version of the terminal + """ if self.use_terminal: res = await self.mt5.version() return res @@ -906,6 +1033,11 @@ class BackTestEngine: @error_handler async def get_symbols_total(self) -> int: + """Get the total number of symbols available in the terminal. + + Returns: + int: The total number of symbols available. + """ if self.use_terminal: syms = await self.mt5.symbols_total() return syms @@ -913,6 +1045,14 @@ class BackTestEngine: @error_handler async def get_symbols(self, *, group: str = "") -> tuple[SymbolInfo, ...]: + """Get the symbols available in the terminal. Filter by group if provided. + + Args: + group (str): The group to filter by (default is "") + + Returns: + tuple[SymbolInfo, ...]: A tuple of symbol information + """ if self.use_terminal: syms = await self.mt5.symbols_get(group=group) return syms @@ -920,10 +1060,23 @@ class BackTestEngine: @error_handler_sync def get_account_info(self) -> AccountInfo: + """Get the account information + + Returns: + AccountInfo: The account information + """ return AccountInfo(self._account.asdict().values()) @error_handler - async def get_symbol_info_tick(self, *, symbol: str) -> Tick | None: + async def get_symbol_info_tick(self, *, symbol: str) -> Tick: + """Get the price tick for a symbol at the current time + + Args: + symbol (str): The symbol + + Returns: + Tick: The price tick + """ tick = await self.get_price_tick(symbol=symbol, time=self.cursor.time) return tick @@ -937,6 +1090,14 @@ class BackTestEngine: @error_handler async def get_symbol_info(self, *, symbol: str) -> SymbolInfo: + """Get the symbol information + + Args: + symbol (str): The symbol to get information for + + Returns: + SymbolInfo: The symbol information + """ if self.use_terminal: info = await self._symbol_info(symbol=symbol) else: @@ -957,6 +1118,17 @@ class BackTestEngine: @error_handler async def get_rates_from(self, *, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int) -> np.ndarray: + """Get rates from a specific date to the current date. Used by the backtester to get rates for a symbol + + Args: + symbol (str): The symbol to get rates for + timeframe (TimeFrame): The timeframe of the rates + date_from (datetime | float): The date from which to get the rates + count (int): The number of rates to get + + Returns: + np.ndarray: An array of rates + """ date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from, tz=UTC) if self.use_terminal: rates = await self.mt5.copy_rates_from(symbol, timeframe, date_from, count) @@ -970,6 +1142,17 @@ class BackTestEngine: @error_handler async def get_rates_from_pos(self, *, symbol: str, timeframe: TimeFrame, start_pos: int, count: int) -> np.ndarray: + """Get a number of rates counting from a specific position. With position zero being the current time. + + Args: + symbol (str): The symbol to get rates for + timeframe (TimeFrame): The timeframe of the rates + start_pos (int): The position to start from + count (int): The number of rates to get + + Returns: + np.ndarray: An array of rates + """ if self.use_terminal: current_time = self.cursor.time if start_pos == 0 else self.cursor.time - start_pos * timeframe.seconds current_time = round_up(current_time, timeframe.seconds) @@ -988,7 +1171,19 @@ class BackTestEngine: return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) @error_handler - async def get_rates_range(self, *, symbol: str, timeframe: TimeFrame, date_from: datetime | float, date_to: datetime | float) -> np.ndarray: + async def get_rates_range(self, *, symbol: str, timeframe: TimeFrame, date_from: datetime | float, + date_to: datetime | float) -> np.ndarray: + """Get rates within a specific date range. Used by the backtester to get rates for a symbol + + Args: + symbol (str): The symbol to get rates for + timeframe (TimeFrame): The timeframe of the rates + date_from (datetime | float): The date from which to get the rates + date_to (datetime | float): The date to which to get the rates + + Returns: + np.ndarray: An array of rates + """ date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from, tz=UTC) date_to = date_to.astimezone(tz=UTC) if isinstance(date_to, datetime) else datetime.fromtimestamp(date_to, tz=UTC) if self.use_terminal: @@ -1002,7 +1197,18 @@ class BackTestEngine: return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) @error_handler - async def get_ticks_from(self, *, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks = CopyTicks.ALL) -> np.ndarray: + async def get_ticks_from(self, *, symbol: str, date_from: datetime | float, count: int, + flags: CopyTicks = CopyTicks.ALL) -> np.ndarray: + """Get a specified number of ticks counting from a specific date. + Args: + symbol (str): The symbol to get ticks for + date_from (datetime | float): The date from which to get the ticks + count (int): The number of ticks to get + flags (CopyTicks): The flags to use when getting the ticks + + Returns: + np.ndarray: An array of ticks + """ date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from, tz=UTC) if self.use_terminal: ticks = await self.mt5.copy_ticks_from(symbol, date_from, count, flags) @@ -1017,6 +1223,17 @@ class BackTestEngine: async def get_ticks_range( self, *, symbol: str, date_from: datetime | float, date_to: datetime | float, flags: CopyTicks = CopyTicks.ALL ) -> np.ndarray: + """Get ticks within a specific date range. + + Args: + symbol (str): The symbol to get ticks for + date_from (datetime | float): The date from which to get the ticks + date_to (datetime | float): The date to which to get the ticks + flags (CopyTicks): The flags to use when getting the ticks + + Returns: + np.ndarray: An array of ticks + """ date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from, tz=UTC) date_to = date_to.astimezone(tz=UTC) if isinstance(date_to, datetime) else datetime.fromtimestamp(date_to, tz=UTC) if self.use_terminal: @@ -1031,8 +1248,21 @@ class BackTestEngine: @error_handler async def order_calc_margin( - self, *, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, price: float, use_terminal: bool = None - ): + self, *, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, + price: float, use_terminal: bool = None): + """Calculate the margin required for a trade. + + Args: + action (Literal[OrderType.BUY, OrderType.SELL]): Type of order + symbol (str): Symbol name + volume (float): Volume of the trade + price (float): The price at which the trade is opened + use_terminal (bool): A flag to override the use_terminal attribute. If true, the terminal will be used + even if the use_terminal attribute is True. + + Returns: + float: The margin required for the trade + """ use_terminal = use_terminal if use_terminal is not None else self.use_terminal if use_terminal: return await self.mt5.order_calc_margin(action, symbol, volume, price) @@ -1045,8 +1275,23 @@ class BackTestEngine: @error_handler async def order_calc_profit( - self, *, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, price_open: float, price_close: float, use_terminal=None - ): + self, *, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, + price_open: float, price_close: float, use_terminal=None): + """ + Calculate the profit for a trade. + + Args: + action (Literal[OrderType.BUY, OrderType.SELL]): Type of order + symbol (str): Symbol name + volume (float): Volume of the trade + price_open (float): The price at which the trade is opened + price_close (float): The price at which the trade is closed + use_terminal (bool): A flag to override the use_terminal attribute. If true, the terminal will be used + even if the use_terminal attribute is True. + + Returns: + float: The profit of the trade + """ use_terminal = use_terminal if use_terminal is not None else self.use_terminal if use_terminal: @@ -1060,8 +1305,7 @@ class BackTestEngine: @error_handler_sync def get_orders_total(self) -> int: - """ - Get the total number of pending orders. + """Get the total number of pending orders. Returns: int: Total number of pending orders @@ -1070,8 +1314,7 @@ class BackTestEngine: @error_handler_sync def get_orders(self, *, symbol: str = "", group: str = "", ticket: int = None) -> tuple[TradeOrder, ...]: - """ - Get pending orders from the terminal history. This has to do with pending orders, which this backtester + """Get pending orders from the terminal history. This has to do with pending orders, which this backtester doesn't support yet. Args: @@ -1080,7 +1323,7 @@ class BackTestEngine: ticket: Order ticket Returns: - tuple[TradeOrder] + tuple[TradeOrder, ...]: Pending orders """ if symbol and group and ticket: return tuple() @@ -1088,8 +1331,7 @@ class BackTestEngine: @error_handler_sync def get_positions_total(self) -> int: - """ - Get the total number of open positions. + """Get the total number of open positions. Returns: int: Total number of open positions @@ -1098,23 +1340,21 @@ class BackTestEngine: @error_handler_sync def get_positions(self, *, symbol: str = None, group: str = None, ticket: int = None) -> tuple[TradePosition, ...]: - """ - Get open positions from the terminal history. + """Get open positions from the terminal history. - Keyword Args: + Args: symbol: The symbol name group: Group argument to filter by ticket: Position ticket Returns: - tuple[TradePosition]: Open positions + tuple[TradePosition, ...]: Open positions """ return self.positions.positions_get(ticket=ticket, symbol=symbol, group=group) @error_handler_sync def get_history_orders_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: - """ - Get the total number of orders in the terminal history. + """Get the total number of orders in the terminal history. Args: date_from: The start date of the history @@ -1128,12 +1368,11 @@ class BackTestEngine: @error_handler_sync def get_history_orders( - self, *, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None - ) -> tuple[TradeOrder, ...]: - """ - Get orders from the terminal history. + self, *, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", + ticket: int = None, position: int = None) -> tuple[TradeOrder, ...]: + """Get orders from the terminal history. - Keyword Args: + Args: date_from: Date from which to start the history date_to: Date to which to end the history group: group keyword to filter by @@ -1141,14 +1380,14 @@ class BackTestEngine: position: position id to filter by Returns: - tuple[TradeOrder]: Orders in the history + tuple[TradeOrder, ...]: Orders in the history """ - return self.orders.history_orders_get(date_from=date_from, date_to=date_to, group=group, ticket=ticket, position=position) + return self.orders.history_orders_get(date_from=date_from, date_to=date_to, group=group, + ticket=ticket, position=position) @error_handler_sync def get_history_deals_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: - """ - Get the total number of deals in the terminal history. + """Get the total number of deals in the terminal history. Args: date_from: Date from which to start the history @@ -1161,12 +1400,11 @@ class BackTestEngine: @error_handler_sync def get_history_deals( - self, *, date_from: datetime | float = None, date_to: datetime | float = None, group: str = None, position: int = None, ticket: int = None - ) -> tuple[TradeDeal, ...]: - """ - Get deals from the terminal history. + self, *, date_from: datetime | float = None, date_to: datetime | float = None, group: str = None, + position: int = None, ticket: int = None) -> tuple[TradeDeal, ...]: + """Get deals from the terminal history. - Keyword Args: + Args: date_from: Date from which to start the history date_to: Date to which to end the history group: group keyword to filter by @@ -1176,4 +1414,5 @@ class BackTestEngine: Returns: tuple[TradeDeal, ...]: Deals in the history """ - return self.deals.history_deals_get(date_from=date_from, date_to=date_to, group=group, position=position, ticket=ticket) + return self.deals.history_deals_get(date_from=date_from, date_to=date_to, group=group, + position=position, ticket=ticket) diff --git a/src/aiomql/core/backtesting/get_data.py b/src/aiomql/core/backtesting/get_data.py index 7b9118e..da30d2a 100644 --- a/src/aiomql/core/backtesting/get_data.py +++ b/src/aiomql/core/backtesting/get_data.py @@ -18,12 +18,33 @@ logger = getLogger(__name__) class Cursor(NamedTuple): + """A cursor to iterate over the data. Marks the current position.""" index: int time: int @dataclass class BackTestData: + """The data class to store the backtesting data. + + Attributes: + name (str): The name of the backtest data. + terminal (dict): The terminal information. + version (tuple): The version of the terminal. + account (dict): The account information. + symbols (dict): The symbols information. + ticks (dict): The ticks data. + rates (dict): The rates data. + span (range): The range of the data. + range (range): The range of the data. + orders (dict): The orders data. + deals (dict): The deals data. + positions (dict): The positions data. + open_positions (set): The open positions. + cursor (Cursor): The cursor to iterate over the data. + margins (dict): The margins data. + fully_loaded (bool): A flag to indicate if the data is fully loaded + """ name: str = "" terminal: dict[str, [str | int | bool | float]] = field(default_factory=dict) version: tuple[int, int, str] = (0, 0, "") @@ -48,18 +69,44 @@ class BackTestData: return f"{self.__class__.__name__}({self.name})" def set_attrs(self, **kwargs): + """Set the attributes of the class on the instance.""" [setattr(self, k, v) for k, v in kwargs.items() if k in self.fields] @property def fields(self): + """A list of the fields of the class.""" return [f.name for f in fields(self)] class GetData: + """A class to get the backtesting data from the MetaTrader5 terminal. + + Attributes: + start (datetime): The start date of the data. + end (datetime): The end date of the data. + symbols (Sequence[str]): The symbols to get the data for. + timeframes (Sequence[TimeFrame]): The timeframes to get the data for. + name (str): The name of the backtest data. + range (range): The range of the data. + span (range): The span of the data. + data (BackTestData): The backtesting data. + mt5 (MetaTrader): The MetaTrader5 instance. + task_queue (TaskQueue): The task queue to handle the requests. + """ data: BackTestData - def __init__(self, *, start: datetime, end: datetime, symbols: Sequence[str], timeframes: Sequence[TimeFrame], name: str = ""): - """""" + def __init__(self, *, start: datetime, end: datetime, symbols: Sequence[str], + timeframes: Sequence[TimeFrame], name: str = ""): + """ + Get the backtesting data from the MetaTrader5 terminal. + + Args: + start (datetime): The start date of the data. + end (datetime): The end date of the data. + symbols (Sequence[str]): The symbols to get the data for. + timeframes (Sequence[TimeFrame]): The timeframes to get the data for. + name (str): The name of the backtest data. + """ self.config = Config() self.start = start.astimezone(tz=UTC) self.end = end.astimezone(tz=UTC) @@ -76,7 +123,12 @@ class GetData: @classmethod def pickle_data(cls, *, data: BackTestData, name: str | Path): - """""" + """Pickle the data to a file. + + Args: + data (BackTestData): The data to pickle. + name (str | Path): The name of the file to pickle the data to. + """ try: with open(name, "wb") as fo: pickle.dump(data, fo, protocol=pickle.HIGHEST_PROTOCOL) @@ -85,7 +137,11 @@ class GetData: @classmethod def load_data(cls, *, name: str | Path) -> BackTestData: - """""" + """Load the data from a file. + + Args: + name (str | Path): The name of the file to load the data from. + """ try: with open(name, "rb") as fo: data = pickle.load(fo) @@ -94,13 +150,23 @@ class GetData: logger.error(f"Error: {err}") def save_data(self, *, name: str | Path = ""): + """Save the data to a file. + + Args: + name (str | Path): The name of the file to save the data to. If not provided, the name of the data is used. + """ name = name or (self.name + ".pkl" if not self.name.endswith(".pkl") else self.name) name = Path(self.config.backtest_dir) / name if not isinstance(name, Path) else name with open(name, "wb") as fo: pickle.dump(self.data, fo, protocol=pickle.HIGHEST_PROTOCOL) async def get_data(self, workers: int = None): - """""" + """Use the task queue to get the data from the MetaTrader5 terminal. + + Args: + workers (int): The number of workers to use in the task queue. If not provided, the default number of workers + is used. + """ if workers: self.task_queue.workers = workers @@ -124,7 +190,6 @@ class GetData: self.data = BackTestData(name=self.name, span=self.span, range=self.range, fully_loaded=False) async def get_terminal_info(self): - """""" terminal = await self.mt5.terminal_info() if terminal is None: self.data.fully_loaded = False @@ -133,7 +198,6 @@ class GetData: self.data.set_attrs(terminal=terminal) async def get_version(self): - """""" version = await self.mt5.version() if version is None: self.data.fully_loaded = False @@ -142,7 +206,6 @@ class GetData: @backoff_decorator async def get_account_info(self): - """""" res = await self.mt5.account_info() if res is None: self.data.fully_loaded = False @@ -151,15 +214,14 @@ class GetData: self.data.set_attrs(account=res) async def get_symbols_info(self): - """""" - [self.task_queue.add(item=QueueItem(self.get_symbol_info, symbol=symbol)) for symbol in self.symbols if self.data.symbols.get(symbol) is None] + [self.task_queue.add(item=QueueItem(self.get_symbol_info, symbol=symbol)) for symbol in self.symbols + if self.data.symbols.get(symbol) is None] async def get_symbols_ticks(self): - """""" - [self.task_queue.add(item=QueueItem(self.get_symbol_ticks, symbol=symbol)) for symbol in self.symbols if self.data.ticks.get(symbol) is None] + [self.task_queue.add(item=QueueItem(self.get_symbol_ticks, symbol=symbol)) for symbol in self.symbols if + self.data.ticks.get(symbol) is None] async def get_symbols_rates(self): - """""" [ self.task_queue.add(item=QueueItem(self.get_symbol_rates, symbol=symbol, timeframe=timeframe), priority=4) for symbol in self.symbols @@ -169,7 +231,6 @@ class GetData: @backoff_decorator async def get_symbol_info(self, *, symbol: str): - """""" res = await self.mt5.symbol_info(symbol) if res is None: self.data.fully_loaded = False @@ -178,7 +239,6 @@ class GetData: @backoff_decorator async def get_symbol_ticks(self, *, symbol: str): - """""" res = await self.mt5.copy_ticks_range(symbol, self.start, self.end, MetaTrader5.COPY_TICKS_ALL) if res is None: self.data.fully_loaded = False @@ -187,7 +247,6 @@ class GetData: @backoff_decorator async def get_symbol_rates(self, *, symbol: str, timeframe: TimeFrame): - """""" res = await self.mt5.copy_rates_range(symbol, timeframe, self.start, self.end) if res is None: self.data.fully_loaded = False diff --git a/src/aiomql/core/backtesting/trades_manager.py b/src/aiomql/core/backtesting/trades_manager.py index 132f8ca..94ffdca 100644 --- a/src/aiomql/core/backtesting/trades_manager.py +++ b/src/aiomql/core/backtesting/trades_manager.py @@ -10,6 +10,38 @@ TradeData = TypeVar("TradeData", bound=TradePosition | TradeOrder | TradeDeal) class TradeManager(Generic[TradeData]): + """A generic class to manage trades data during a backtest. It is the parent class of the + PositionsManager, OrdersManager, and DealsManager. It implements some dict-like methods to manage the data. + It has a private attribute _data to store the data. It exposes the data through the values, keys, and items methods. + It also has a to_dict method to convert the data to a dictionary. + + Attributes: + _data (dict[int, TradeData]): The data to store the trades. + + Examples: + >>> manager = TradeManager() + >>> manager[123456] = TradePosition(ticket=123456, symbol="EURUSD", volume=0.1) + >>> manager.update(ticket=123456, symbol="EURUSD", volume=0.1) + >>> manager[123456] + TradePosition(ticket=123456, symbol='EURUSD', volume=0.1) + >>> manager.values() + (TradePosition(ticket=123456, symbol='EURUSD', volume=0.1),) + >>> manager.keys() + (123456,) + >>> manager.items() + ((123456, TradePosition(ticket=123456, symbol='EURUSD', volume=0.1)),) + >>> manager.to_dict() + {123456: {'ticket': 123456, 'symbol': 'EURUSD', 'volume': 0.1}} + >>> pos = manager.get(123456) + >>> pos + TradePosition(ticket=123456, symbol='EURUSD', volume=0.1) + >>> pos in manager + True + >>> len(manager) + 1 + >>> pos in manager + False + """ _data: dict[int, TradeData] def __init__(self, *, data: dict = None): @@ -37,6 +69,12 @@ class TradeManager(Generic[TradeData]): return self._data.get(key, default) def update(self, *, ticket: int, **kwargs): + """Update the data of a trade. Given the ticket of the trade and the new data to update. + + Args: + ticket (int): The ticket of the trade to update. + **kwargs: The new data to update. + """ try: res = self[ticket] klass = type(res) @@ -49,24 +87,48 @@ class TradeManager(Generic[TradeData]): logger.error(f"Update Operation Failed: Could Not Find Ticket") def values(self) -> tuple[TradeData, ...]: + """Returns the values of the data.""" return tuple(value for value in self._data.values()) def keys(self) -> tuple[int, ...]: + """Returns the keys of the data.""" return tuple(key for key in self._data.keys()) def items(self) -> tuple[tuple[int, TradeData], ...]: + """Returns the items of the data.""" return tuple((key, value) for key, value in self._data.items()) def to_dict(self): + """Convert the data to a dictionary.""" return {key: value._asdict() for key, value in self._data.items()} class PositionsManager(TradeManager): + """A class to manage the open positions during a backtest. It is a subclass of TradeManager. It has an additional + attribute _open_positions to store the open positions. It also has a margins attribute to store the margins of the + open positions. It overrides some methods of the TradeManager class to manage the open positions. + + Attributes: + _open_positions (set[int]): The open positions. + margins (dict[int, float]): The margins of the open positions. + """ _data: dict[int, TradePosition] _open_positions: set[int] margins: dict[int, float] - def __init__(self, *, data: dict = None, open_positions: set = None, margins: dict = None): + def __init__(self, *, data: dict = None, open_positions: set[int] = None, margins: dict = None): + """Positions manager manages the open positions during a backtest. It is a subclass of TradeManager. It has an + additional attribute _open_positions to store the open positions. It also has a margins attribute to store the + margins of the open positions. It overrides some methods of the TradeManager class to manage the open positions. + + Args: + data (dict, optional): The data to store the trades. This used for continuation of the backtesting, if it + was stopped with some open positions. + + open_positions (set, optional): The open positions. Defaults to None. + + margins (dict, optional): The margins of the open positions. Defaults to None. + """ super().__init__(data=data) self._open_positions = open_positions or {trade.ticket for trade in self._data.values()} self.margins: dict[int, float] = margins or dict() @@ -96,20 +158,54 @@ class PositionsManager(TradeManager): return sum(self.margins.values()) def close(self, *, ticket: int) -> bool: + """Close a position. Given the ticket of the position to close. + + Args: + ticket (int): The ticket of the position to close. + """ is_open = ticket in self._open_positions self._open_positions.discard(ticket) return is_open def get_margin(self, *, ticket: int) -> float: + """Get the margin of a position. Given the ticket of the position. + + Args: + ticket (int): The ticket of the position. + + Returns: + float: The margin of the position. + """ return self.margins.get(ticket, 0.0) def delete_margin(self, *, ticket: int): + """Delete the margin of a position. Given the ticket of the position. + + Args: + ticket (int): The ticket of the position. + """ return self.margins.pop(ticket, 0) def set_margin(self, *, ticket: int, margin: float): + """Set the margin of a position. Given the ticket of the position and the margin. + + Args: + ticket (int): The ticket of the position. + margin (float): The margin of the position + """ self.margins[ticket] = margin def positions_get(self, *, ticket: int = None, symbol: str = None, group: None = None) -> tuple[TradePosition, ...]: + """Get positions. Given the ticket, symbol, or group of the positions. + + Args: + ticket (int): The ticket of the position. + symbol (str): The symbol of the position. + group (str): The group + + Returns: + tuple[TradePosition, ...]: The positions + """ if ticket: return tuple(position for position in self.open_positions if position.ticket == ticket) @@ -125,24 +221,58 @@ class PositionsManager(TradeManager): return tuple() def positions_total(self) -> int: + """Get the total number of open positions. + + Returns: + int: The total number of open positions. + """ return len(self._open_positions) @property def open_positions(self) -> tuple[TradePosition, ...]: + """Returns the open positions. + + Args: + tuple[TradePosition, ...]: The open positions. + """ return tuple(position for position in self.values() if position.ticket in self._open_positions) class OrdersManager(TradeManager): + """Managers orders data during a backtest. It is a subclass of TradeManager. It manages access to the historical + orders data + """ _data = dict[int, TradeOrder] def get_orders_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]: + """Get orders within a date range. Given the start and end date of the range. + + Args: + date_from (float): The start date of the range. + date_to (float): The end date of the range. + + Returns: + tuple[TradeData, ...]: The orders within the date range. + """ start = date_from.timestamp() if isinstance(date_from, datetime) else date_from end = date_to.timestamp() if isinstance(date_to, datetime) else date_to return tuple(order for order in self.values() if start <= order.time_setup <= end) - def history_orders_get( - self, *, date_from: float | datetime = None, date_to: float | datetime = None, group: str = "", ticket: int = None, position: int = None - ) -> tuple[TradeOrder, ...]: + def history_orders_get(self, *, date_from: float | datetime = None, date_to: float | datetime = None, + group: str = "", ticket: int = None, position: int = None) -> tuple[TradeOrder, ...]: + """Get historical orders. Given the start and end date of the range, the group, ticket, or position of the + orders. + + Args: + date_from (float, datetime): The start date of the range. + date_to (float, datetime): The end date of the range. + group (str): The group of the orders. + ticket (int): The ticket of the order. + position (int): The position of the order. + + Returns: + tuple[TradeOrder, ...]: The historical orders. + """ if date_from and date_to: orders = self.get_orders_range(date_from=date_from, date_to=date_to) if group: @@ -158,6 +288,12 @@ class OrdersManager(TradeManager): return () def history_orders_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: + """Get the total number of historical orders. Given the start and end date of the range. + + Args: + date_from (datetime, float): The start date of the range. + date_to (datetime, float): The end date of the range. + """ return len(self.get_orders_range(date_from=date_from, date_to=date_to)) @@ -165,13 +301,30 @@ class DealsManager(TradeManager): _data = dict[int, TradeDeal] def get_deals_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]: + """Get deals within a date range. Given the start and end date of the range. + + Args: + date_from (float): The start date of the range. + date_to (float): The end date of the range. + + Returns: + tuple[TradeData, ...]: The deals within the date range. + """ start = date_from.timestamp() if isinstance(date_from, datetime) else date_from end = date_to.timestamp() if isinstance(date_to, datetime) else date_to return tuple(deal for deal in self.values() if start <= deal.time <= end) - def history_deals_get( - self, *, date_from: float | datetime = None, date_to: float | datetime = None, group: str = "", ticket: int = None, position: int = None - ) -> tuple[TradeDeal, ...]: + def history_deals_get(self, *, date_from: float | datetime = None, date_to: float | datetime = None, + group: str = "", ticket: int = None, position: int = None) -> tuple[TradeDeal, ...]: + """History deals get. Given the start and end date of the range, the group, ticket, or position of the deals. + + Args: + date_from (float, datetime): The start date of the range. + date_to (float, datetime): The end date of the range. + group (str): The group of the deals. + ticket (int): The ticket of the deal. + position (int): The position of the deal. + """ if date_from and date_to: deals = self.get_deals_range(date_from=date_from, date_to=date_to) if group: @@ -187,4 +340,13 @@ class DealsManager(TradeManager): return () def history_deals_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: + """Get the total number of historical deals. Given the start and end date of the range + + Args: + date_from (datetime, float): The start date of the range. + date_to (datetime, float): The end date of the range. + + Returns: + int: The total number of historical deals. + """ return len(self.get_deals_range(date_from=date_from, date_to=date_to)) diff --git a/src/aiomql/core/base.py b/src/aiomql/core/base.py index 813f8ea..6b2bee5 100644 --- a/src/aiomql/core/base.py +++ b/src/aiomql/core/base.py @@ -12,10 +12,16 @@ logger = getLogger(__name__) class Base: """A base class for all data structure classes in the aiomql package. This class provides a set of common methods and attributes for handling data. + + Attributes: + exclude (set[str]): A set of attributes to be excluded when retrieving attributes + using the get_dict and dict method. + include (set [str]): A set of attributes to be included when retrieving attributes + using the get_dict and dict method. """ - exclude: set - include: set + exclude: set[str] + include: set[str] def __init__(self, **kwargs): """ @@ -74,12 +80,12 @@ class Base: annots |= getattr(base, "__annotations__", {}) return annots - def get_dict(self, exclude: set = None, include: set = None) -> dict: - """Returns class attributes as a dict, with the ability to filter + def get_dict(self, exclude: set[str] = None, include: set[str] = None) -> dict: + """Returns class attributes as a dict, with the ability to filter out specific attributes - Keyword Args: - exclude: A set of attributes to be excluded - include: Specific attributes to be returned + Args: + exclude (set[str]): A set of attributes to be excluded + include (set[str]): Specific attributes to be returned Returns: dict: A dictionary of specified class attributes @@ -120,6 +126,9 @@ class Base: class _Base(Base): + """Base class that provides access to the MetaTrader and Config classes as well as the MetaBackTester class for + backtesting mode. + """ def __init__(self, **kwargs): self.config = Config() self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester() diff --git a/src/aiomql/core/config.py b/src/aiomql/core/config.py index 2b75cf5..de44b87 100644 --- a/src/aiomql/core/config.py +++ b/src/aiomql/core/config.py @@ -1,4 +1,3 @@ -import inspect import os from pathlib import Path from typing import Iterator, Literal, TypeVar, Self @@ -12,28 +11,32 @@ Bot = TypeVar("Bot") BackTestEngine = TypeVar("BackTestEngine") -def func(): - stack = inspect.stack() - calling_context = next(context for context in stack if context.filename != __file__) - print(calling_context.filename) - return calling_context.filename - - class Config: """A class for handling configuration settings for the aiomql package. Attributes: - record_trades (bool): Whether to keep record of trades or not. - trade_record_mode: How to save trade, json or csv. Defaults to json - filename (str): Name of the config file - records_dir (str): Path to the directory where trade records are saved - login (int): Trading account number - password (str): Trading account password - server (str): Broker server - path (str): Path to terminal file - timeout (int): Timeout for terminal connection - state (dict): A global state dictionary for storing data across the framework - root (Path): Root directory of the project + login (int): The account login number + trade_record_mode (Literal["csv", "json"]): The mode for recording trades + password (str): The account password + server (str): The account server + path (str | Path): The path to the terminal + timeout (int): The timeout argument for the terminal + filename (str): The filename of the config file + state (dict): The + root (Path): The root directory of the project + record_trades (bool): To record trades or not. Default is True + records_dir (Path): The directory to store trade records, relative to the root directory + records_dir_name (str): The name of the trade records directory + backtest_dir (Path): The directory to store backtest results, relative to the root directory + backtest_dir_name (str): The name of the backtest directory + task_queue (TaskQueue): The TaskQueue object for handling background tasks + _backtest_engine (BackTestEngine): The backtest engine object + bot (Bot): The bot object + _instance (Self): The instance of the Config class + mode (Literal["backtest", "live"]): The trading mode, either backtest or live, default is live + use_terminal_for_backtesting (bool): Use the terminal for backtesting, default is True + shutdown (bool): A signal to shut down the terminal, default is False + force_shutdown (bool): A signal to force shut down the terminal, default is False Notes: By default, the config class looks for a file named aiomql.json. @@ -41,7 +44,6 @@ class Config: or the load_config method. By passing reload=True to the load_config method, you can reload and search again for the config file. """ - login: int trade_record_mode: Literal["csv", "json"] password: str @@ -93,6 +95,7 @@ class Config: return cls._instance def __init__(self, **kwargs): + """Initialize the Config object. The root directory can be set here or in the load_config method.""" root = kwargs.pop("root", None) if root is not None: self.load_config(root=root, **kwargs) @@ -101,16 +104,18 @@ class Config: @property def backtest_engine(self): + """Returns the backtest engine object""" return self._backtest_engine @backtest_engine.setter def backtest_engine(self, value: BackTestEngine): + """Set the backtest engine object""" self._backtest_engine = value def set_attributes(self, **kwargs): """Set keyword arguments as object attributes, The root folder attribute can't be set here. - Keyword Args: + Args: **kwargs: Object attributes and values as keyword arguments """ if kwargs.pop("root", None) is not None: @@ -146,11 +151,11 @@ class Config: def load_config(self, *, file: str | Path = None, filename: str = None, root: str | Path = None, **kwargs) -> Self: """Load configuration settings from a file. - Keyword Args: + Args: file (str | Path): The absolute path to the config file. filename (str): The name of the file to load if file path is not specified. If not provided aiomql.json is used root (str): The root directory of the project. - kwargs: Additional keyword arguments to set as object attributes. + **kwargs: Additional keyword arguments to be set on the config object. """ if root is not None: root = Path(root).resolve() @@ -197,6 +202,6 @@ class Config: """Returns Account login details as found in the config object if available Returns: - dict: A dictionary of login details + dict[str, int | str]: A dictionary of login details """ return {"login": self.login, "password": self.password, "server": self.server} diff --git a/src/aiomql/core/meta_backtester.py b/src/aiomql/core/meta_backtester.py index 1f07d92..14cfd7e 100644 --- a/src/aiomql/core/meta_backtester.py +++ b/src/aiomql/core/meta_backtester.py @@ -15,7 +15,11 @@ BackTestEngine = TypeVar("BackTestEngine") class MetaBackTester(MetaTrader): - """A class for testing trading strategies in the MetaTrader 5 terminal. A subclass of MetaTrader.""" + """A class for testing trading strategies in the MetaTrader 5 terminal. A subclass of MetaTrader. + + Attributes: + backtest_engine (BackTestEngine): The backtesting engine to use for testing trading strategies. + """ backtest_engine: BackTestEngine diff --git a/src/aiomql/core/meta_trader.py b/src/aiomql/core/meta_trader.py index 7ca328d..63f0384 100644 --- a/src/aiomql/core/meta_trader.py +++ b/src/aiomql/core/meta_trader.py @@ -1,7 +1,7 @@ import asyncio from datetime import datetime from logging import getLogger -from typing import Literal +from typing import Literal, Self from pathlib import Path import numpy as np @@ -23,7 +23,7 @@ class MetaTrader(MetaCore): self.config = Config() self.error: Error = Error(1) - async def __aenter__(self) -> "MetaTrader": + async def __aenter__(self) -> Self: """ Async context manager entry point. Initializes the connection to the MetaTrader terminal. diff --git a/src/aiomql/core/task_queue.py b/src/aiomql/core/task_queue.py index 75cd0a2..c9d381d 100644 --- a/src/aiomql/core/task_queue.py +++ b/src/aiomql/core/task_queue.py @@ -26,25 +26,40 @@ class QueueItem: try: if asyncio.iscoroutinefunction(self.task_item): await self.task_item(*self.args, **self.kwargs) - - else: - self.task_item(*self.args, **self.kwargs) - except Exception as err: - logger.error(f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs {self.kwargs}") + logger.error(f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs" + f" {self.kwargs}") class TaskQueue: - def __init__( - self, - size: int = 0, - workers: int = 10, - timeout: int = None, - queue: asyncio.Queue = None, - on_exit: Literal["cancel", "complete_priority"] = "complete_priority", - mode: Literal["finite", "infinite"] = "infinite", - worker_timeout: int = 60, - ): + """TaskQueue is a class that allows you to queue tasks and run them concurrently with a specified number of workers. + + Attributes: + - `workers` (int): The number of workers to run concurrently. Default is 10. + + - `timeout` (int): The maximum time to wait for the queue to complete. Default is None. If timeout is provided + the queue is joined using `asyncio.wait_for` with the timeout. + + - `queue` (asyncio.Queue): The queue to store the tasks. Default is `asyncio.PriorityQueue` with no size limit. + + - `on_exit` (Literal["cancel", "complete_priority"]): The action to take when the queue is stopped. + + - `mode` (Literal["finite", "infinite"]): The mode of the queue. If `finite` the queue will stop when all tasks + are completed. If `infinite` the queue will continue to run until stopped. + + - `worker_timeout` (int): The time to wait for a task to be added to the queue before stopping the worker or + adding a dummy sleep task to the queue. + + - `stop` (bool): A flag to stop the queue instance. + + - `tasks` (list): A list of the worker tasks running concurrently, including the main task that joins the queue. + + - `priority_tasks` (set): A set to store the QueueItems that must complete before the queue stops. + """ + + def __init__(self, size: int = 0, workers: int = 10, timeout: int = None, queue: asyncio.Queue = None, + on_exit: Literal["cancel", "complete_priority"] = "complete_priority", + mode: Literal["finite", "infinite"] = "infinite", worker_timeout: int = 60): self.queue = queue or asyncio.PriorityQueue(maxsize=size) self.workers = workers self.tasks = [] @@ -55,7 +70,14 @@ class TaskQueue: self.mode = mode self.worker_timeout = worker_timeout - def add(self, *, item: QueueItem, priority=3, must_complete=False): + def add(self, *, item: QueueItem, priority: int = 3, must_complete: bool = False): + """Add a task to the queue. + + Args: + item (QueueItem): The task to add to the queue. + priority (int): The priority of the task. Default is 3. + must_complete (bool): A flag to indicate if the task must complete before the queue stops. Default is False. + """ try: if self.stop: return @@ -67,6 +89,9 @@ class TaskQueue: except asyncio.QueueFull: logger.error("Queue is full") + except Exception as err: + logger.error("%s: Error occurred in %s.add", err, self.__class__.__name__) + async def worker(self): while True: try: @@ -98,7 +123,7 @@ class TaskQueue: await asyncio.sleep(self.worker_timeout) except Exception as err: - logger.error("%s: Error occurred in worker", err) + logger.error("%s: Error occurred in %s worker", err, self.__class__.__name__) async def run(self, timeout: int = 0): start = time.perf_counter() @@ -145,7 +170,7 @@ class TaskQueue: ... except Exception as err: - logger.error(f"%s: Error occurred in %s.clean_up", err, self.__class__.__name__) + logger.error("%s: Error occurred in %s.clean_up", err, self.__class__.__name__) finally: self.cancel() diff --git a/src/aiomql/lib/candle.py b/src/aiomql/lib/candle.py index 623e242..80bd836 100644 --- a/src/aiomql/lib/candle.py +++ b/src/aiomql/lib/candle.py @@ -194,10 +194,10 @@ class Candles: def __len__(self): return len(self._data.index) - def __contains__(self, item: Self): + def __contains__(self, item: Candle): return item.time == self[item.Index].time - def __getitem__(self, index) -> Self | Self | Series: + def __getitem__(self, index: slice | int | str) -> Self | Series | Candle: if isinstance(index, slice): cls = self.__class__ data = self._data.iloc[index]