From 4b5f26750986402480277184eab7db6b2a627cf1 Mon Sep 17 00:00:00 2001 From: Ichinga Samuel Date: Mon, 1 Jan 2024 05:25:41 +0100 Subject: [PATCH] version 3.12 --- docs/account.md | 14 +- docs/bot_builder.md | 20 +- docs/core/meta_trader.md | 644 +++++++++++++++++++---- docs/trader.md | 71 ++- pyproject.toml | 4 +- src/aiomql/account.py | 13 +- src/aiomql/bot_builder.py | 24 +- src/aiomql/candle.py | 73 +-- src/aiomql/core/base.py | 17 +- src/aiomql/core/config.py | 41 +- src/aiomql/core/constants.py | 4 +- src/aiomql/core/meta_trader.py | 8 +- src/aiomql/core/models.py | 4 +- src/aiomql/lib/strategies/__init__.py | 3 +- src/aiomql/lib/strategies/finger_trap.py | 187 ++----- src/aiomql/lib/strategies/tracker.py | 35 ++ src/aiomql/lib/symbols/crypto_symbol.py | 30 -- src/aiomql/lib/symbols/forex_symbol.py | 10 +- src/aiomql/lib/traders/__init__.py | 1 + src/aiomql/lib/traders/simple_trader.py | 61 +++ src/aiomql/order.py | 4 +- src/aiomql/positions.py | 8 +- src/aiomql/ram.py | 4 +- src/aiomql/records.py | 69 ++- src/aiomql/result.py | 17 +- src/aiomql/sessions.py | 2 +- src/aiomql/strategy.py | 34 +- src/aiomql/symbol.py | 9 +- src/aiomql/trader.py | 97 ++-- 29 files changed, 995 insertions(+), 513 deletions(-) create mode 100644 src/aiomql/lib/strategies/tracker.py delete mode 100644 src/aiomql/lib/symbols/crypto_symbol.py create mode 100644 src/aiomql/lib/traders/simple_trader.py diff --git a/docs/account.md b/docs/account.md index 68067e9..678358f 100644 --- a/docs/account.md +++ b/docs/account.md @@ -3,7 +3,8 @@ ```python class Account(AccountInfo) ``` -Singleton class for managing a trading account. A subclass of [AccountInfo](#accountinfo). All AccountInfo attributes are available in this class. +Singleton class for managing a trading account. A subclass of [AccountInfo](#accountinfo). +All AccountInfo attributes are available in this class. ### Attributes: |Name|Type|Description|Default| @@ -38,8 +39,8 @@ This method will only look for config details in the config instance if the logi ```python async def __aenter__() -> 'Account' ``` -Connect to a trading account and return the account instance. -Async context manager for the Account class. +Async context manager for the Account class. Connects to a trading account and returns the account instance. + #### Returns: |Type|Description| |---|---| @@ -63,8 +64,8 @@ Connect to a trading account. ```python def has_symbol(symbol: str | Type[SymbolInfo]) ``` -Checks to see if a symbol is available for a trading account\ -#### Arguments: +Checks to see if a symbol is available for a trading account +#### Parameters: |Name|Type|Description| |---|---|---| |**symbol**|**str** or **SymbolInfo**|A symbol name or SymbolInfo instance| @@ -81,5 +82,4 @@ Get all financial instruments from the MetaTrader 5 terminal available for the c #### Returns: |Type|Description| |---|---| -|**set[SymbolInfo]**|A set of SymbolInfo instances| - +|**set[SymbolInfo]**|A set of SymbolInfo instances| \ No newline at end of file diff --git a/docs/bot_builder.md b/docs/bot_builder.md index 3be23c1..1565753 100644 --- a/docs/bot_builder.md +++ b/docs/bot_builder.md @@ -1,7 +1,7 @@ ## Bot Builder ```python -class Bot() +class Bot ``` The bot class. Create a bot instance to run your strategies. ### Attributes: @@ -25,27 +25,27 @@ Prepares the bot by signing in to the trading account and initializing the symbo ```python def execute() ``` -Execute the bot. This method calls start internally. To enable you run your bot outside of an async function. +Execute the bot. ### start ```python async def start() ``` -Starts the bot by calling the initialize method and running the strategies in the executor. +Initialize the bot and execute it. Similar to calling `execute` method but is a coroutine. ### add_coroutine ```python def add_coroutine(coro: Coroutine, **kwargs) ``` -#### Arguments: +#### Parameters: |Name|Type|Description| |---|---|---| |**coro**|**Coroutine**|A coroutine to run in the executor| ### add_function ```python -def add_coroutine(func: Callable, **kwargs) +def add_function(func: Callable, **kwargs) ``` -#### Arguments: +#### Parameters: | Name | Type | Description | |----------|--------------|-----------------------------------| | **func** | **Callable** | A function to run in the executor | @@ -55,7 +55,7 @@ def add_coroutine(func: Callable, **kwargs) def add_strategy(strategy: Strategy) ``` Add a strategy to the executor. An added strategy will only run if it's symbol was successfully initialized. -#### Arguments: +#### Parameters: |Name|Type|Description| |---|---|---| |**strategy**|**Strategy**|A Strategy instance to run on bot| @@ -65,7 +65,7 @@ Add a strategy to the executor. An added strategy will only run if it's symbol w def add_strategies(strategies: Iterable[Strategy]) ``` Add multiple strategies at the same time -#### Arguments: +#### Parameters: |Name|Type|Description| |---|---|---| |**strategies**|**Iterable[Strategy]**|An iterable of Strategy instances| @@ -76,7 +76,7 @@ def add_strategy_all(*, strategy: Type[Strategy], params: dict | None = None) ``` Use this to run a single strategy on all available instruments in the market using the default parameters i.e one set of parameters for all trading symbols -#### Arguments +#### Parameters: |Name|Type|Description| |---|---|---| |**strategy**|**Type[Strategy]**|A Strategy class| @@ -95,7 +95,7 @@ async def init_symbol(symbol: Symbol) -> Symbol Initialize a symbol before the beginning of a trading session. Removes it from the list of symbols if it was not successfully initialized or not available for the account. -#### Arguments: +#### Parameters: |Name|Type|Description| |---|---|---| |**symbol**|**Symbol**|A Symbol instance| diff --git a/docs/core/meta_trader.md b/docs/core/meta_trader.md index fcdbe53..e16ee1a 100644 --- a/docs/core/meta_trader.md +++ b/docs/core/meta_trader.md @@ -1,82 +1,86 @@ -# Table of Contents - -* [aiomql.core.meta\_trader](#aiomql.core.meta_trader) - * [MetaTrader](#aiomql.core.meta_trader.MetaTrader) - * [\_\_aenter\_\_](#aiomql.core.meta_trader.MetaTrader.__aenter__) - * [\_\_aexit\_\_](#aiomql.core.meta_trader.MetaTrader.__aexit__) - * [login](#aiomql.core.meta_trader.MetaTrader.login) - * [initialize](#aiomql.core.meta_trader.MetaTrader.initialize) - * [shutdown](#aiomql.core.meta_trader.MetaTrader.shutdown) - * [version](#aiomql.core.meta_trader.MetaTrader.version) - * [account\_info](#aiomql.core.meta_trader.MetaTrader.account_info) - * [orders\_get](#aiomql.core.meta_trader.MetaTrader.orders_get) - - - -# aiomql.core.meta\_trader - - - -## MetaTrader Objects - +* [MetaTrader](#MetaTrader) + * [\_\_aenter\_\_](#__aenter__) + * [\_\_aexit\_\_](#aexit) + * [login](#MetaTrader.login) + * [initialize](#MetaTrader.initialize) + * [shutdown](#MetaTrader.shutdown) + * [version](#MetaTrader.version) + * [account\_info](#MetaTrader.account_info) + * [terminal\_info](#MetaTrader.terminal_info) + * [last\_error](#MetaTrader.last_error) + * [symbols\_total](#MetaTrader.symbols_total) + * [symbols\_get](#MetaTrader.symbols_get) + * [symbol\_info](#MetaTrader.symbol_info) + * [symbol\_info\_tick](#MetaTrader.symbol_info_tick) + * [symbol\_select](#MetaTrader.symbol_select) + * [market\_book\_add](#MetaTrader.market_book_add) + * [market\_book\_get](#MetaTrader.market_book_get) + * [market\_book\_release](#MetaTrader.market_book_release) + * [copy\_rates\_from](#MetaTrader.copy_rates_from) + * [copy\_rates\_from\_pos](#MetaTrader.copy_rates_from_pos) + * [copy\_rates\_range](#MetaTrader.copy_rates_range) + * [copy\_ticks\_from](#MetaTrader.copy_ticks_from) + * [copy\_ticks\_range](#MetaTrader.copy_ticks_range) + * [orders\_total](#MetaTrader.orders_total) + * [orders\_get](#MetaTrader.orders_get) + * [order\_calc\_margin](#MetaTrader.order_calc_margin) + * [order\_calc\_profit](#MetaTrader.order_calc_profit) + * [order\_check](#MetaTrader.order_check) + * [order\_send](#MetaTrader.order_send) + * [positions\_total](#MetaTrader.positions_total) + * [positions\_get](#MetaTrader.positions_get) + * [history\_orders\_total](#MetaTrader.history_orders_total) + * [history\_orders\_get](#MetaTrader.history_orders_get) + * [history\_deals\_total](#MetaTrader.history_deals_total) + * [history\_deals\_get](#MetaTrader.history_deals_get) + + +## MetaTrader ```python class MetaTrader(metaclass=BaseMeta) ``` +The MetaTrader class is a wrapper around the MetaTrader terminal. +It provides methods for connecting to the MetaTrader terminal and retrieving data from it. - - -#### \_\_aenter\_\_ - +### \_\_aenter\_\_ ```python async def __aenter__() -> 'MetaTrader' ``` - Async context manager entry point. Initializes the connection to the MetaTrader terminal. -**Returns**: - -- `MetaTrader` - An instance of the MetaTrader class. - - - -#### \_\_aexit\_\_ +#### Returns: +|Type|Description| +|---|---| +|**MetaTrader**|An instance of the MetaTrader class| +#### \_\_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 ``` - 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.| -**Arguments**: - -- `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**: - -- `bool` - True if successful, False otherwise. - - - -#### initialize - +#### initialize ```python async def initialize(path: str = "", login: int = 0, @@ -85,81 +89,519 @@ async def initialize(path: 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** or **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.| -**Arguments**: - -- `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_ - The timeout for the connection in seconds. -- `portable` _bool_ - If True, the terminal will be launched in portable mode. - - -**Returns**: - -- `bool` - True if successful, False otherwise. - - - -#### shutdown - +#### shutdown ```python async def shutdown() -> None ``` - Closes the connection to the MetaTrader terminal. -**Returns**: - -- `None` - None - - - -#### version - +#### version ```python async def version() -> tuple[int, int, str] | None ``` +Returns the version of the MetaTrader terminal. +#### Returns: +|Type| Description | +|---|-----------------------------------------------------------------------------------------------------| +|**tuple[int, int, str]**| A tuple of the MetaTrader terminal version. **Terminal Version**, **Build**, **Build Release Date** | - - - - + #### account\_info - ```python async def account_info() -> AccountInfo | None ``` +Returns the account information for the connected account. +#### Returns: +|Type|Description| +|---|---| +|**AccountInfo**|An instance of the AccountInfo class| + + +#### terminal\_info +```python +async def terminal_info() -> TerminalInfo | None +``` +Returns the terminal information for the connected terminal. +#### Returns: +|Type| Description | +|---|------------------------------------------------| +|**TerminalInfo**| An instance of the TerminalInfo class. A tuple | + + +#### last\_error +```python +async def last_error() -> tuple[int, str] +``` +Returns the last error code and description. +#### Returns: +|Type|Description| +|---|---| +|**tuple[int, str]**|A tuple of the last error code and description.| + + +#### symbols\_total +```python +async def symbols_total() -> int +``` +Returns the total number of symbols. +#### Returns: +|Type|Description| +|---|---| +|**int**|The total number of symbols.| + + +#### 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: +|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: +|Type|Description| +|---|---| +|**tuple[SymbolInfo]**|A tuple of SymbolInfo objects.| + +#### symbol\_info +```python +async def symbol_info(symbol: str) -> SymbolInfo | None +``` +Returns the symbol information for the specified symbol. +#### Parameters: +|Name|Type|Description| +|---|---|---| +|**symbol**|**str**|The symbol name.| +#### Returns: +|Type|Description| +|---|---| +|**SymbolInfo**|An instance of the SymbolInfo class.| - + +#### symbol\_info\_tick +```python +async def symbol_info_tick(symbol: str) -> Tick | None +``` +Returns the latest tick for the specified symbol. +#### Parameters: +|Name|Type|Description| +|---|---|---| +|**symbol**|**str**|The symbol name.| +#### Returns: +|Type|Description| +|---|---| +|**Tick**|An instance of the Tick class.| + +#### symbol\_select +```python +async def symbol_select(symbol: str, enable: bool) -> bool +``` +Selects or unselects the specified symbol in the Market Watch window. +#### 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: +|Type|Description| +|---|---| +|**bool**|True if successful, False otherwise.| + + +#### market\_book\_add +```python +async def market_book_add(symbol: str) -> bool +``` +Adds the specified symbol to the market book. +#### Parameters: +|Name|Type|Description| +|---|---|---| +|**symbol**|**str**|The symbol name.| +#### Returns: +|Type|Description| +|---|---| +|**bool**|True if successful, False otherwise.| + + + +#### market\_book\_get +```python +async def market_book_get(symbol: str) -> tuple[BookInfo] | None +``` +Returns the market depth for the specified symbol. +#### Parameters: +|Name|Type|Description| +|---|---|---| +|**symbol**|**str**|The symbol name.| +#### Returns: +|Type|Description| +|---|---| +|**tuple[BookInfo]**|A tuple of BookInfo objects.| + + +#### market\_book\_release +```python +async def market_book_release(symbol: str) -> bool +``` +Removes the specified symbol from the market book. +#### Parameters: +|Name|Type|Description| +|---|---|---| +|**symbol**|**str**|The symbol name.| +#### Returns: +|Type|Description| +|---|---| +|**bool**|True if successful, False otherwise.| + + +#### copy\_rates\_from + +```python +import numpy + + +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: +|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: +|Type|Description| +|---|---| +|**numpy.ndarray**|A numpy array of OHLCV rates.| + + +#### copy\_rates\_from\_pos +```python +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: +|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: +|Type|Description| +|---|---| +|**numpy.ndarray**|A numpy array of OHLCV rates.| + + +#### copy\_rates\_range +```python +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: +|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 +```python +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: +|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: +|Type|Description| +|---|---| +|**tuple[Tick]**|A tuple of Tick objects.| + + +#### copy\_ticks\_range +```python +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: +|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: +|Type|Description| +|---|---| +|**tuple[Tick]**|A tuple of Tick objects.| + + +#### orders\_total +```python +async def orders_total() -> int +``` +Returns the total number of active orders. +#### Returns: +|Type|Description| +|---|---| +|**int**|The total number of active orders.| + + #### orders\_get - ```python 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: +|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| -**Arguments**: + +#### order\_calc\_margin +```python +async def order_calc_margin(action: OrderType, + symbol: str, + volume: float, + price: float) -> float | None +``` +Calculates the margin required to open a trade. +#### 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: +|Type|Description| +|---|---| +|**float**|The margin required to open a trade.| -- `symbol` _str_ - Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored. - -- `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. - + +#### order\_calc\_profit +```python +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: +|Name|Type|Description| +|---|---|---| +|**action**|**OrderType**|The order type.| +|**symbol**|**str**|The symbol name.| +|**volume**|**float**|The order volume.| +|**price_open**|**float**|The order open price.| +|**price_close**|**float**|The order close price.| +#### Returns: +|Type|Description| +|---|---| +|**float**|The profit for a closed trade.| -**Returns**: + +#### order\_check +```python +async def order_check(request: dict) -> OrderCheckResult +``` +Checks the specified order for validity. +#### Parameters: +|Name|Type|Description| +|---|---|---| +|**request**|**dict**|The order request.| +#### Returns: +|Type|Description| +|---|---| +|**OrderCheckResult**|An instance of the OrderCheckResult class.| -- `list[TradeOrder]` - A list of active trade orders as TradeOrder objects + +#### order\_send +```python +async def order_send(request: dict) -> OrderSendResult +``` +Sends the specified order request to the MetaTrader terminal. +#### Parameters: +|Name|Type|Description| +|---|---|---| +|**request**|**dict**|The order request.| +#### Returns: +|Type|Description| +|---|---| +|**OrderSendResult**|An instance of the OrderSendResult class.| + +#### positions\_total +```python +async def positions_total() -> int +``` +Returns the total number of open positions. +#### Returns: +|Type|Description| +|---|---| +|**int**|The total number of open positions.| + + + +#### positions\_get +```python +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: +|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 +```python +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: +|Name|Type|Description| +|---|---|---| +|**date_from**|**datetime** or **int**|The start date.| +|**date_to**|**datetime** or **int**|The end date.| +#### Returns: +|Type|Description| +|---|---| +|**int**|The total number of closed orders for the specified period.| + + + +#### 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 +``` +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: +|Name|Type|Description| +|---|---|---| +|**date_from**|**datetime** or **int**|The start date. Optional named parameter.| +|**date_to**|**datetime** or **int**|The end date. Optional named parameter.| +|**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 +```python +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: +|Name|Type|Description| +|---|---|---| +|**date_from**|**datetime** or **int**|The start date.| +|**date_to**|**datetime** or **int**|The end date.| +#### Returns: +|Type|Description| +|---|---| +|**int**|The total number of closed deals for the specified period.| + + + +#### 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 +``` +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: +|Name|Type|Description| +|---|---|---| +|**date_from**|**datetime** or **int**|The start date. Optional named parameter.| +|**date_to**|**datetime** or **int**|The end date. Optional named parameter.| +|**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 | \ No newline at end of file diff --git a/docs/trader.md b/docs/trader.md index 35c342e..84ff150 100644 --- a/docs/trader.md +++ b/docs/trader.md @@ -6,36 +6,30 @@ class Trader() ``` Base class for creating a Trader object. Handles the creation of an order and the placing of trades ### Attributes: -|Name|Type|Description|Default| -|---|---|---|---| -|**name**|**str**|A name for the strategy.|None| -|**account**|**Account**|Account instance.|None| -|**mt5**|**MetaTrader**|MetaTrader instance.|None| -|**config**|**Config**|Config instance.|None| -|**symbol**|**Symbol**|The Financial Instrument as a Symbol Object|None| -|**parameters**|**Dict**|A dictionary of parameters for the strategy.|None| +| Name | Type | Description |Default| +|-------------|-------------|------------------------------------------------------|---| +| **ram** | **RAM** | Risk Assessment Management System. |None| +| **config** | **Config** | Config instance. |None| +| **order** | **Order** | Order instance. |None| +| **symbol** | **Symbol** | The Financial Instrument |None| +| **params** | **Dict** | A dictionary of parameters associated with the trade. |None| ### \_\_init\_\_ ```python def __init__(*, symbol: Symbol, ram: RAM = None) ``` +### Parameters: |Name| Type | Description | Default | |---|--------------------|-----------------------------|-------------------| |**symbol**| **Symbol** | The Financial instrument | None | |**ram**| **RAM** | Risk Assessment and Management instance | None | -Initializes the order object and RAM instance -#### Arguments: -|Name| Type | Description | Default | -|---|--------------------|-----------------------------|-------------------| -|**symbol**| **Symbol** | The Financial instrument | None | -|**ram**| **RAM** | Risk Assessment and Management instance | None | + ### create\_order ```python async def create_order(*, order_type: OrderType, **kwargs) ``` Complete the order object with the required values. Creates a simple order. -Uses the ram instance to set the volume. -#### Arguments: +#### Parameters: |Name| Type | Description | Default | |---|--------------------|-----------------------------|-------------------| |**order_type**| **OrderType** | Type of order | None | @@ -45,19 +39,56 @@ Uses the ram instance to set the volume. ```python async def set_order_limits(pips: float) ``` -Sets the stop loss and take profit for the order. -This method uses pips as defined for forex instruments. -#### Arguments: +Sets the stop loss and take profit for the order. This method uses pips as defined for forex instruments. +#### Parameters: |Name| Type | Description | Default | |---|--------------------|-----------------------------|-------------------| |**pips**| **float** | Target pips | None | +### set\_trade\_stop\_levels +```python +async def set_trade_stop_levels(*, points) +``` +sets the stop loss and take profit for the order. This method uses points as defined by MetaTrader5 for all symbols. + +#### Parameters: +|Name| Type | Description | Default | +|---|--------------------|-----------------------------|-------------------| +|**points**| **float** | Target points | None | + +### send\_order +```python +async def send_order() +``` +Sends the order to the broker for execution. Record the trade. + +### check_order +```python +async def check_order() +``` +Checks the status of the order before placing the trade. + +#### Returns: +|Type|Description| +|---|---| +|**bool**|True if order is valid else False| + +### record_trade +```python +async def record_trade(result: OrderSendResult) +``` +Records the trade and the order details if **Config.record_trades** is true. +#### Parameters: +|Name| Type | Description | Default | +|---|--------------------|--------------------------------|-------------------| +|**result**| **OrderSendResult** | The result of the placed order | None | + ### place\_trade ```python async def place_trade(order_type: OrderType, params: dict = None, **kwargs) ``` Places a trade based on the order_type. -#### Arguments: +#### Parameters: |Name| Type | Description | Default | |---|--------------------|-----------------------------|-------------------| |**order_type**| **OrderType** | Type of order | None | diff --git a/pyproject.toml b/pyproject.toml index 8f4e01c..cc54a55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "aiomql" -version = "3.0.7" +version = "3.12" readme = "README.md" requires-python = ">=3.10" classifiers = [ @@ -22,4 +22,4 @@ description = "Asynchronous MetaTrader5 library and Bot Building Framework" [project.urls] "Homepage" = "https://github.com/Ichinga-Samuel/aiomql" -"Bug Tracker" = "https://github.com/Ichinga-Samuel/aiomql/issues" +"Bug Tracker" = "https://github.com/Ichinga-Samuel/aiomql/issues" \ No newline at end of file diff --git a/src/aiomql/account.py b/src/aiomql/account.py index 153954a..f749714 100644 --- a/src/aiomql/account.py +++ b/src/aiomql/account.py @@ -27,9 +27,7 @@ class Account(AccountInfo): return cls._instance async def refresh(self): - """ - Refreshes the account instance with the latest account details from the MetaTrader 5 terminal - """ + """Refreshes the account instance with the latest account details from the MetaTrader 5 terminal""" account_info = await self.mt5.account_info() acc = account_info._asdict() self.set_attributes(**acc) @@ -83,8 +81,8 @@ class Account(AccountInfo): await self.mt5.shutdown() return False - def has_symbol(self, symbol: str | Type[SymbolInfo]): - """Checks to see if a symbol is available for a trading account + def has_symbol(self, symbol: str | SymbolInfo): + """Checks to see if a symbol is available for a trading account. Args: symbol (str | SymbolInfo): @@ -93,8 +91,7 @@ class Account(AccountInfo): bool: True if symbol is present otherwise False """ try: - symbol = SymbolInfo(name=str(symbol)) if not isinstance(symbol, SymbolInfo) else symbol - return symbol in self.symbols + return str(symbol) in {s.name for s in self.symbols} except Exception as err: logger.warning(f'Error: {err}; {symbol} not available in this market') return False @@ -106,4 +103,4 @@ class Account(AccountInfo): set[Symbol]: A set of available symbols. """ syms = await self.mt5.symbols_get() - return {SymbolInfo(name=sym.name) for sym in syms} + return {SymbolInfo(name=sym.name) for sym in syms} \ No newline at end of file diff --git a/src/aiomql/bot_builder.py b/src/aiomql/bot_builder.py index 228bfac..0d5b9d0 100644 --- a/src/aiomql/bot_builder.py +++ b/src/aiomql/bot_builder.py @@ -9,8 +9,8 @@ from .strategy import Strategy as _Strategy logger = logging.getLogger(__name__) -Strategy = TypeVar('Strategy', bound=_Strategy) -Symbol = TypeVar('Symbol', bound=_Symbol) +Strategy = TypeVar("Strategy", bound=_Strategy) +Symbol = TypeVar("Symbol", bound=_Symbol) class Bot: @@ -21,6 +21,7 @@ class Bot: executor: The default thread executor. symbols (list[Symbols]): A set of symbols for the trading session """ + account: Account = Account() def __init__(self): @@ -34,10 +35,10 @@ class Bot: SystemExit if sign in was not successful """ init = await self.account.sign_in() - logger.info("Login Successful") if not init: - logger.warning('Unable to sign in to MetaTrder 5 Terminal') + logger.warning("Unable to sign in to MetaTrder 5 Terminal") raise SystemExit + logger.info("Login Successful") await self.init_symbols() self.executor.remove_workers() @@ -63,13 +64,11 @@ class Bot: self.executor.add_coroutine(coro, kwargs) def execute(self): - """Execute the bot. - """ + """Execute the bot.""" asyncio.run(self.start()) async def start(self): - """Starts the bot by calling the initialize method and running the strategies in the executor. - """ + """Initialize the bot and execute it. Similar to calling `execute` method but is a coroutine.""" await self.initialize() await self.executor.execute() @@ -100,7 +99,10 @@ class Bot: strategy (Strategy): Strategy class params (dict): A dictionary of parameters for the strategy """ - [self.add_strategy(strategy(symbol=symbol, params=params)) for symbol in self.symbols] + [ + self.add_strategy(strategy(symbol=symbol, params=params)) + for symbol in self.symbols + ] async def init_symbols(self): """Initialize the symbols for the current trading session. This method is called internally by the bot.""" @@ -123,5 +125,5 @@ class Bot: if init: self.symbols.add(symbol) return symbol - logger.warning(f'Unable to initialize symbol {symbol}') - logger.warning(f'{symbol} not a available for this market') + logger.warning(f"Unable to initialize symbol {symbol}") + logger.warning(f"{symbol} not a available for this market") \ No newline at end of file diff --git a/src/aiomql/candle.py b/src/aiomql/candle.py index da0d4b6..43e8190 100644 --- a/src/aiomql/candle.py +++ b/src/aiomql/candle.py @@ -26,6 +26,7 @@ class Candle: real_volume (float): Trade volume spread (float): Spread Index (int): Custom attribute representing the position of the candle in a sequence. + mid (float): The median of the high and low price. """ time: float high: float @@ -36,6 +37,7 @@ class Candle: open: float tick_volume: float Index: int + mid: float def __init__(self, **kwargs): """Create a Candle object from keyword arguments. @@ -45,24 +47,30 @@ class Candle: """ self.time = kwargs.pop('time', 0) self.Index = kwargs.pop('Index', 0) + self.mid = kwargs.pop('mid', (kwargs['high'] + kwargs['low']) / 2) self.set_attributes(**kwargs) def __repr__(self): - keys = reprlib.repr(', '.join('%s=%s' % (i, j) for i, j in self.__dict__.items()))[1:-1] - return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': keys} + return ("%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s," + " mid=%(mid)s)") % {"class": self.__class__.__name__, "open": self.open, "high": self.high, + "low": self.low, "close": self.close, "time": self.time, "mid": self.mid, + 'Index': self.Index} - def __eq__(self, other: 'Candle'): + def __eq__(self, other: "Candle"): return self.time == other.time def __hash__(self): return hash(self.time) - def __lt__(self, other: 'Candle'): + def __lt__(self, other: "Candle"): return self.time < other.time - def __gt__(self, other: 'Candle'): + def __gt__(self, other: "Candle"): return self.time > other.time + def __getitem__(self, item): + return self.__dict__[item] + def set_attributes(self, **kwargs): """Set keyword arguments as instance attributes @@ -71,17 +79,8 @@ class Candle: """ [setattr(self, i, j) for i, j in kwargs.items()] - @property - def mid(self) -> float: - """The median of open and close - - Returns: - float: The median of open and close - """ - return (self.open + self.close) / 2 - def is_bullish(self) -> bool: - """ A simple check to see if the candle is bullish. + """A simple check to see if the candle is bullish. Returns: bool: True or False @@ -96,8 +95,9 @@ class Candle: """ return self.open > self.close -_Candle = TypeVar('_Candle', bound=Candle) -_Candles = TypeVar('_Candles', bound='Candles') + +_Candle = TypeVar("_Candle", bound=Candle) +_Candles = TypeVar("_Candles", bound="Candles") class Candles(Generic[_Candle]): @@ -132,9 +132,10 @@ class Candles(Generic[_Candle]): tick_volume: Series real_volume: Series spread: Series + mid: Series Candle: Type[Candle] timeframe: TimeFrame - + def __init__(self, *, data: DataFrame | _Candles | Iterable, flip=False, candle_class: Type[_Candle] = None): """A container class of Candle objects in chronological order. @@ -152,43 +153,52 @@ class Candles(Generic[_Candle]): elif isinstance(data, Iterable): data = DataFrame(data) else: - raise ValueError(f'Cannot create DataFrame from object of {type(data)}') + raise ValueError(f"Cannot create DataFrame from object of {type(data)}") - self._data = data.iloc[::-1] if flip else data + self._data = data.loc[::-1].reset_index(drop=True) if flip else data + if 'mid' not in self._data.columns.values: + mid = (self._data['high'] + self._data['low']) / 2 + self._data.insert(0, 'mid', mid) self.Candle = candle_class or Candle def __repr__(self): return self._data.__repr__() def __len__(self): - return self._data.shape[0] + return len(self._data.index) def __contains__(self, item: _Candle): return item.time == self[item.Index].time - def __getitem__(self, index) -> _Candle | _Candles: + def __getitem__(self, index) -> _Candle | _Candles | Series: if isinstance(index, slice): cls = self.__class__ data = self._data.iloc[index] data.reset_index(drop=True, inplace=True) return cls(data=data) - if isinstance(index, str): + elif isinstance(index, str): + if index == 'Index': + return Series(self._data.index) return self._data[index] - item = self._data.iloc[index] - return self.Candle(Index=index, **item) + elif isinstance(index, int): + index = index if index >= 0 else len(self) + index + return self.Candle(**self._data.iloc[index]) + raise TypeError(f"Expected int, slice or str got {type(index)}") def __setitem__(self, index, value: Series): if isinstance(value, Series): self._data[index] = value return - raise TypeError(f'Expected Series got {type(value)}') + raise TypeError(f"Expected Series got {type(value)}") def __getattr__(self, item): if item in list(self._data.columns.values): return self._data[item] - raise AttributeError(f'Attribute {item} not defined on class {self.__class__.__name__}') + if item == 'Index': + return Series(self._data.index) + raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}") def __iter__(self): return (self.Candle(**row._asdict()) for row in self._data.itertuples()) @@ -213,7 +223,7 @@ class Candles(Generic[_Candle]): Returns: ta: The ta library - """ + """ return ta @property @@ -221,7 +231,7 @@ class Candles(Generic[_Candle]): """The original data passed to the class as a pandas DataFrame""" return self._data - def rename(self, inplace=True, **kwargs) -> _Candles | None : + def rename(self, inplace=True, **kwargs) -> _Candles: """Rename columns of the candles class. Keyword Args: @@ -229,8 +239,7 @@ class Candles(Generic[_Candle]): **kwargs: The new names of the columns Returns: - Candles: A new instance of the class with the renamed columns if inplace is False. - None: If inplace is True + Candles: A new instance of the class with the renamed columns if inplace is False else the modified instance """ res = self._data.rename(columns=kwargs, inplace=inplace) - return res if inplace else self.__class__(data=res) + return self if inplace else self.__class__(data=res) \ No newline at end of file diff --git a/src/aiomql/core/base.py b/src/aiomql/core/base.py index 325cb0c..77ecf10 100644 --- a/src/aiomql/core/base.py +++ b/src/aiomql/core/base.py @@ -1,5 +1,5 @@ from functools import cache -import reprlib +import enum from logging import getLogger from .config import Config @@ -29,8 +29,11 @@ class Base: self.set_attributes(**kwargs) def __repr__(self): - keys = reprlib.repr(', '.join('%s=%s' % (i, j) for i, j in self.__dict__.items()))[1:-1] - return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': keys} + kv = [(k, v) for k, v in self.__dict__.items() if not k.startswith('_') and + (type(v) in (int, float, str) or isinstance(v, enum.Enum))] + args = (', '.join('%s=%s' % (i, j) for i, j in kv[:3])) + args = args if len(kv) <= 3 else args + ' ... ' + ', '.join('%s=%s' % (i, j) for i, j in kv[-1:]) + return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': args} def set_attributes(self, **kwargs): """Set keyword arguments as object attributes @@ -100,7 +103,7 @@ class Base: clss = self.__class__.__mro__[-3::-1] cls_dict = {} for cls in clss: - cls_dict |= cls.__dict__ + cls_dict |= cls.__dict__ return {key: value for key, value in cls_dict.items() if key in self.annotations} @property @@ -111,7 +114,8 @@ class Base: dict: A dictionary of instance and class attributes """ try: - return {key: value for key, value in (self.class_vars | self.__dict__).items() if key not in self.Meta.filter} + return {key: value for key, value in (self.class_vars | self.__dict__).items() if + key not in self.Meta.filter} except Exception as err: logger.warning(err) @@ -133,5 +137,4 @@ class Base: Returns: set: A set of attributes to be excluded """ - return cls.exclude.difference(cls.include) - \ No newline at end of file + return cls.exclude.difference(cls.include) \ No newline at end of file diff --git a/src/aiomql/core/config.py b/src/aiomql/core/config.py index bc519e6..19156f3 100644 --- a/src/aiomql/core/config.py +++ b/src/aiomql/core/config.py @@ -31,43 +31,42 @@ class Config: You can change this by passing the filename keyword argument to the constructor. By passing reload=True to the load_config method, you can reload and search again for the config file. """ + login: int = 0 - password: str = '' - server: str = '' - path: str = '' + password: str = "" + server: str = "" + path: str = "" timeout: int = 60000 record_trades: bool = True - filename: str = 'aiomql.json' + filename: str = "aiomql.json" win_percentage: float = 0.85 - records_dir = Path.home() / 'Documents' / 'Aiomql' / 'Trade Records' if record_trades else None + records_dir = Path.home() / "Documents" / "Aiomql" / "Trade Records" _load = 1 def __new__(cls, *args, **kwargs): - if not hasattr(cls, '_instance'): + if not hasattr(cls, "_instance"): cls._instance = super().__new__(cls) return cls._instance - + def __init__(self, **kwargs): self.load_config(reload=False) [setattr(self, key, value) for key, value in kwargs] - @staticmethod def walk_to_root(path: str) -> Iterator[str]: - if not os.path.exists(path): - raise IOError('Starting path not found') - + raise IOError("Starting path not found") + if os.path.isfile(path): path = os.path.dirname(path) - + last_dir = None current_dir = os.path.abspath(path) while last_dir != current_dir: yield current_dir parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir)) last_dir, current_dir = current_dir, parent_dir - + def find_config(self): current_file = __file__ frame = _getframe() @@ -77,13 +76,13 @@ class Config: frame = frame.f_back frame_filename = frame.f_code.co_filename path = os.path.dirname(os.path.abspath(frame_filename)) - + for dirname in self.walk_to_root(path): check_path = os.path.join(dirname, self.filename) if os.path.isfile(check_path): return check_path return None - + def load_config(self, file: str = None, reload: bool = True): if reload: self._load = 1 @@ -93,18 +92,18 @@ class Config: self._load = 0 data = {} if (file := (file or self.find_config())) is None: - logger.warning('No Config File Found') + logger.warning("No Config File Found") else: - fh = open(file, mode='r') + fh = open(file, mode="r") data = json.load(fh) fh.close() [setattr(self, key, value) for key, value in data.items()] self.records_dir.mkdir(parents=True, exist_ok=True) if self.records_dir else ... - def account_info(self) -> dict['login', 'password', 'server']: + def account_info(self) -> dict["login", "password", "server"]: """Returns Account login details as found in the config object if available - Returns: - dict: A dictionary of login details + Returns: + dict: A dictionary of login details """ - return {'login': self.login, 'password': self.password, 'server': self.server} + return {"login": self.login, "password": self.password, "server": self.server} \ No newline at end of file diff --git a/src/aiomql/core/constants.py b/src/aiomql/core/constants.py index e54a78a..b3d78ed 100644 --- a/src/aiomql/core/constants.py +++ b/src/aiomql/core/constants.py @@ -16,7 +16,7 @@ Examples: class Repr: __enum_name__ = "" - def __str__(self): + def __repr__(self): return f"{self.__enum_name__}_{self.name}" @@ -783,4 +783,4 @@ class AccountMarginMode(Repr, IntEnum): __enum_name__ = "ACCOUNT_MARGIN_MODE" RETAIL_NETTING = mt5.ACCOUNT_MARGIN_MODE_RETAIL_NETTING EXCHANGE = mt5.ACCOUNT_MARGIN_MODE_EXCHANGE - RETAIL_HEDGING = mt5.ACCOUNT_MARGIN_MODE_RETAIL_HEDGING + RETAIL_HEDGING = mt5.ACCOUNT_MARGIN_MODE_RETAIL_HEDGING \ No newline at end of file diff --git a/src/aiomql/core/meta_trader.py b/src/aiomql/core/meta_trader.py index ebfc9d1..8b03328 100644 --- a/src/aiomql/core/meta_trader.py +++ b/src/aiomql/core/meta_trader.py @@ -137,7 +137,6 @@ class MetaTrader(metaclass=BaseMeta): if res is None: err = await self.last_error() logger.warning(f'Error in obtaining account information.{Error(*err)}') - return res async def terminal_info(self) -> TerminalInfo | None: @@ -210,17 +209,14 @@ class MetaTrader(metaclass=BaseMeta): err = await self.last_error() logger.warning(f'Error in obtaining rates for {symbol}.{Error(*err)}') return res - return res async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int): res = await asyncio.to_thread(self._copy_rates_from_pos, symbol, timeframe, start_pos, count) - if res is None: err = await self.last_error() logger.warning(f'Error in obtaining rates for {symbol}.{Error(*err)}') return res - return res async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | int, @@ -270,7 +266,7 @@ class MetaTrader(metaclass=BaseMeta): ticket (int): Order ticket (ORDER_TICKET). Optional named parameter. Returns: - list[TradeOrder]: A list of active trade orders as TradeOrder objects + tuple[TradeOrder]: A list of active trade orders as TradeOrder objects """ kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value} res = await asyncio.to_thread(self._orders_get, **kwargs) @@ -352,4 +348,4 @@ class MetaTrader(metaclass=BaseMeta): logger.warning(f'Error in getting deals.{Error(*err)}') return res - return res + return res \ No newline at end of file diff --git a/src/aiomql/core/models.py b/src/aiomql/core/models.py index 9dbb043..aa1015e 100644 --- a/src/aiomql/core/models.py +++ b/src/aiomql/core/models.py @@ -338,7 +338,7 @@ class SymbolInfo(Base): super().__init__(**kwargs) def __repr__(self): - return self.name + return '%(class)s(name=%(name)s)' % {'class': self.__class__.__name__, 'name': self.name} def __str__(self): return self.name @@ -610,4 +610,4 @@ class TradeDeal(Base): tp: float symbol: str comment: str - external_id: str + external_id: str \ No newline at end of file diff --git a/src/aiomql/lib/strategies/__init__.py b/src/aiomql/lib/strategies/__init__.py index cb41d25..ee6a1ee 100644 --- a/src/aiomql/lib/strategies/__init__.py +++ b/src/aiomql/lib/strategies/__init__.py @@ -1 +1,2 @@ -from .finger_trap import FingerTrap \ No newline at end of file +from .finger_trap import FingerTrap +from .tracker import Tracker \ No newline at end of file diff --git a/src/aiomql/lib/strategies/finger_trap.py b/src/aiomql/lib/strategies/finger_trap.py index 4c100dd..6c160dd 100644 --- a/src/aiomql/lib/strategies/finger_trap.py +++ b/src/aiomql/lib/strategies/finger_trap.py @@ -1,8 +1,8 @@ import asyncio import logging -from typing import Literal -from dataclasses import dataclass +from .tracker import Tracker +from ..traders import SimpleTrader from ...symbol import Symbol from ...trader import Trader from ...candle import Candles @@ -13,49 +13,6 @@ from ...sessions import Sessions logger = logging.getLogger(__name__) -@dataclass -class Entry: - """ - Entry class for FingerTrap strategy. Will be used to store entry conditions and other entry related data. - - Attributes: - bearish (bool): True if the market is bearish - bullish (bool): True if the market is bullish - ranging (bool): True if the market is ranging - snooze (float): Time to wait before checking for entry conditions - trend (str): The current trend of the market - new (bool): True if the last candle is new - order_type (OrderType): The type of order to place - """ - - bearish: bool = False - bullish: bool = False - ranging: bool = True - trending: bool = False - trend: Literal["ranging", "bullish", "bearish"] = "ranging" - snooze: float = 0 - last_trend_time: float = 0 - last_entry_time: float = 0 - new: bool = True - order_type: OrderType | None = None - - def update(self, **kwargs): - fields = self.__dict__ - for key in kwargs: - if key in fields: - setattr(self, key, kwargs[key]) - match self.trend: - case "ranging": - self.ranging = True - self.trending = self.bullish = self.bearish = False - case "bullish": - self.ranging = self.bearish = False - self.bullish = self.trending = True - case "bearish": - self.ranging = self.bullish = False - self.bearish = self.trending = True - - class FingerTrap(Strategy): trend_time_frame: TimeFrame entry_time_frame: TimeFrame @@ -64,123 +21,75 @@ class FingerTrap(Strategy): slow_period: int entry_period: int parameters: dict - prices: Candles - name = "FingerTrap" - interval: TimeFrame entry_candles_count: int trend_candles_count: int + trader: Trader + tracker: Tracker + _parameters = {"trend": 3, "fast_period": 8, "slow_period": 34, "entry_time_frame": TimeFrame.M5, + "trend_time_frame": TimeFrame.H1, "entry_period": 8, + "trend_candles_count": 48, "entry_candles_count": 50} - def __init__( - self, - *, - symbol: Symbol, - params: dict | None = None, - trader: Trader = None, - sessions: Sessions = None, - ): - super().__init__(symbol=symbol, params=params, sessions=sessions) - self.trend = self.parameters.get("trend", 3) - self.fast_period = self.parameters.setdefault("fast_period", 8) - self.slow_period = self.parameters.setdefault("slow_period", 34) - self.entry_time_frame = self.parameters.setdefault( - "entry_time_frame", TimeFrame.M5 - ) - self.trend_time_frame = self.parameters.setdefault( - "trend_time_frame", TimeFrame.H1 - ) - self.trader = trader or Trader(symbol=self.symbol) - self.entry: Entry = Entry(snooze=self.trend_time_frame.time) - self.entry_period = self.parameters.setdefault("entry_period", 8) - - self.trend_candles_count = self.parameters.setdefault( - "trend_candles_count", 86400 // self.trend_time_frame.time - ) - self.trend_candles_count = max(self.trend_candles_count, self.slow_period) - self.entry_candles_count = self.trend_candles_count * ( - self.trend_time_frame.time // self.entry_time_frame.time - ) - self.entry_candles_count = max(self.entry_candles_count, self.entry_period) + def __init__(self, *, symbol: Symbol, params: dict | None = None, trader: Trader = None, sessions: Sessions = None, + name: str = 'FingerTrap'): + super().__init__(symbol=symbol, params=params, sessions=sessions, name=name) + self.trader = trader or SimpleTrader(symbol=self.symbol) + self.tracker: Tracker = Tracker(snooze=self.trend_time_frame.time) async def check_trend(self): try: - candles = await self.symbol.copy_rates_from_pos( - timeframe=self.trend_time_frame, count=self.trend_candles_count - ) - current = candles[-1] - if current.time > self.entry.last_trend_time: - self.entry.update(new=True, last_trend_time=current.time) - else: - self.entry.update(new=False) + candles: Candles = await self.symbol.copy_rates_from_pos(timeframe=self.trend_time_frame, + count=self.trend_candles_count) + if not ((current := candles[-1].time) >= self.tracker.trend_time): + self.tracker.new = False return - + self.tracker.update(new=True, trend_time=current) candles.ta.ema(length=self.slow_period, append=True, fillna=0) candles.ta.ema(length=self.fast_period, append=True, fillna=0) - candles.rename( - inplace=True, - **{ - f"EMA_{self.fast_period}": "fast", - f"EMA_{self.slow_period}": "slow", - }, - ) - + candles.rename(inplace=True, **{f"EMA_{self.fast_period}": "fast", f"EMA_{self.slow_period}": "slow"}) # Compute candles["fast_A_slow"] = candles.ta_lib.above(candles.fast, candles.slow) candles["fast_B_slow"] = candles.ta_lib.below(candles.fast, candles.slow) candles["close_A_fast"] = candles.ta_lib.above(candles.close, candles.fast) candles["close_B_fast"] = candles.ta_lib.below(candles.close, candles.fast) - trend = candles[-self.trend : -1] - if all( - (c.is_bullish() and c.fast_A_slow and c.close_A_fast) for c in trend - ): - self.entry.update(trend="bullish") - - elif all( - c.is_bearish() and c.fast_B_slow and c.close_B_fast for c in trend - ): - self.entry.update(trend="bearish") + trend = candles[-self.trend: -1] + if all((c.is_bullish() and c.fast_A_slow and c.close_A_fast) for c in trend): + self.tracker.update(trend="bullish") + elif all(c.is_bearish() and c.fast_B_slow and c.close_B_fast for c in trend): + self.tracker.update(trend="bearish") else: - self.entry.update(trend="ranging", snooze=self.trend_time_frame.time) + self.tracker.update(trend="ranging", snooze=self.trend_time_frame.time) except Exception as exe: logger.error(f"{exe}. Error in {self.__class__.__name__}.check_trend") async def confirm_trend(self): try: - candles = await self.symbol.copy_rates_from_pos( - timeframe=self.entry_time_frame, count=self.entry_candles_count - ) - current = candles[-1] - if current.time > self.entry.last_entry_time: - self.entry.update(new=True, last_entry_time=current.time) - else: - self.entry.update(new=False) + candles = await self.symbol.copy_rates_from_pos(timeframe=self.entry_time_frame, + count=self.entry_candles_count) + if not ((current := candles[-1].time) >= self.tracker.entry_time): + self.tracker.new = False return - + self.tracker.update(new=True, entry_time=current) candles.ta.ema(length=self.entry_period, append=True, fillna=0) candles.rename(**{f"EMA_{self.entry_period}": "ema"}) candles["close_A_ema"] = candles.ta_lib.above(candles.close, candles.ema) candles["close_B_ema"] = candles.ta_lib.below(candles.close, candles.ema) candles["close_XA_ema"] = candles.ta_lib.cross(candles.close, candles.ema) - candles["close_XB_ema"] = candles.ta_lib.cross( - candles.close, candles.ema, above=False - ) - if self.entry.bullish and current.close_XA_ema: - self.entry.update( - snooze=self.entry_time_frame.time, order_type=OrderType.BUY - ) - elif self.entry.bearish and current.close_XB_ema: - self.entry.update( - snooze=self.entry_time_frame.time, order_type=OrderType.SELL - ) + candles["close_XB_ema"] = candles.ta_lib.cross(candles.close, candles.ema, above=False) + current = candles[-2] + if self.tracker.bullish and current.close_XA_ema: + self.tracker.update(snooze=self.entry_time_frame.time, order_type=OrderType.BUY) + elif self.tracker.bearish and current.close_XB_ema: + self.tracker.update(snooze=self.entry_time_frame.time, order_type=OrderType.SELL) else: - self.entry.update(snooze=self.entry_time_frame.time, order_type=None) + self.tracker.update(snooze=self.entry_time_frame.time, order_type=None) except Exception as exe: - logger.error(f"{exe} Error in {self.__class__.__name__}.confirm_trend") + logger.error(f"{exe} Error in {self.name}.confirm_trend") async def watch_market(self): await self.check_trend() - if not self.entry.ranging: + if not self.tracker.ranging: await self.confirm_trend() async def trade(self): @@ -190,21 +99,15 @@ class FingerTrap(Strategy): await sess.check() try: await self.watch_market() - if not self.entry.new: + if not self.tracker.new: await asyncio.sleep(2) continue - if self.entry.order_type is None: - await self.sleep(self.entry.snooze) + if self.tracker.order_type is None: + await self.sleep(self.tracker.snooze) continue - - await self.trader.place_trade( - order_type=self.entry.order_type, params=self.parameters - ) - await self.sleep(self.entry.snooze) + await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters) + await self.sleep(self.tracker.snooze) except Exception as err: - logger.error( - f"Error: {err}\t Symbol: {self.symbol} in {self.__class__.__name__}.trade" - ) + logger.error(f"Error: {err}\t Symbol: {self.symbol} in {self.__class__.__name__}.trade") await self.sleep(self.trend_time_frame.time) - continue - + continue \ No newline at end of file diff --git a/src/aiomql/lib/strategies/tracker.py b/src/aiomql/lib/strategies/tracker.py new file mode 100644 index 0000000..273cb02 --- /dev/null +++ b/src/aiomql/lib/strategies/tracker.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass +from typing import Literal + +from ...core.constants import OrderType + + +@dataclass +class Tracker: + """Keeps track of a strategy's data and state""" + trend: Literal["ranging", "bullish", "bearish"] = "ranging" + bullish: bool = False + bearish: bool = False + ranging: bool = True + snooze: float = 0 + trend_time: float = 0 + entry_time: float = 0 + new: bool = True + order_type: OrderType = None + + def update(self, **kwargs): + fields = self.__dict__ + for key in kwargs: + if key in fields: + setattr(self, key, kwargs[key]) + if 'trend' in kwargs: + match self.trend: + case "ranging": + self.ranging = True + self.bullish = self.bearish = False + case "bullish": + self.ranging = self.bearish = False + self.bullish = True + case "bearish": + self.ranging = self.bullish = False + self.bearish = True \ No newline at end of file diff --git a/src/aiomql/lib/symbols/crypto_symbol.py b/src/aiomql/lib/symbols/crypto_symbol.py deleted file mode 100644 index 39b7954..0000000 --- a/src/aiomql/lib/symbols/crypto_symbol.py +++ /dev/null @@ -1,30 +0,0 @@ -from ...symbol import Symbol -from ...core.exceptions import VolumeError - - -class CryptoSymbol(Symbol): - """Subclass of Symbol for Crypto/Fiat Symbols. Handles the computation of volume based on the amount to risk.""" - - async def compute_volume(self, *, amount: float, points, use_limits=False) -> float: - """Compute volume given an amount to risk and target pips. Round the computed volume to the nearest step. - - Args: - amount (float): Amount to risk. Given in terms of the account currency. - points (float): Target pips. - use_limits (bool): If True, the computed volume checked against the maximum and minimum volume. - - Returns: - float: volume - - Raises: - VolumeError: If the computed volume is less than the minimum volume or greater than the maximum volume. - """ - if self.currency_profit != self.account.currency: - amount = await self.convert_currency(amount=amount, base=self.currency_profit, quote=self.account.currency) - volume = amount / (self.point * points * self.trade_contract_size) - volume = self.round_off_volume(volume) - if self.check_volume(volume)[0]: - return volume - if use_limits: - return self.check_volume(volume)[1] - raise VolumeError(f'Incorrect Volume. Computed Volume outside the range of permitted volumes') diff --git a/src/aiomql/lib/symbols/forex_symbol.py b/src/aiomql/lib/symbols/forex_symbol.py index 029c67f..16ea3d9 100644 --- a/src/aiomql/lib/symbols/forex_symbol.py +++ b/src/aiomql/lib/symbols/forex_symbol.py @@ -7,12 +7,12 @@ class ForexSymbol(Symbol): take profit and volume. """ - async def compute_volume(self, *, amount: float, pips, use_limits=False) -> float: - """Compute volume given an amount to risk and target pips. Round the computed volume to the nearest step. + async def compute_volume(self, *, amount: float, points, use_limits=False) -> float: + """Compute volume given an amount to risk and target points. Round the computed volume to the nearest step. Args: amount (float): Amount to risk. Given in terms of the account currency. - pips (float): Target pips. + points (float): Target pips. use_limits (bool): If True, the computed volume checked against the maximum and minimum volume. Returns: @@ -23,10 +23,10 @@ class ForexSymbol(Symbol): """ if self.currency_profit != self.account.currency: amount = await self.convert_currency(amount=amount, base=self.currency_profit, quote=self.account.currency) - volume = amount / (self.pip * pips * self.trade_contract_size) + volume = amount / (self.point * points * self.trade_contract_size) volume = self.round_off_volume(volume) if self.check_volume(volume)[0]: return volume if use_limits: return self.check_volume(volume)[1] - raise VolumeError(f'Incorrect Volume. Computed Volume outside the range of permitted volumes') + raise VolumeError(f"Incorrect Volume. Computed Volume outside the range of permitted volumes") \ No newline at end of file diff --git a/src/aiomql/lib/traders/__init__.py b/src/aiomql/lib/traders/__init__.py index e69de29..53681c0 100644 --- a/src/aiomql/lib/traders/__init__.py +++ b/src/aiomql/lib/traders/__init__.py @@ -0,0 +1 @@ +from .simple_trader import SimpleTrader \ No newline at end of file diff --git a/src/aiomql/lib/traders/simple_trader.py b/src/aiomql/lib/traders/simple_trader.py new file mode 100644 index 0000000..4d65d51 --- /dev/null +++ b/src/aiomql/lib/traders/simple_trader.py @@ -0,0 +1,61 @@ +"""Trader class module. Handles the creation of an order and the placing of trades""" + +from logging import getLogger + +from ..symbols import ForexSymbol +from ...ram import RAM +from ...core.models import OrderType +from ...positions import Positions +from ...trader import Trader + +logger = getLogger(__name__) + + +class SimpleTrader(Trader): + """A simple trader class. Limits the number of loosing trades per symbol""" + def __init__(self, *, symbol: ForexSymbol, ram: RAM = None, num_trades: int = 1): + """Initializes the order object and RAM instance + + Args: + symbol (Symbol): Financial instrument + ram (RAM): Risk Assessment and Management instance + num_trades (int): Number of open trades in loosing positions to allow per symbol + """ + super().__init__(symbol=symbol, ram=ram) + self.positions = Positions(symbol=symbol.name) + self.num_trades = num_trades + + async def create_order(self, *, order_type: OrderType, points: float = 0): + """Complete the order object with the required values. Creates a simple order. + + Args: + order_type (OrderType): Type of order + points (float): Target points + """ + positions = await self.positions.positions_get() + positions.sort(key=lambda pos: pos.time_msc) + loosing = [trade for trade in positions if trade.profit < 0] + if (losses := len(loosing)) > self.num_trades: + raise RuntimeError(f"Last {losses} trades in a losing position") + points = points or self.symbol.trade_stops_level * 2 + amount = self.ram.amount or await self.ram.get_amount() + self.order.volume = await self.symbol.compute_volume(amount=amount, points=points) + self.order.type = order_type + await self.set_trade_stop_levels(points=points) + + async def place_trade(self, order_type: OrderType, parameters: dict = None, points: float = 0): + """Places a trade based on the order_type. + + Args: + order_type (OrderType): Type of order + parameters: parameters of the trading strategy used to place the trade + points (float): Target points + """ + try: + self.parameters |= parameters or {} + await self.create_order(order_type=order_type, points=points) + if not await self.check_order(): + return + await self.send_order() + except Exception as err: + logger.error(f"{err}. Symbol: {self.order.symbol}\n {self.__class__.__name__}.place_trade") \ No newline at end of file diff --git a/src/aiomql/order.py b/src/aiomql/order.py index 27e4358..a883dba 100644 --- a/src/aiomql/order.py +++ b/src/aiomql/order.py @@ -79,7 +79,7 @@ class Order(TradeRequest): """ res = await self.mt5.order_send(self.dict) if res is None: - raise OrderError(f'Failed to send order {self.symbol} {self.type} {self.volume} {self.price} {res}') + raise OrderError(f'Failed to send order {self.symbol} {self.type} {self.volume} {self.price}') return OrderSendResult(**res._asdict()) async def calc_margin(self) -> float: @@ -109,4 +109,4 @@ class Order(TradeRequest): if res is None: raise OrderError( f'Failed to calculate profit for {self.symbol} {self.type} {self.volume} {self.price} {self.tp}') - return res + return res \ No newline at end of file diff --git a/src/aiomql/positions.py b/src/aiomql/positions.py index 98f8b7c..6a5d0ff 100644 --- a/src/aiomql/positions.py +++ b/src/aiomql/positions.py @@ -54,10 +54,8 @@ class Positions: Returns: list[TradePosition]: A list of open trade positions """ - symbol = symbol or self.symbol - group = group or self.group - ticket = ticket or self.ticket - positions = await self.mt5.positions_get(group=group, symbol=symbol, ticket=ticket) + positions = await self.mt5.positions_get(group=group or self.group, symbol=symbol or self.symbol, + ticket=ticket or self.ticket) if not positions: return [] return [TradePosition(**pos._asdict()) for pos in positions] @@ -87,4 +85,4 @@ class Positions: results = await asyncio.gather(*[order for order in orders], return_exceptions=True) amount_closed = len([res for res in results if res.retcode == 10009]) - return amount_closed + return amount_closed \ No newline at end of file diff --git a/src/aiomql/ram.py b/src/aiomql/ram.py index c87eca3..ba9fb39 100644 --- a/src/aiomql/ram.py +++ b/src/aiomql/ram.py @@ -7,6 +7,8 @@ class RAM: risk_to_reward: float risk: float amount: float + points: float + pips: float def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, amount: float = 0, **kwargs): """Initialize Risk Assessment and Management with the provided keyword arguments. @@ -33,4 +35,4 @@ class RAM: """ await self.account.refresh() risk = risk or self.risk - return self.account.equity * risk + return self.account.equity * risk \ No newline at end of file diff --git a/src/aiomql/records.py b/src/aiomql/records.py index 3aabca7..caef3ac 100644 --- a/src/aiomql/records.py +++ b/src/aiomql/records.py @@ -3,9 +3,11 @@ import asyncio from pathlib import Path import csv +import logging -from .history import History -from .core import Config +from .core import Config, MetaTrader + +logger = logging.getLogger(__name__) class Records: @@ -17,6 +19,7 @@ class Records: from the config """ config: Config = Config() + mt5: MetaTrader = MetaTrader() def __init__(self, records_dir: Path = ''): """Initialize the Records class. The main method of this class is update_records which you should call to update @@ -43,16 +46,43 @@ class Records: Args: file: Trade record file """ - fr = open(file, mode='r', newline='') - reader = csv.DictReader(fr) - rows = [row for row in reader] - rows = await self.update_rows(rows) - fr.close() - fw = open(file, mode='w', newline='') - writer = csv.DictWriter(fw, fieldnames=reader.fieldnames) - writer.writeheader() - writer.writerows(rows) - fw.close() + try: + fr = open(file, mode='r', newline='') + reader = csv.DictReader(fr) + rows = [row for row in reader] + rows = await self.update_rows(rows) + fr.close() + fw = open(file, mode='w', newline='') + writer = csv.DictWriter(fw, fieldnames=reader.fieldnames, extrasaction='ignore', restval=None) + writer.writeheader() + writer.writerows(rows) + fw.close() + except Exception as err: + logger.error(f'Error: {err}. Unable to read and update trade records') + + async def update_row(self, row: dict) -> dict: + """Update a single row of entered trade in the csv file with the actual profit. + + Args: + row: A dictionary from the dictionary writer object of the csv file. + + Returns: + dict: A dictionary with the actual profit and win status. + """ + try: + order = int(row['order']) + deals = await self.mt5.history_deals_get(position=order) + if not deals or len(deals) <= 1: + return row + deals = [deal for deal in deals if (deal.order != deal.position_id and deal.position_id == order + and deal.entry == 1)] + deals.sort(key=lambda x: x.time_msc) + deal = deals[-1] + row.update(actual_profit=deal.profit, win=deal.profit > 0, closed=True) + return row + except Exception as err: + logging.error(f'Error: {err}. Unable to update trade record') + return row async def update_rows(self, rows: list[dict]) -> list[dict]: """Update the rows of entered trades in the csv file with the actual profit. @@ -63,11 +93,14 @@ class Records: Returns: list[dict]: A list of dictionaries with the actual profit and win status. """ - tasks = [History(position=int(row['order'])).get_deals() for row in rows] - deals = [deal for deals in await asyncio.gather(*tasks) for deal in deals] - deals = {str(deal.position_id): deal.profit for deal in deals if deal.order != deal.position_id} - [row.update(actual_profit=(profit := deals[order]), win=profit > 0) for row in rows if (order := row['order']) in deals] - return rows + closed, unclosed = [], [] + for row in rows: + if (row.get('closed', 'FALSE')).title() == 'True': + closed.append(row) + else: + unclosed.append(row) + unclosed = await asyncio.gather(*[self.update_row(row) for row in unclosed]) + return closed + unclosed async def update_records(self): """Update trade records in the records_dir folder.""" @@ -76,4 +109,4 @@ class Records: async def update_record(self, file: Path | str): """Update a single trade record file.""" - await self.read_update(file) + await self.read_update(file) \ No newline at end of file diff --git a/src/aiomql/result.py b/src/aiomql/result.py index b8922f6..5b77003 100644 --- a/src/aiomql/result.py +++ b/src/aiomql/result.py @@ -17,7 +17,6 @@ class Result: name: Any desired name for the result file object """ config = Config() - data: dict def __init__(self, result: OrderSendResult, parameters: dict = None, name: str = ''): """ @@ -29,30 +28,28 @@ class Result: """ self.parameters = parameters or {} self.result = result - self.name = name or parameters.get('name', 'Strategy') + self.name = name or parameters.get('name', 'Trades') def get_data(self) -> dict: - result = self.result.get_dict(exclude={'retcode', 'retcode_external', 'request_id', 'request'}) - return self.parameters | result | {'actual_profit': 0, 'closed': False, 'win': False} + return (self.parameters | self.result.get_dict(exclude={'retcode', 'comment', 'retcode_external', 'request_id', 'request'}) + | {'actual_profit': 0, 'closed': False, 'win': False}) def to_csv(self): """Record trade results and associated parameters as a csv file """ try: - self.data = self.get_data() + data = self.get_data() file = self.config.records_dir / f"{self.name}.csv" exists = file.exists() with open(file, 'a', newline='') as fh: - writer = csv.DictWriter(fh, fieldnames=sorted(list(self.data.keys())), extrasaction='ignore', restval=None) + writer = csv.DictWriter(fh, fieldnames=sorted(list(data.keys())), extrasaction='ignore', restval=None) if not exists: writer.writeheader() - writer.writerow(self.data) + writer.writerow(data) except Exception as err: logger.error(f'Error: {err}. Unable to save trade results') async def save_csv(self): """Save trade results and associated parameters as a csv file in a separate thread """ - # exe = self.config.executor - loop = asyncio.get_running_loop() - loop.run_in_executor(None, self.to_csv) + self.to_csv() \ No newline at end of file diff --git a/src/aiomql/sessions.py b/src/aiomql/sessions.py index e1aeb31..4bbce6e 100644 --- a/src/aiomql/sessions.py +++ b/src/aiomql/sessions.py @@ -212,4 +212,4 @@ class Sessions: print(f'sleeping for {secs} seconds until next {current_session} session') await sleep(secs) self.current_session = current_session - await self.current_session.begin() + await self.current_session.begin() \ No newline at end of file diff --git a/src/aiomql/strategy.py b/src/aiomql/strategy.py index 40356b2..4976d2b 100644 --- a/src/aiomql/strategy.py +++ b/src/aiomql/strategy.py @@ -11,18 +11,19 @@ from .account import Account from .core import Config from .sessions import Sessions, Session -Symbol = TypeVar('Symbol', bound=_Symbol) +Symbol = TypeVar("Symbol", bound=_Symbol) class Strategy(ABC): """The base class for creating strategies. Attributes: + name (str): The name of the strategy. symbol (Symbol): The Financial Instrument as a Symbol Object parameters (Dict): A dictionary of parameters for the strategy. + sessions (Sessions): The sessions to use for the strategy. Class Attributes: - name (str): A name for the strategy. account (Account): Account instance. mt5 (MetaTrader): MetaTrader instance. config (Config): Config instance. @@ -30,12 +31,15 @@ class Strategy(ABC): Notes: Define the name of a strategy as a class attribute. If not provided, the class name will be used as the name. """ - name: str = '' + name: str + symbol: Symbol + sessions: Sessions account = Account() mt5: MetaTrader() config = Config() + _parameters = {} - def __init__(self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None): + def __init__(self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None, name=''): """Initiate the parameters dict and add name and symbol fields. Use class name as strategy name if name is not provided @@ -43,15 +47,26 @@ class Strategy(ABC): symbol (Symbol): The Financial instrument params (Dict): Trading strategy parameters """ + self.parameters = self._parameters | (params or {}) self.symbol = symbol - self.parameters = params.copy() if isinstance(params, dict) else {} - self.parameters['symbol'] = symbol.name - self.parameters['name'] = self.name or self.__class__.__name__ + self.name = name or self.__class__.__name__ + self.parameters["symbol"] = symbol.name + self.parameters["name"] = self.name self.sessions = sessions or Sessions(Session(start=0, end=dtime(hour=23, minute=59, second=59))) def __repr__(self): return f"{self.name}({self.symbol!r})" + def __getattr__(self, item): + if item in self.parameters: + return self.parameters[item] + raise AttributeError(f'{item} not an attribute of {self.name}') + + def __setattr__(self, key, value): + if key in self.__dict__.get('parameters', {}): + self.parameters[key] = value + super().__setattr__(key, value) + @staticmethod async def sleep(secs: float): """Sleep for the needed amount of seconds in between requests to the terminal. @@ -65,9 +80,8 @@ class Strategy(ABC): secs = secs - mod if mod != 0 else mod await asyncio.sleep(secs + 0.1) - @abstractmethod async def trade(self): """Place trades using this method. This is the main method of the strategy. - It will be called by the strategy runner. - """ + It will be called by the strategy runner. + """ \ No newline at end of file diff --git a/src/aiomql/symbol.py b/src/aiomql/symbol.py index 7ba1b75..cf069cf 100644 --- a/src/aiomql/symbol.py +++ b/src/aiomql/symbol.py @@ -79,11 +79,12 @@ class Symbol(SymbolInfo): Raises: ValueError: If request was unsuccessful and None was returned """ - info = await self.mt5.symbol_info(self.name) if info: - self.set_attributes(**info._asdict()) - return SymbolInfo(**info._asdict()) + info = info._asdict() + info['swap_rollover3days'] = info.get('swap_rollover3days', 0) % 7 + self.set_attributes(**info) + return SymbolInfo(**info) raise ValueError(f'Could not get info for {self.name}') async def init(self) -> bool: @@ -332,4 +333,4 @@ class Symbol(SymbolInfo): ticks = await self.mt5.copy_ticks_range(self.name, date_from, date_to, flags) if ticks is not None: return Ticks(data=ticks) - raise ValueError(f'Could not get ticks for {self.name}') + raise ValueError(f'Could not get ticks for {self.name}') \ No newline at end of file diff --git a/src/aiomql/trader.py b/src/aiomql/trader.py index a98d107..62ec5dd 100644 --- a/src/aiomql/trader.py +++ b/src/aiomql/trader.py @@ -1,5 +1,5 @@ """Trader class module. Handles the creation of an order and the placing of trades""" - +from abc import ABC, abstractmethod from datetime import datetime from typing import TypeVar from logging import getLogger @@ -14,21 +14,18 @@ from .utils import dict_to_string from .result import Result logger = getLogger(__name__) -Symbol = TypeVar('Symbol', bound=_Symbol) +Symbol = TypeVar("Symbol", bound=_Symbol) -class Trader: - """Base class for creating a Trader object. Handles the creation of an order and the placing of trades +class Trader(ABC): + """Base class for creating a Trader object. Handles the creation of an order and the placing of trades. Attributes: - symbol (Symbol): Financial instrument class Symbol class or any subclass of it. + symbol (Symbol): The financial instrument. ram (RAM): RAM instance order (Order): Trade order Class Attributes: - name (str): A name for the strategy. - account (Account): Account instance. - mt5 (MetaTrader): MetaTrader instance. config (Config): Config instance. """ config = Config() @@ -43,21 +40,13 @@ class Trader: self.symbol = symbol self.order = Order(symbol=symbol.name) self.ram = ram or RAM() - self.params = {} + self.parameters = {} - async def create_order(self, *, order_type: OrderType, **kwargs): - """Complete the order object with the required values. Creates a simple order. + @abstractmethod + async def create_order(self, *args, **kwargs): + """Complete the order object with the required values. Creates a simple order.""" - Args: - order_type (OrderType): Type of order - kwargs: keyword arguments as required for the specific trader - """ - points = kwargs.get('points', self.symbol.trade_stops_level+self.symbol.spread) - self.order.volume = await self.symbol.compute_volume() - self.order.type = order_type - await self.set_trade_stop_levels(points=points) - - async def set_order_limits(self, pips: float): + async def set_order_limits(self, *, pips: float): """Sets the stop loss and take profit for the order. This method uses pips as defined for forex instruments. Args: @@ -67,24 +56,32 @@ class Trader: sl, tp = pips, pips * self.ram.risk_to_reward tick = await self.symbol.info_tick() if self.order.type == OrderType.BUY: - self.order.sl, self.order.tp = tick.ask - sl, tick.ask + tp + self.order.sl, self.order.tp = round(tick.ask - sl, self.symbol.digits), round(tick.ask + tp, + self.symbol.digits) self.order.price = tick.ask elif self.order.type == OrderType.SELL: - self.order.sl, self.order.tp = tick.bid + sl, tick.bid - tp + self.order.sl, self.order.tp = round(tick.bid + sl, self.symbol.digits), round(tick.bid - tp, + self.symbol.digits) self.order.price = tick.bid else: raise ValueError(f"Invalid order type: {self.order.type}") async def set_trade_stop_levels(self, *, points): - """Set the stop loss and take profit levels of the order based on the points.""" + """Set the stop loss and take profit levels of the order based on the points. + + Args: + points: Target points + """ points = points * self.symbol.point sl, tp = points, points * self.ram.risk_to_reward tick = await self.symbol.info_tick() if self.order.type == OrderType.BUY: - self.order.sl, self.order.tp = tick.ask - sl, tick.ask + tp + self.order.sl, self.order.tp = round(tick.ask - sl, self.symbol.digits), round(tick.ask + tp, + self.symbol.digits) self.order.price = tick.ask else: - self.order.sl, self.order.tp = tick.bid + sl, tick.bid - tp + self.order.sl, self.order.tp = round(tick.bid + sl, self.symbol.digits), round(tick.bid - tp, + self.symbol.digits) self.order.price = tick.bid async def check_order(self) -> bool: @@ -95,51 +92,41 @@ class Trader: """ check = await self.order.check() if check.retcode != 0: - logger.warning( - f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(check.get_dict(include={'comment', 'retcode'}), multi=True)}") + logger.warning(f"Symbol: {self.order.symbol}\nResult:\n" + f"{dict_to_string(check.get_dict(include={'comment', 'retcode'}), multi=True)}") return False return True async def send_order(self): + """Send the order to the broker.""" + parameters = self.parameters.copy() result = await self.order.send() if result.retcode != 10009: - logger.warning( - f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(result.get_dict(include={'comment', 'retcode'}), multi=True)}") + logger.warning(f"Symbol: {self.order.symbol}\nResult:\n" + f"{dict_to_string(result.get_dict(include={'comment', 'retcode'}), multi=True)}") return logger.info(f"Symbol: {self.order.symbol}\nOrder: {dict_to_string(result.dict, multi=True)}\n") - await self.record_trade(result) + await self.record_trade(result, parameters) + + async def record_trade(self, result: OrderSendResult, parameters: dict): + """Record the trade in a csv file. - async def record_trade(self, result: OrderSendResult): - """ - Record the trade in a csv file. Args: result (OrderSendResult): Result of the order send + parameters: parameters of the trading strategy used to place the trade """ if result.retcode != 10009 or not self.config.record_trades: return + params = parameters profit = await self.order.calc_profit() - params = self.params - params['expected_profit'] = profit + params["expected_profit"] = profit date = datetime.utcnow() - date = date.replace(tzinfo=ZoneInfo('UTC')) - params['date'] = date - params['time'] = date.timestamp() + date = date.replace(tzinfo=ZoneInfo("UTC")) + params["date"] = date + params["time"] = date.timestamp() res = Result(result=result, parameters=params) await res.save_csv() - async def place_trade(self, order_type: OrderType, params: dict = None, **kwargs): - """Places a trade based on the order_type. - - Args: - order_type (OrderType): Type of order - params: parameters of the trading strategy used to place the trade - kwargs: keyword arguments as required for the specific trader - """ - try: - await self.create_order(order_type=order_type, **kwargs) - if not await self.check_order(): - return - self.params |= params or {} - await self.send_order() - except Exception as err: - logger.error(f"{err}. Symbol: {self.order.symbol}\n {self.__class__.__name__}.place_trade") + @abstractmethod + async def place_trade(self, *args, **kwargs): + """Places a trade based on the order_type.""" \ No newline at end of file