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