This commit is contained in:
Ichinga Samuel
2024-05-05 00:08:57 +01:00
parent 9b22d253df
commit 1eeabd99fd
39 changed files with 1192 additions and 407 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+1
View File
@@ -17,6 +17,7 @@ pip install aiomql
- Helper classes for Bot Building. Easy to use and extend.
- Compatible with pandas-ta.
- Sample Pre-Built strategies
- Visualization of charts using matplotlib and mplfinance
- Manage Trading periods using Sessions
- Risk Management
- Run multiple bots concurrently with different accounts from the same broker or different brokers
+2
View File
@@ -3,6 +3,7 @@
- [Config](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/core/config.md)
- [Base](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/core/base.md)
- [Constants](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/core/constants.md)
- [TaskQueue](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/core/task_queue.md)
- [Models](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/core/models.md)
- [Bot_Builder](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/bot_builder.md)
- [Account](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/account.md)
@@ -14,6 +15,7 @@
- [Positions](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/postions.md)
- [RAM](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/ram.md)
- [Records](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/records.md)
- [TradeRecords](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/trade_records.md)
- [Result](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/result.md)
- [Session](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/sessions.md)
- [Sessions](https://github.com/Ichinga-Samuel/aiomql/blob/master/docs/sessions.md)
+9 -8
View File
@@ -146,16 +146,17 @@ Removes it from the list of symbols if it was not successfully initialized or no
<a id='bb.run_bots'></a>
```python
@classmethod
def run_bots(cls, bots: dict[Callable: dict] = None, num_workers: int = None):
def run_bots(cls, funcs: dict[Callable: dict] = None, num_workers: int = None):
```
Run multiple bots at the same time. They will run in parallel. Using multiple bots is useful when you want to run
different strategies on different accounts. The callable should be a function that runs a bot instance and defines its
own Config instance within the function scope. The dictionary should contain the callable as the key and the dictionary
of keyword arguments to pass to the callable as the value. Use the path attribute of the config instance to specify the
terminal path of each account. The num_workers parameter specifies the number of workers to use. If not specified, the
number of workers will be the number of bots.
Run multiple functions (scripts, bots) at the same time in parallel with different accounts.
Running multiple functions is useful when you want to run different strategies on different accounts.
The callable can for example be a bot instance that defines its own Config instance within the function scope.
The dictionary should contain the callable as the key and the dictionary of keyword arguments to pass to the callable as
the value. Use the path attribute of the config instance to specify the terminal path of each account.
The num_workers parameter specifies the number of workers to use. If not specified, the number of workers will be the
number of bots.
#### Parameters
| Name | Type | Description |
|---------------|------------------------|---------------------------------------------------------------------------------|
| `bots` | `dict[Callable: dict]` | A dictionary of callables and their keyword arguments to run as bots |
| `funcs` | `dict[Callable: dict]` | A dictionary of callables and their keyword arguments to run as bots |
| `num_workers` | `int` | The number of workers to use. If not specified, the number of bots will be used |
+54
View File
@@ -84,6 +84,23 @@ A simple check to see if the candle is bearish.
|------|---------------|
| bool | True or False |
<a id="candle.dict"></a>
### dict
```python
def dict(self, exclude: set = None, include: set = None) -> Dict[str, Any]
```
Return a dictionary representation of the Candle object.
#### Parameters:
| Name | Type | Description |
|-----------|------------|-----------------------------------------------------------------------------|
| `exclude` | `set[str]` | A set of attributes to exclude from the dictionary. |
| `include` | `set[str]` | A set of attributes to include in the dictionary. |
#### Returns:
| Type | Description |
|---------------|--------------------------------------------------|
| `Dict[str, Any]` | A dictionary representation of the Candle object.|
### <a id="candles"></a> Candles
```python
@@ -186,3 +203,40 @@ Rename columns of the data object.
| Type | Description |
|-----------|---------------------------------------------------------------------------|
| `Candles` | A new instance of the class with the renamed columns if inplace is False. |
<id="candles.visualize"></a>
### visualize
```python
async def visualize(self, *, count: int = 50, type='candle', savefig: str | dict = None, addplot: dict = None,
style: str = 'charles', ylabel: str = 'Price', title: str = 'Chart', **kwargs)
```
Visualize the candles using the mplfinance library.
#### Parameters:
| Name | Type | Description | Default |
|----------|------------------|-----------------------------------------------------------------------------------------------|---------|
| `count` | `int` | The number of candles to visualize. | 50 |
| `type` | `str` | The type of chart to plot. | 'candle'|
| `savefig`| `str` or `dict` | The path to save the figure or a dictionary of keyword arguments to pass to the savefig method.| None |
| `addplot`| `dict` | A dictionary of keyword arguments to pass to the addplot method. | None |
| `style` | `str` | The style of the chart. | 'charles'|
| `ylabel` | `str` | The label of the y-axis. | 'Price' |
| `title` | `str` | The title of the chart. | 'Chart' |
| `kwargs` | `Any` | Additional keyword arguments to pass to the plot method. | |
<id="candles.make_addplot"></a>
```python
def make_addplot(self, *, count: int = 50, columns: list = None, **kwargs) -> dict
```
Make subplots for adding to the main plot.
#### Parameters:
| Name | Type | Description | Default |
|-----------|--------|-----------------------------------------------------------------------------------------------|---------|
| `count` | `int` | The number of candles to visualize. | 50 |
| `columns` | `list` | The columns to plot. | None |
| `kwargs` | `Any` | Additional keyword arguments to pass to the addplot method. | |
#### Returns:
| Type | Description |
|------|-----------------|
| dict | A makeplot dict |
+91
View File
@@ -0,0 +1,91 @@
# TaskQueue and QueueItem
## Table of Contents
- [QueueItem](#queue_item)
- [run](#run)
- [TaskQueue](#task_queue)
- [TaskQueue.add](#task_queue.add)
- [TaskQueue.add_task](#task_queue.add_task)
- [TaskQueue.worker](#task_queue.worker)
- [TaskQueue.start](#task_queue.start)
<a id="queue_item"></a>
### QueueItem
```python
class QueueItem:
def __init__(self, task: Callable | Awaitable, *args, **kwargs):
```
A task to be executed by the `TaskQueue`. The task can be a callable or an awaitable. The task is wrapped as a
`QueueItem` object, which is then added to the `TaskQueue` for execution. The arguments and keyword arguments are
passed to the task when it is executed. All parameters are created as attributes of the `QueueItem` object.
#### Parameters:
| Name | Type | Description |
|----------|---------------------------|-------------------------------------------------------------------|
| `task` | `Callable` \| `Awaitable` | A callable or awaitable task to be executed by the `TaskQueue` |
| `args` | `Any` | Positional arguments to be passed to the task when it is executed |
| `kwargs` | `Any` | Keyword arguments to be passed to the task when it is executed |
<a id="run"></a>
### run
```python
def run(self) -> Any
```
Run the task. If the task is a coroutine, it is awaited. If the task is a callable, it is called.
### TaskQueue
```python
class TaskQueue:
def __init__(self):
```
#### Attributes:
| Name | Type | Description |
|---------------|-----------------|---------------------------------------------------------------------------------|
| `queue` | `asyncio.Queue` | An asyncio.Queue queue of `QueueItem` objects to be executed by the `TaskQueue` |
<a id="task_queue.add"></a>
### add
```python
def add(self, item: QueueItem, *args, **kwargs) -> None
```
Add a `QueueItem` to the `TaskQueue` queue.
#### Parameters:
| Name | Type | Description |
|--------|-------------|----------------------------------------|
| `item` | `QueueItem` | A `QueueItem` to be added to the queue |
<a id="task_queue.add_task"></a>
### add_task
```python
def add_task(self, task: Callable | Awaitable, *args, **kwargs) -> None
```
Create a QueueItem from the task and add it to the `TaskQueue` queue. The task can be a callable or an awaitable.
The arguments and keyword arguments are passed to the QueueItem.
#### Parameters:
| Name | Type | Description |
|----------|---------------------------|-------------------------------------------------------------------|
| `task` | `Callable` \| `Awaitable` | A callable or awaitable task to be executed by the `TaskQueue` |
| `args` | `Any` | Positional arguments to be passed to the task when it is executed |
| `kwargs` | `Any` | Keyword arguments to be passed to the task when it is executed |
<a id="task_queue.worker"></a>
### worker
```python
async def worker(self) -> None
```
A worker that processes the `QueueItem` objects in the `TaskQueue` queue. The worker runs indefinitely, processing
`QueueItem` objects as they are added to the queue.
<a id="task_queue.start"></a>
### start
```python
def start(self) -> None
```
Start the worker that processes the `QueueItem` objects in the `TaskQueue` queue.
+96 -10
View File
@@ -5,8 +5,12 @@
- [\_\_init\_\_](#__init__)
- [init](#init)
- [get_deals](#get_deals)
- [get_deals_ticket](#get_deals_ticket)
- [get_deals_position](#get_deals_position)
- [deals_total](#deals_total)
- [get_orders](#get_orders)
- [get_orders_position](#get_orders_position)
- [get_order_ticket](#get_order_ticket)
- [orders_total](#orders_total)
<a id='history'></a>
@@ -68,13 +72,54 @@ Get history deals and orders
<a id='get_deals'></a>
### get_deals
```python
async def get_deals() -> list[TradeDeal]
async def get_deals(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
retries=3) -> list[TradeDeal]
```
Get deals from trading history using the parameters set in the constructor.
#### Returns
| Name | Type | Description | Default |
|---------|-------------------|-----------------------|---------|
| `deals` | `list[TradeDeal]` | A list of trade deals | [] |
#### Parameters:
| Name | Type | Description | Default |
|-------------|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| `date_from` | `datetime\|float` | Date the deals are requested from. Set by the 'datetime' object or as a number of seconds elapsed since 1970.01.01. Defaults to twenty-four hours from the current time in 'utc' | None |
| `date_to` | `datetime\|float` | Date up to which the deals are requested. Set by the 'datetime' object or as a number of seconds elapsed since 1970.01.01. Defaults to the current time in "utc" | None |
| `group` | `str` | Filter for selecting history by symbols. | "" |
| `retries` | `int` | Number of retries if the request fails. | 3 |
#### Returns:
| Name | Type | Description | Default |
|---------|--------------------|-----------------------|---------|
| `deals` | `tuple[TradeDeal]` | A list of trade deals | [] |
<a id='get_deals_ticket'></a>
### get_deals_ticket
```python
async def get_deals_ticket(self, *, ticket: int) -> tuple[TradeDeal]
```
Get deals by ticket number
#### Parameters:
| Name | Type | Description | Default |
|----------|------|-----------------------|---------|
| `ticket` | `int`| Ticket number to get | 0 |
#### Returns:
| Name | Type | Description | Default |
|---------|--------------------|-----------------------|---------|
| `deals` | `tuple[TradeDeal]` | A list of trade deals | [] |
<a id='get_deals_position'></a>
### get_deals_position
```python
async def get_deals_position(self, *, position: int) -> list[TradeDeal]
```
Get deals by position
#### Parameters:
| Name | Type | Description | Default |
|------------|------|-----------------------|---------|
| `position` | `int`| Position number to get | 0 |
#### Returns:
| Name | Type | Description | Default |
|---------|--------------------|-----------------------|---------|
| `deals` | `tuple[TradeDeal]` | A list of trade deals | [] |
<a id='deals_total'></a>
### deals_total
@@ -90,13 +135,54 @@ Get total number of deals within the specified period in the constructor.
<a id='get_orders'></a>
### get_orders
```python
async def get_orders() -> list[TradeOrder]
async def get_orders(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
retries=3) -> tuple[TradeOrder]
```
Get orders from trading history using the parameters set in the constructor.
#### Parameters
| Name | Type | Description | Default |
|-------------|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| `date_from` | `datetime\|float` | Date the deals are requested from. Set by the 'datetime' object or as a number of seconds elapsed since 1970.01.01. Defaults to twenty-four hours from the current time in 'utc' | None |
| `date_to` | `datetime\|float` | Date up to which the deals are requested. Set by the 'datetime' object or as a number of seconds elapsed since 1970.01.01. Defaults to the current time in "utc" | None |
| `group` | `str` | Filter for selecting history by symbols. | "" |
#### Returns
| Name | Type | Description | Default |
|----------|--------------------|------------------------|---------|
| `orders` | `list[TradeOrder]` | A list of trade orders | [] |
| Name | Type | Description | Default |
|----------|---------------------|------------------------|---------|
| `orders` | `tuple[TradeOrder]` | A list of trade orders | [] |
<a id='get_orders_position'></a>
### get_orders_position
```python
async def get_orders_position(self, *, position: int) -> tuple[TradeOrder]
```
Get orders by position.
#### Parameters
| Name | Type | Description | Default |
|------------|------|-----------------------|---------|
| `position` | `int`| Position number to get | 0 |
#### Returns
| Name | Type | Description | Default |
|----------|---------------------|------------------------|---------|
| `orders` | `tuple[TradeOrder]` | A list of trade orders | [] |
<a id='get_order_ticket'></a>
### get_order_ticket
```python
async def get_order_ticket(self, ticket: int | None = None) -> TradeOrder
```
Get a single order by ticket number.
#### Parameters
| Name | Type | Description | Default |
|----------|-------|----------------------|---------|
| `ticket` | `int` | Ticket number to get | 0 |
#### Returns
| Name | Type | Description | Default |
|----------|---------------------|------------------------|---------|
| `order` | `TradeOrder` | A single trade order | None |
<a id='orders_total'></a>
### orders_total
+8
View File
@@ -4,6 +4,7 @@
- [Order](#Order)
- [\_\_init\_\_](#__init__)
- [orders_total](#orders_total)
- [get_order](#get_order)
- [get_orders](#get_orders)
- [check](#check)
- [send](#send)
@@ -42,6 +43,13 @@ Get the total number of active orders.
|-------|-------------------------------|
| `int` | total number of active orders |
<a id="get_order"></a>
### get_order
```python
async def get_order(self, ticket: int) -> TradeOrder
```
Get an active trade order by ticket.
<a id="get_orders"></a>
### get_orders
```python
+29
View File
@@ -5,9 +5,11 @@
- [Attributes](#attributes)
- [\_\_init\_\_](#__init__)
- [positions_total](#positions_total)
- [position_get](#position_get)
- [positions_get](#positions_get)
- [close](#close)
- [close_by](#close_by)
- [close_position](#close_position)
- [close_all](#close_all)
<a id="positions"></a>
@@ -67,6 +69,21 @@ Get open positions with the ability to filter by symbol or ticket.
|-----------------------|--------------------------------|
| `list[TradePosition]` | A list of open trade positions |
<a id="position_get"></a>
### position_get
```python
async def position_get(self, *, ticket: int) -> TradePosition
```
Get a position by ticket number.
#### Arguments
| Name | Type | Description |
|----------|-------|-----------------|
| `ticket` | `int` | Position ticket |
#### Returns
| Type | Description |
|-----------------|----------------|
| `TradePosition` | Trade position |
<a id="close"></a>
### close
@@ -88,12 +105,24 @@ Close a position by ticket number.
```python
async def close_by(self, pos: TradePosition):
```
Close a position by position object.
#### Arguments
| Name | Type | Description |
|-------|-----------------|-----------------|
| `pos` | `TradePosition` | Position object |
<a id='close_position'></a>
### close_position
```python
async def close_position(self, *, position: TradePosition):
```
Close a position by position object.
#### Arguments
| Name | Type | Description |
|------------|-----------------|-----------------|
| `position` | `TradePosition` | Position object |
<a id="close_all"></a>
### close_all
```python
+12 -11
View File
@@ -5,7 +5,7 @@
- [\_\_init\_\_](#__init__)
- [get\_amount](#get_amount)
- [check_losing_positions](#check_losing_positions)
- [check_balance_level](#check_balance_level)
- [check_risk_level](#check_balance_level)
<a id="RAM"></a>
### RAM
@@ -14,16 +14,17 @@ class RAM
```
Risk Assessment and Management. You can customize this class based on how you want to manage risk.
#### Attributes
| Name | Type | Description | Default |
|------------------|---------|------------------------------------------------------|---------|
| `risk_to_reward` | `float` | Risk to reward ratio | 1 |
| `risk` | `float` | Percentage of account balance to risk per trade | |
| `points` | `float` | A fixed number of points per trade can be fixed here | |
| `pips` | `float` | A fixed number of pips per trade can be fixed here | |
| `min_amount` | `float` | Minimum amount to risk per trade | |
| `max_amount` | `float` | Maximum amount to risk per trade | |
| `balance_level` | `float` | Ratio of margin to available balance as a percentage | 10 |
| `loss_limit` | `int` | Number of open losing trades to allow at any time | 3 |
| Name | Type | Description | Default |
|------------------|---------|--------------------------------------------------------|---------|
| `risk_to_reward` | `float` | Risk to reward ratio | 1 |
| `risk` | `float` | Percentage of account balance to risk per trade | |
| `points` | `float` | A fixed number of points per trade can be fixed here | |
| `pips` | `float` | A fixed number of pips per trade can be fixed here | |
| `min_amount` | `float` | Minimum amount to risk per trade | |
| `max_amount` | `float` | Maximum amount to risk per trade | |
| `risk_level` | `float` | Ratio of free margin to current equity as a percentage | 50 |
| `loss_limit` | `int` | Number of open losing trades to allow at any time | 3 |
<a id="__init__"></a>
### \_\_init\_\_
+5 -5
View File
@@ -17,7 +17,7 @@ class Records()
This utility class read trade records from csv files, and update them based on their closing positions. To use this default
implementation the csv files should at least have the following columns `['order', 'symbol', 'actual_profit', 'win', 'closed']`
Once a trade have been closed, the actual profit and win status will be updated in the csv file.
#### Headers
#### Default Headers
| column | type | description |
|---------------|-------|-------------------------------------------------------|
| order | int | Order id of the trade |
@@ -36,8 +36,8 @@ Once a trade have been closed, the actual profit and win status will be updated
```python
def __init__(records_dir: Path | str = '')
```
Initialize the Records class.
#### Arguments
Initialize an instance of the class
#### Parameters
| name | type | description |
|--------------|------|----------------------------------------------------------------|
| records_dir | Path | Absolute path to directory containing record of placed trades. |
@@ -59,7 +59,7 @@ Get trade records from records_dir folder
async def read_update(file: Path)
```
Read and update trade records
#### Arguments
#### Parameters
| name | type | description |
|------|------|-------------------|
| file | Path | Trade record file |
@@ -70,7 +70,7 @@ Read and update trade records
async def update_rows(rows: list[dict]) -> list[dict]
```
Update the rows of entered trades in the csv file with the actual profit.
#### Arguments
#### Parameters
| name | type | description |
|------|------------|---------------------------------------------------------------------------|
| rows | list[dict] | A list of dictionaries from the dictionary writer object of the csv file. |
+10
View File
@@ -5,6 +5,7 @@
- [__init__](#__init__)
- [get_data](#get_data)
- [to_csv](#to_csv)
- [to_json](#to_json)
<a id="result"></a>
```python
@@ -33,6 +34,7 @@ Prepare result data for record keeping and analysis.
| `name` | `str` | The name of the result object |
<a id="get_data"></a>
### get\_data
```python
def get_data(self) -> dict:
```
@@ -48,3 +50,11 @@ Get the result data as a dictionary
async def to_csv()
```
Record trade results and associated parameters as a csv file
<a id="to_json"></a>
### to\_json
```python
async def to_json()
```
Record trade results and associated parameters as a json file
```
+127
View File
@@ -0,0 +1,127 @@
# Trade Records
## Table of contents
- [Trade Records](#trade_records)
- [\_\_init\_\_](#__init__)
- [get_csv_records](#get_csv_records)
- [get_json_records](#get_json_records)
- [read_update_csv](#read_update_csv)
- [read_update_json](#read_update_json)
- [update_rows](#update_rows)
- [update_row](#update_row)
- [update_csv_records](#update_csv_records)
- [update_json_records](#update_json_records)
- [update_csv_record](#update_csv_record)
- [update_json_record](#update_json_record)
<a id="trade_records"></a>
### Trade Records
```python
class TradeRecords()
```
This utility class read trade records from csv and json files, and update them based on their closing positions.
To use this default implementation the csv or json file should be able to provide the following data.
`['order', 'symbol', 'actual_profit', 'win', 'closed']`
Once a trade have been closed, the actual profit and win status will be updated in the csv file.
#### Default Headers
| column | type | description |
|---------------|-------|-------------------------------------------------------|
| order | int | Order id of the trade |
| symbol | str | the name of the Symbol |
| actual_profit | float | The actual profit of the trade, this zero by default |
| win | bool | The win status of the trade, this is False by default |
| closed | bool | The status of the trade, this is False by default |
#### Attributes
| name | type | description |
|-------------|--------|--------------------------------------------------------------|
| records_dir | Path | Absolut path to directory containing record of placed trades |
| config | Config | Config object |
<a id="__init__"></a>
### \_\_init\_\_
```python
def __init__(records_dir: Path | str = '')
```
Initialize an instance of the class.
#### Parameters
| name | type | description |
|--------------|------|----------------------------------------------------------------|
| records_dir | Path | Absolute path to directory containing record of placed trades. |
<a id="get_csv_records"></a>
### get_csv_records
```python
async def get_csv_records()
```
Get trade records from records_dir folder.
#### Yields
| type | description |
|------|--------------------|
| Path | Trade record files |
<a id="get_json_records"></a>
### get_json_records
```python
async def get_json_records()
```
Get trade records from records_dir folder.
#### Yields
| type | description |
|------|--------------------|
| Path | Trade record files |
<a id="read_update_csv"></a>
### read_update_csv
```python
async def read_update_csv(file: Path)
```
Read and update trade records from a csv file.
#### Parameters
| name | type | description |
|------|------|-------------------|
| file | Path | Trade record file |
<a id="read_update_json"></a>
### read_update_json
```python
async def read_update_json(file: Path)
```
Read and update trade records from a json file.
#### Parameters
| name | type | description |
|------|------|-------------------|
| file | Path | Trade record file |
<a id="update_rows"></a>
### update_rows
```python
async def update_rows(rows: list[dict]) -> list[dict]
```
Update the rows of entered trades with the actual profit.
#### Parameters
| name | type | description |
|------|------------|---------------------------------------------------------------------------|
| rows | list[dict] | A list of dictionaries from the dictionary writer object of the csv file. |
#### Returns
| type | description |
|------------|---------------------------------------------------------------|
| list[dict] | A list of dictionaries with the actual profit and win status. |
<a id="update_row"></a>
### update_row
```python
async def update_row(row: dict) -> dict
```
Update the row of an entered trade with the actual profit.
#### Parameters
| name | type | description |
|------|------|-------------------------------------------|
| row | dict | A dictionary from the csv file row object |
#### Returns
| type | description |
|------|------------------------------------|
| dict | A dictionary with the actual profit |
+8 -5
View File
@@ -93,13 +93,16 @@ Checks the status of the order before placing the trade.
<a name="record_trade"></a>
### record_trade
```python
async def record_trade(result: OrderSendResult)
async def record_trade(result: OrderSendResult, parameters: dict = None, name: str = '', exclude: set = None)
```
Records the trade and the order details if `Config.record_trades` is true.
Records the trade and the order details if `Config.record_trades` is true. Trades are recorded as either json or csv.
#### Parameters
| Name | Type | Description | Default |
|----------|-------------------|--------------------------------|---------|
| `result` | `OrderSendResult` | The result of the placed order | None |
| Name | Type | Description | Default |
|--------------|-------------------|--------------------------------------------------------------|---------|
| `result` | `OrderSendResult` | The result of the placed order | None |
| `parameters` | `dict` | parameters to saved instead of the ones in `self.parameters` | None |
| `name` | `str` | Name for the csv or json file | '' |
| `exclude` | `set` | Set of keys to exclude from the saved parameters | None |
<a name="place_trade"></a>
### place\_trade
-40
View File
@@ -1,40 +0,0 @@
from datetime import time
import logging
from aiomql import Bot, ForexSymbol, FingerTrap, Session, Sessions, RAM, SimpleTrader, TimeFrame
logging.basicConfig(level=logging.INFO)
def build_bot():
bot = Bot()
# create sessions for the strategies
london = Session(name='London', start=8, end=time(hour=15, minute=30), on_end='close_all')
new_york = Session(name='New York', start=13, end=time(hour=20, minute=30))
tokyo = Session(name='Tokyo', start=23, end=time(hour=6, minute=30))
# configure the parameters and the trader for a strategy
params = {'trend_candles_count': 500, 'fast_period': 8, 'slow_period': 34, 'entry_timeframe': TimeFrame.M5}
gbpusd = ForexSymbol(name='GBPUSD')
st1 = FingerTrap(symbol=gbpusd, params=params,
trader=SimpleTrader(symbol=gbpusd, ram=RAM(risk=0.05, risk_to_reward=2)),
sessions=Sessions(london, new_york))
# use the default for the other strategies
st2 = FingerTrap(symbol=ForexSymbol(name='AUDUSD'), sessions=Sessions(tokyo, new_york))
st3 = FingerTrap(symbol=ForexSymbol(name='USDCAD'), sessions=Sessions(new_york))
st4 = FingerTrap(symbol=ForexSymbol(name='USDJPY'), sessions=Sessions(tokyo))
st5 = FingerTrap(symbol=ForexSymbol(name='EURGBP'), sessions=Sessions(london))
# sessions are not required
st6 = FingerTrap(symbol=ForexSymbol(name='EURUSD'))
# add strategies to the bot
bot.add_strategies([st1, st2, st3, st4, st5, st6])
bot.execute()
# run the bot
build_bot()
-52
View File
@@ -1,52 +0,0 @@
import asyncio
from aiomql import Symbol, TimeFrame, Account, Candle, Candles
async def main():
"""Example of using the Candle and Candles classes.
The candle class is a single price bar. Holding the OHLCV data for a single price bar.
The Candles class is a container of Candle objects. It is an Iterable of Candle objects.
It can be sliced and indexed. It can also be accessed with keywords.
It is a wrapper around a pandas DataFrame. Which is what it uses to store the data.
"""
async with Account():
sym = Symbol(name="EURUSD")
# Get EURUSD price bars for the past 48 hours
candles: Candles = await sym.copy_rates_from_pos(timeframe=TimeFrame.H1, count=48, start_position=0)
# get size of candles
print(len(candles)) # 48
# get the latest candle by accessing the last one.
last: Candle = candles[-1] # A Candle object
print(type(last))
print(last.Index)
# slicing returns a Candles object
half = candles[24:]
print(type(half))
print(len(half))
close = candles['close'] # close price of all the candles as a pandas series
print(type(close))
print(close)
# compute ema using pandas ta
candles.ta.ema(length=34, append=True, fillna=0)
# rename the column to ema
candles.rename(EMA_34='ema')
# use talib to compute crossover. This returns a series object that is not part of the candles object.
closeXema = candles.ta_lib.cross(candles.close, candles.ema)
# add to the candles
candles['closeXema'] = closeXema
print(candles)
# iterate over the first 5 candles
for candle in candles[:5]:
print(candle.open, candle.Index)
asyncio.run(main())
-37
View File
@@ -1,37 +0,0 @@
import asyncio
from aiomql import Account, OrderType, TradeAction, Order, ForexSymbol
async def main():
async with Account():
# create a symbol
sym = ForexSymbol(name="EURUSD-T")
# Confirm the symbol is available for this account and initialize with default values.
res = await sym.init()
# I want to place a market buy order, risk only 2usd, and target 10 pips in this trade.
# The ForexSymbol object has a compute_volume method that can be used to compute the volume
# given a target pips and amount.
volume = await sym.compute_volume(amount=2, points=100)
# a risk to reward ratio of 1:2
# get the price tick of the symbol
tick = await sym.info_tick()
sl = tick.ask - (10 * sym.pip)
tp = tick.ask + (20 * sym.pip)
# create order
order = Order(symbol=sym.name, type=OrderType.BUY, volume=volume, action=TradeAction.DEAL,
price=tick.ask, sl=sl, tp=tp)
# check order. returns an OrderCheckResult object
chk = await order.check()
print(chk)
# send order returns an OrderSendResult object
res = await order.send()
print(res)
asyncio.run(main())
-63
View File
@@ -1,63 +0,0 @@
import logging
import asyncio
from datetime import datetime
from aiomql import ForexSymbol, Account, Positions, History, SimpleTrader as Trader, OrderType, RAM
logging.basicConfig(level=logging.INFO)
async def main():
# Account details are in the aiomql.json file
async with Account():
# get start time
start = datetime.now()
# create two symbols and initialize them
sym1 = ForexSymbol(name="EURUSD-T")
sym2 = ForexSymbol(name="GBPUSD-T")
await sym1.init()
await sym2.init()
# Risk Assets Management instance
# fix the amount to be risked at 2 USD. USD is the account currency.
ram = RAM(amount=2, points=100)
# Create two traders instance
trd = Trader(symbol=sym1, ram=ram)
trd2 = Trader(symbol=sym2, ram=ram)
# Place Trades
await trd.place_trade(order_type=OrderType.SELL)
await trd2.place_trade(order_type=OrderType.BUY)
# Create a Positions object
pos = Positions(group='*USD*')
# get the number of open positions
total = await pos.positions_total()
print(f'{total} Open positions') # 2
# close all open positions
await pos.close_all()
end = datetime.now()
# get the number of open positions
total = await pos.positions_total()
print(f'{total} Open positions')
# get historical trades
start = datetime(day=start.day-1, month=start.month, year=start.year, hour=start.hour, minute=0, second=0)
his = History(date_from=start.timestamp(), date_to=end.timestamp())
# get the number of order
orders = await his.orders_total()
print(f'{orders} orders')
# get the number of deals
# total_deals = await his.deals_total()
# print(f'{total_deals} Deals')
asyncio.run(main())
-7
View File
@@ -1,7 +0,0 @@
actual_profit,ask,bid,closed,date,deal,ecc,entry_ema,etf,expected_profit,fast_ema,name,order,price,slow_ema,symbol,tcc,time,ttf,volume,win
0,9213.42,9213.19,False,2024-02-11,1950149753,3360,5,M5,1.97,8,FingerTrap,5052174005,9213.42,20,Volatility 10 (1s) Index,672,22:15:53.721625,H1,0.56,False
0,9209.47,9209.24,False,2024-02-11,1950153282,3360,5,M5,1.46,8,FingerTrap,5052177651,9209.47,20,Volatility 10 (1s) Index,672,22:27:18.718854,H1,0.77,False
0,250524.34,250470.34,False,2024-02-11,1950153281,3360,5,M5,1.16,8,FingerTrap,5052177650,250524.34,20,Volatility 75 Index,672,22:27:18.424899,H1,0.001,False
0,2013.326,2013.201,False,2024-02-11,1950156825,3360,5,M5,1.45,8,FingerTrap,5052181298,2013.201,20,Volatility 25 Index,672,22:38:15.979751,H1,0.86,False
0,8620.27,8618.53,False,2024-02-11,1950156826,3360,5,M5,1.46,8,FingerTrap,5052181299,8618.53,20,Volatility 75 (1s) Index,672,22:38:16.244947,H1,0.109,False
0,2018.152,2018.027,False,2024-02-12,1950193973,3360,5,M5,1.44,8,FingerTrap,5052218853,2018.152,20,Volatility 25 Index,672,01:00:00.730605,H1,1.48,False
1 actual_profit ask bid closed date deal ecc entry_ema etf expected_profit fast_ema name order price slow_ema symbol tcc time ttf volume win
2 0 9213.42 9213.19 False 2024-02-11 1950149753 3360 5 M5 1.97 8 FingerTrap 5052174005 9213.42 20 Volatility 10 (1s) Index 672 22:15:53.721625 H1 0.56 False
3 0 9209.47 9209.24 False 2024-02-11 1950153282 3360 5 M5 1.46 8 FingerTrap 5052177651 9209.47 20 Volatility 10 (1s) Index 672 22:27:18.718854 H1 0.77 False
4 0 250524.34 250470.34 False 2024-02-11 1950153281 3360 5 M5 1.16 8 FingerTrap 5052177650 250524.34 20 Volatility 75 Index 672 22:27:18.424899 H1 0.001 False
5 0 2013.326 2013.201 False 2024-02-11 1950156825 3360 5 M5 1.45 8 FingerTrap 5052181298 2013.201 20 Volatility 25 Index 672 22:38:15.979751 H1 0.86 False
6 0 8620.27 8618.53 False 2024-02-11 1950156826 3360 5 M5 1.46 8 FingerTrap 5052181299 8618.53 20 Volatility 75 (1s) Index 672 22:38:16.244947 H1 0.109 False
7 0 2018.152 2018.027 False 2024-02-12 1950193973 3360 5 M5 1.44 8 FingerTrap 5052218853 2018.152 20 Volatility 25 Index 672 01:00:00.730605 H1 1.48 False
-40
View File
@@ -1,40 +0,0 @@
import asyncio
from datetime import datetime
from aiomql import ForexSymbol, TimeFrame, Account, Config
config = Config()
async def main():
async with Account():
sym = ForexSymbol(name="EURUSD-T")
res = await sym.init()
if not res:
print('Symbol not available')
return
# get the last 1000 rates.
# data is returned as a Candles object
candles = await sym.copy_rates_from_pos(timeframe=TimeFrame.H1, count=1000, start_position=0)
print(len(candles)) # 1000
# get candles of the last 24 hours
today = datetime.now()
yesterday = today.replace(day=today.day - 1)
rates = await sym.copy_rates_range(timeframe=TimeFrame.H1, date_from=yesterday, date_to=today)
print(len(rates)) # 24
# get price ticks for the last 24 hours
# data is returned as a Ticks object
ticks = await sym.copy_ticks_range(date_from=yesterday, date_to=today)
print(len(ticks)) # ??
# get the current price tick
tick = await sym.info_tick()
# ask and bid price
ask, bid = tick.ask, tick.bid
print(ask, bid)
asyncio.run(main())
+3 -3
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aiomql"
version = "3.20"
version = "3.21"
readme = "README.md"
requires-python = ">=3.11"
classifiers = [
@@ -16,9 +16,9 @@ classifiers = [
"Operating System :: OS Independent",
]
keywords = ['MetaTrader5', 'Asynchronous', 'Algorithmic Trading', 'Trading Bot']
dependencies = ["MetaTrader5>=5.0.37", "pandas>=1.5.0", "pandas-ta>=0.3.14b0"]
dependencies = ["MetaTrader5>=5.0.37", "pandas>=1.5.0", "pandas-ta>=0.3.14b0", "matplotlib>=3.8.4", "mplfinance>=0.12.10b0"]
authors = [{name = "Ichinga Samuel", email = "ichingasamuel@gmail.com"}]
description = "Asynchronous MetaTrader5 library and Bot Building Framework"
description = "Asynchronous MetaTrader5 library and Algorithmic Trading Framework"
[project.urls]
"Homepage" = "https://github.com/Ichinga-Samuel/aiomql"
+154 -3
View File
@@ -1,3 +1,154 @@
MetaTrader5~=5.0.45
pandas~=2.1.1
setuptools~=65.5.1
anyio==4.3.0
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
arrow==1.3.0
asttokens==2.4.1
async-lru==2.0.4
attrs==23.2.0
Babel==2.14.0
beautifulsoup4==4.12.3
black==23.9.1
bleach==6.1.0
build==1.0.3
certifi==2023.7.22
cffi==1.16.0
charset-normalizer==3.3.0
click==8.1.7
colorama==0.4.6
comm==0.2.2
contourpy==1.2.1
cycler==0.12.1
databind.core==4.4.1
databind.json==4.4.1
debugpy==1.8.1
decorator==5.1.1
defusedxml==0.7.1
Deprecated==1.2.14
docspec==2.2.1
docspec-python==2.2.1
docstring-parser==0.11
docutils==0.20.1
executing==2.0.1
fastjsonschema==2.19.1
fonttools==4.51.0
fqdn==1.5.1
h11==0.14.0
httpcore==1.0.5
httpx==0.27.0
idna==3.4
importlib-metadata==6.8.0
iniconfig==2.0.0
ipykernel==6.29.4
ipython==8.23.0
ipywidgets==8.1.2
isoduration==20.11.0
jaraco.classes==3.3.0
jedi==0.19.1
Jinja2==3.1.2
json5==0.9.24
jsonpointer==2.4
jsonschema==4.21.1
jsonschema-specifications==2023.12.1
jupyter==1.0.0
jupyter-console==6.6.3
jupyter-events==0.10.0
jupyter-lsp==2.2.5
jupyter_client==8.6.1
jupyter_core==5.7.2
jupyter_server==2.13.0
jupyter_server_terminals==0.5.3
jupyterlab==4.1.6
jupyterlab_pygments==0.3.0
jupyterlab_server==2.26.0
jupyterlab_widgets==3.0.10
keyring==24.2.0
kiwisolver==1.4.5
markdown-it-py==3.0.0
MarkupSafe==2.1.3
matplotlib==3.8.4
matplotlib-inline==0.1.6
mdurl==0.1.2
MetaTrader5==5.0.45
mistune==3.0.2
more-itertools==10.1.0
mplfinance==0.12.10b0
mypy-extensions==1.0.0
nbclient==0.10.0
nbconvert==7.16.3
nbformat==5.10.4
nest-asyncio==1.6.0
nh3==0.2.14
notebook==7.1.2
notebook_shim==0.2.4
nr-date==2.1.0
nr-stream==1.1.5
nr.util==0.8.12
numpy==1.26.0
overrides==7.7.0
packaging==23.2
pandas==2.1.1
pandas-ta==0.3.14b0
pandocfilters==1.5.1
parso==0.8.4
pathspec==0.11.2
pillow==10.3.0
pkginfo==1.9.6
platformdirs==3.11.0
pluggy==1.3.0
prometheus_client==0.20.0
prompt-toolkit==3.0.43
psutil==5.9.8
pure-eval==0.2.2
pycparser==2.22
pydoc-markdown==4.8.2
Pygments==2.16.1
pyparsing==3.1.2
pyproject_hooks==1.0.0
pytest==7.4.4
python-dateutil==2.8.2
python-json-logger==2.0.7
python-telegram-bot==21.0.1
pytz==2023.3.post1
pywin32==306
pywin32-ctypes==0.2.2
pywinpty==2.0.13
PyYAML==6.0.1
pyzmq==25.1.2
qtconsole==5.5.1
QtPy==2.4.1
readme-renderer==42.0
referencing==0.34.0
requests==2.31.0
requests-toolbelt==1.0.0
rfc3339-validator==0.1.4
rfc3986==2.0.0
rfc3986-validator==0.1.1
rich==13.6.0
rpds-py==0.18.0
Send2Trash==1.8.3
six==1.16.0
sniffio==1.3.1
soupsieve==2.5
stack-data==0.6.3
terminado==0.18.1
tinycss2==1.2.1
tomli==2.0.1
tomli_w==1.0.0
tornado==6.4
traitlets==5.14.2
twine==4.0.2
typeapi==2.1.1
types-python-dateutil==2.9.0.20240316
typing_extensions==4.6.3
tzdata==2023.3
uri-template==1.3.0
urllib3==2.0.6
watchdog==3.0.0
wcwidth==0.2.13
webcolors==1.13
webencodings==0.5.1
websocket-client==1.7.0
widgetsnbextension==4.0.10
wrapt==1.15.0
yapf==0.40.2
zipp==3.17.0
+1
View File
@@ -6,6 +6,7 @@ from .strategy import Strategy
from .bot_builder import Bot
from .result import Result
from .records import Records
from .trade_records import TradeRecords
from .candle import Candle, Candles
from .positions import Positions
from .executor import Executor
+1 -1
View File
@@ -83,7 +83,7 @@ class Account(AccountInfo):
if ini and res:
return True
else:
await asyncio.sleep(tries)
await asyncio.sleep(5+tries)
return await self._login(acc=acc, tries=tries-1)
def has_symbol(self, symbol: str | SymbolInfo):
+9 -4
View File
@@ -37,11 +37,16 @@ class Bot:
self.executor = Executor()
@classmethod
def run_bots(cls, bots: dict[Callable: dict] = None, num_workers: int = None):
"""Run multiple bots at the same time."""
num_workers = num_workers or len(bots) * 2
def run_bots(cls, funcs: dict[Callable: dict] = None, num_workers: int = None):
"""Run multiple scripts or bots in parallel with different accounts.
Args:
funcs (dict): A dictionary of functions to run with their respective keyword arguments as a dictionary
num_workers (int): Number of workers to run the functions
"""
num_workers = num_workers or len(funcs) * 2
with ProcessPoolExecutor(max_workers=num_workers) as executor:
for bot, kwargs in bots.items():
for bot, kwargs in funcs.items():
executor.submit(bot, **kwargs)
async def initialize(self):
+42 -13
View File
@@ -4,7 +4,9 @@ from typing import Type, TypeVar, Generic, Iterable
from logging import getLogger
from pandas import DataFrame, Series
import pandas as pd
import pandas_ta as ta
import mplfinance as mplt
from .core.constants import TimeFrame
@@ -12,7 +14,7 @@ logger = getLogger(__name__)
class Candle:
"""A class representing bars from the MetaTrader 5 terminal as a customized class analogous to Japanese
"""A customized class representing rates from the MetaTrader 5 terminal analogous to Japanese
Candlesticks. You can subclass this class for added customization.
Attributes:
@@ -25,7 +27,6 @@ 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
open: float
@@ -36,7 +37,6 @@ class Candle:
spread: float
tick_volume: float
Index: int
mid: float
def __init__(self, **kwargs):
"""Create a Candle object from keyword arguments. This class must always be instantiated with open, high, low
@@ -49,17 +49,15 @@ class Candle:
raise ValueError("Candle must be instantiated with open, high, low and close prices")
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):
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}
return ("%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)"
% {"class": self.__class__.__name__, "open": self.open, "high": self.high,
"low": self.low, "close": self.close, "time": self.time, 'Index': self.Index})
def __str__(self):
return self.dict()
return str(self.dict())
def __eq__(self, other: "Candle"):
return self.time == other.time
@@ -152,7 +150,6 @@ class Candles(Generic[_Candle]):
tick_volume: Series
real_volume: Series
spread: Series
mid: Series
Candle: Type[Candle]
timeframe: TimeFrame
_data: DataFrame
@@ -177,9 +174,6 @@ class Candles(Generic[_Candle]):
raise ValueError(f"Cannot create DataFrame from object of {type(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):
@@ -268,3 +262,38 @@ class Candles(Generic[_Candle]):
"""
res = self._data.rename(columns=kwargs, inplace=inplace)
return self if inplace else self.__class__(data=res)
def make_addplot(self, *, count: int = 50, columns: list = None, **kwargs) -> dict:
"""
Make subplots for adding to the main plot
Args:
count (int): The numbers of candles to make the addplot for. Defaults to 50.
columns (list[str]): The columns to make the plot from. Defaults to None.
**kwargs: Valid arguments for the mplfinance make_addplot function
"""
columns = columns or []
data = self._data[-count:]
data.index = pd.to_datetime(data['time'], unit='s')
return mplt.make_addplot(data[columns], **kwargs)
def visualize(self, *, count: int = 50, type='candle', savefig: str | dict = None, addplot: dict = None,
style: str = 'charles', ylabel: str = 'Price', title: str = 'Chart', **kwargs):
"""Visualize the candles using the mplfinance library.
Args:
count (int): The number of candles to visualize, counting from behind, i.e the most recent candles.
Defaults to 50.
type: Type of chart, defaults to candle
savefig (str|dict): The path to save the figure or a dictionary of parameters to pass to the savefig method.
addplot: Additional plots to add to the chart. Defaults to None. They should match the dimension of the
original data which is specified via the count parameter.
style (str): The style of the chart. Defaults to 'charles'.
ylabel (str): The label of the y-axis. Defaults to 'Price'.
title (str): The title of the chart. Defaults to 'Chart'.
kwargs: valid kwargs for the plot function.
"""
kwargs |= {key: arg for key, arg in (('savefig', savefig), ('addplot', addplot), ('style', style),
('ylabel', ylabel), ('title', title), ('type', type)) if arg}
data = self._data[-count:]
data.index = pd.to_datetime(data['time'], unit='s')
mplt.plot(data, **kwargs)
+37 -24
View File
@@ -1,12 +1,13 @@
import os
from pathlib import Path
from typing import Iterator
from typing import Iterator, Literal, TypeVar
import json
from logging import getLogger
from .task_queue import TaskQueue
logger = getLogger(__name__)
Bot = TypeVar("Bot")
class Config:
@@ -14,6 +15,7 @@ class Config:
Attributes:
record_trades (bool): Whether to keep record of trades or not.
trade_record_mode: How to save trade, json or csv. Defaults to json
filename (str): Name of the config file
records_dir (str): Path to the directory where trade records are saved
login (int): Trading account number
@@ -31,20 +33,21 @@ class Config:
By passing reload=True to the load_config method, you can reload and search again for the config file.
"""
login: int = 0
trade_record_mode: Literal['csv', 'json'] = 'csv'
password: str = ""
server: str = ""
path: str | Path = ""
timeout: int = 60000
record_trades: bool = True
filename: str = "aiomql.json"
win_percentage: float = 0.85
records_dir: str | Path = 'records'
config_dir: str = ''
_initialize = True
state: dict = {}
root_dir: Path = Path('.').absolute().resolve()
root: Path
root_dir: Path
records_dir: Path
config_dir: str = ''
task_queue: TaskQueue = TaskQueue()
bot: 'Bot' = None
bot: Bot = None
_instance: 'Config'
def __new__(cls, *args, **kwargs):
@@ -54,19 +57,16 @@ class Config:
def __init__(self, **kwargs):
reload = kwargs.pop('reload', False)
root_dir = kwargs.pop('root_dir', None)
setattr(self, 'root_dir', root_dir) if root_dir else ...
[setattr(self, key, value) for key, value in kwargs.items()]
self.load_config(reload=reload)
self.load_config(reload=reload, **kwargs)
def set_root(self, *, root: str | Path):
root = Path(root) if str else root
self.root = root.absolute().resolve()
self.root_dir = self.root
def __setattr__(self, key, value):
if key == 'root_dir':
value = Path(value).absolute().resolve()
if key == 'records_dir':
self.create_records_dir(records_dir=value)
return
if key == 'path':
value = self.root_dir / Path(value) if not Path(value).exists() else value
value = str(self.root_dir / Path(value).absolute().resolve())
super().__setattr__(key, value)
@staticmethod
@@ -96,40 +96,53 @@ class Config:
return
def create_records_dir(self, *, records_dir: str | Path = 'records'):
"""Create records directory if it does not exist. Relative to the root directory of the project.
"""Create records directory if it does not exist. By default, it is relative to the root directory of the
project unless an absolute path is provided.
Keyword Args:
records_dir (str|Path): The name of the directory to create
records_dir (str|Path): The directory to save trade records. Default is 'records'
"""
try:
records_dir = Path(records_dir) if isinstance(records_dir, str) else records_dir
records_dir = self.root_dir / records_dir
if isinstance(records_dir, str):
records_dir = self.root_dir / records_dir
elif isinstance(records_dir, Path):
records_dir = records_dir.absolute().resolve()
records_dir.mkdir(parents=True, exist_ok=True)
super().__setattr__('records_dir', records_dir)
return records_dir
self.records_dir = records_dir
except Exception as err:
logger.warning(f"{err}: Unable to create records directory")
def load_config(self, *, file: str = None, reload: bool = True, filename: str = None, config_dir: str = ''):
def load_config(self, *, file: str = None, reload: bool = True, filename: str = None,
config_dir: str = '', **kwargs):
"""Load configuration settings from a file.
Keyword Args:
file (str): The path to the file to load. If not provided, the file is searched for
reload (bool): Whether to reload the config object. Default is True
filename (str): The name of the file to load. If not provided, the default filename is used
config_dir (str): The name of the directory to search for the file. Default is the root directory
root_dir (str): The root directory of the project
kwargs: Additional keyword arguments
"""
if not (self._initialize or reload):
return
self._initialize = False
data = {}
self.filename = filename or self.filename
self.config_dir = config_dir or self.config_dir
root_dir = kwargs.pop('root_dir', None)
records_dir = kwargs.pop('records_dir', 'records')
if self._initialize or (root_dir is not None):
self.set_root(root=(root_dir or '.'))
self.create_records_dir(records_dir=records_dir)
if (file := (file or self.find_config())) is None:
logger.warning("No Config File Found")
else:
fh = open(file, mode="r")
data = json.load(fh)
fh.close()
data |= kwargs
[setattr(self, key, value) for key, value in data.items()]
self._initialize = False
def account_info(self) -> dict[str, int | str]:
"""Returns Account login details as found in the config object if available
+9 -10
View File
@@ -310,7 +310,7 @@ class MetaTrader(metaclass=BaseMeta):
async def positions_total(self) -> int:
return await asyncio.to_thread(self._positions_total)
async def positions_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradePosition] | None:
async def positions_get(self, group: str = "", ticket: int = None, symbol: str = "") -> tuple[TradePosition] | None:
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
res = await asyncio.to_thread(self._positions_get, **kwargs)
if res is None:
@@ -324,11 +324,10 @@ class MetaTrader(metaclass=BaseMeta):
return await asyncio.to_thread(self._history_orders_total, date_from, date_to)
async def history_orders_get(self, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = '',
ticket: int = 0, position: int = 0) -> tuple[TradeOrder] | None:
kwargs = {key: value for key, value in (('date_from', date_from), ('date_to', date_to), ('group', group),
('ticket', ticket), ('position', position)) if value}
res = await asyncio.to_thread(self._history_orders_get, **kwargs)
group: str = '', ticket: int = None, position: int = None) -> tuple[TradeOrder] | None:
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value}
args = tuple(arg for arg in (date_from, date_to) if arg)
res = await asyncio.to_thread(self._history_orders_get, *args, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
@@ -340,10 +339,10 @@ class MetaTrader(metaclass=BaseMeta):
return await asyncio.to_thread(self._history_deals_total, date_from, date_to)
async def history_deals_get(self, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = '', ticket: int = 0, position: int = 0) -> tuple[TradeDeal] | None:
kwargs = {key: value for key, value in (('date_from', date_from), ('date_to', date_to), ('group', group),
('ticket', ticket), ('position', position)) if value}
res = await asyncio.to_thread(self._history_deals_get, **kwargs)
group: str = '', ticket: int = None, position: int = None) -> tuple[TradeDeal] | None:
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value}
args = tuple(arg for arg in (date_from, date_to) if arg)
res = await asyncio.to_thread(self._history_deals_get, *args, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
+2 -1
View File
@@ -18,7 +18,8 @@ class QueueItem:
else:
return self.task(*self.args, **self.kwargs)
except Exception as err:
logger.error(f'Error in running {self.task.__name__} with {str(self.args)}, {self.kwargs}: {err}')
logger.error(f"Error in running {getattr(self.task, '__name__', str(self.task))}"
f" with {str(self.args)}, {self.kwargs}: {err}")
class TaskQueue:
+134 -29
View File
@@ -2,8 +2,11 @@ import asyncio
from datetime import datetime
from logging import getLogger
from pandas import DataFrame
import pandas as pd
from .core.config import Config
from .core.meta_trader import MetaTrader
from .core.meta_trader import MetaTrader, CopyTicks, OrderType
from .core.models import TradeDeal, TradeOrder
logger = getLogger(__name__)
@@ -27,8 +30,8 @@ class History:
mt5: MetaTrader
config: Config
def __init__(self, *, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = "", ticket: int = 0, position: int = 0):
def __init__(self, *, date_from: datetime | int = None, date_to: datetime | int = None,
group: str = "", ticket: int = None, position: int = None):
"""
Args:
date_from (datetime, float): Date the orders are requested from. Set by the 'datetime' object or as a
@@ -71,63 +74,165 @@ class History:
self.initialized = all(res)
return self.initialized
async def get_deals(self, retries=3) -> list[TradeDeal]:
async def get_deals(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
retries: int = 3) -> tuple[TradeDeal, ...]:
"""Get deals from trading history using the parameters set in the constructor.
Returns:
list[TradeDeal]: A list of trade deals
tuple[TradeDeal]: A list of trade deals
"""
if retries < 1:
logger.warning(f'Failed to get deals: {self.mt5.error}')
return []
deals = await self.mt5.history_deals_get(date_from=self.date_from, date_to=self.date_to, position=self.position,
group=self.group, ticket=self.ticket)
return tuple()
date_from, date_to, group = date_from or self.date_from, date_to or self.date_to, group or self.group
deals = await self.mt5.history_deals_get(date_from=date_from, date_to=date_to, group=group)
if deals is not None:
self.deals = [TradeDeal(**deal._asdict()) for deal in deals]
self.deals = tuple(TradeDeal(**deal._asdict()) for deal in deals)
self.total_deals = len(self.deals)
return self.deals
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_deals(retries=retries - 1)
return await self.get_deals(date_from=date_from, date_to=date_to, group=group, retries=retries-1)
logger.warning(f'Failed to get deals: {self.mt5.error}')
return []
return tuple()
async def deals_total(self) -> int:
async def get_deals_ticket(self, *, ticket: int = None) -> tuple[TradeDeal, ...]:
"""Call specifying the order ticket. Return all deals having the specified order ticket in the DEAL_ORDER
property.
Args:
ticket (int): The order ticket
Returns:
tuple[TradeDeal]: A tuple of all deals with the order ticket
"""
ticket = ticket or self.ticket
assert ticket is not None, 'ticket not provided'
deals = await self.mt5.history_deals_get(ticket=ticket)
return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals], key=lambda x: x.time_msc))
async def get_deals_position(self, *, position: int = None) -> tuple[TradeDeal, ...]:
"""
Get all deals with the specified position ticket in the DEAL_POSITION_ID property
Args:
position (int): The position ticket
Returns:
tuple[TradeDeal]: A tuple of all deals with the position ticket
"""
position = position or self.position
assert position is not None, 'position not provided'
deals = await self.mt5.history_deals_get(position=position)
return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals], key=lambda x: x.time_msc))
async def deals_total(self, *, date_from: int | datetime = None, date_to: int | datetime = None) -> int:
"""Get total number of deals within the specified period in the constructor.
Args:
date_from (int|datetime): Date the orders are requested from. Set by the 'datetime' object or as a number of
seconds elapsed since 1970.01.01.
date_to (int|datetime): Date up to which the orders are requested. Set by the 'datetime' object or as a
number of seconds elapsed since 1970.01.01.
Returns:
int: Total number of Deals
"""
self.total_deals = await self.mt5.history_deals_total(self.date_from, self.date_to)
return self.total_deals
date_from, date_to = date_from or self.date_from, date_to or self.date_to
assert date_from is not None and date_to is not None, 'date_from and/or date_to not provided'
total_deals = await self.mt5.history_deals_total(date_from, date_to)
return total_deals
async def get_orders(self, retries=3) -> list[TradeOrder]:
"""Get orders from trading history using the parameters set in the constructor.
async def get_orders(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
retries: int = 3) -> tuple[TradeOrder, ...]:
"""Get orders from trading history using the parameters set in the constructor or the method arguments.
Returns:
list[TradeOrder]: A list of trade orders
"""
if retries < 1:
logger.warning(f'Failed to get orders: {self.mt5.error}')
return []
orders = await self.mt5.history_orders_get(date_from=self.date_from, date_to=self.date_to, group=self.group,
position=self.position, ticket=self.ticket)
return tuple()
date_from, date_to, group = date_from or self.date_from, date_to or self.date_to, group or self.group
orders = await self.mt5.history_orders_get(date_from=date_from, date_to=date_to, group=group)
if orders is not None:
self.orders = [TradeOrder(**order._asdict()) for order in orders]
self.total_orders = len(self.orders)
return self.orders
return tuple(TradeOrder(**order._asdict()) for order in orders)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_orders(retries=retries - 1)
logger.warning(f'Failed to get orders: {self.mt5.error}')
return []
return await self.get_orders(date_from=date_from, date_to=date_to, group=group, retries=retries - 1)
async def orders_total(self) -> int:
logger.warning(f'Failed to get orders: {self.mt5.error}')
return tuple()
async def get_order_ticket(self, ticket: int | None = None) -> TradeOrder:
ticket = ticket or self.ticket
assert isinstance(ticket, int), 'ticket not provided'
orders = await self.mt5.history_orders_get(ticket=ticket)
order = orders[0]
assert order.ticket == ticket
return TradeOrder(**order._asdict())
async def get_orders_position(self, position: int = None) -> tuple[TradeOrder, ...]:
"""
Call specifying the position ticket. Return all orders with a position ticket specified in the
ORDER_POSITION_ID property
Args:
position: The position ticket
Returns:
tuple[TradeOrder]: A tuple of all orders with the position ticket
"""
position = position or self.position
assert isinstance(position, int), 'position not provided'
orders = await self.mt5.history_orders_get(position=position)
return tuple(sorted([TradeOrder(**order._asdict()) for order in orders], key=lambda x: x.time_done_msc))
async def orders_total(self, date_from: int | datetime = None, date_to: int | datetime = None) -> int:
"""Get total number of orders within the specified period in the constructor.
Returns:
int: Total number of orders
"""
self.total_orders = await self.mt5.history_orders_total(self.date_from, self.date_to)
return self.total_orders
date_from, date_to = date_from or self.date_from, date_to or self.date_to
assert date_from is not None and date_to is not None, 'date_from and/or date_to not provided'
total_orders = await self.mt5.history_orders_total(date_from, date_to)
return total_orders
async def track_order(self, *, position: int = None, end_time: datetime = None) -> DataFrame:
"""
Track an order from the time it was opened to the time it was closed or any given time.
The tracking is done by getting the ticks
for the order symbol from the time the order was opened to the time it was closed. The profit for each tick is
calculated using the order type, symbol, initial volume, open price and the bid or ask price of the tick
depending on the order type.
Args:
end_time (datetime): The time to stop tracking the order. If not provided, the tracking will continue until
the order is closed.
position (int): The position ticket
end_time (int): The time to stop tracking the order in seconds. If not provided, the tracking will continue
until the order is closed.
Returns:
DataFrame: A pandas DataFrame of the ticks and profit for the order.
"""
orders = await self.get_orders_position(position=position)
deals = await self.get_deals_position(position=position)
open_order = orders[0]
open_deal = deals[0]
close_deal = deals[-1]
time_done = datetime.timestamp(end_time) if end_time is not None else close_deal.time
time_done_msc = int(time_done * 1000)
open_order.set_attributes(time_done_msc=time_done_msc, time_done=time_done, price_open=open_deal.price)
ticks = await self.mt5.copy_ticks_range(open_order.symbol, open_order.time_setup, open_order.time_done,
CopyTicks.ALL)
data = pd.DataFrame(ticks)
profit = lambda x: self.mt5._order_calc_profit(open_order.type, open_order.symbol, open_order.volume_initial,
open_order.price_open,
x.ask if open_order.type == OrderType.BUY else x.bid)
data['profits'] = data.apply(profit, axis=1)
data['time'] = pd.to_datetime(data['time'], unit='s')
data.set_index('time', inplace=True)
return data
+5 -7
View File
@@ -69,15 +69,13 @@ class FingerTrap(Strategy):
self.tracker.update(new=True, entry_time=current)
candles.ta.ema(length=self.entry_ema, append=True)
candles.rename(**{f"EMA_{self.entry_ema}": "ema"})
cae = candles.ta_lib.cross(candles.close, candles.ema)
cbe = candles.ta_lib.cross(candles.close, candles.ema, above=False)
trend = self.ttf.time // self.etf.time
bull_trend = cae.iloc[-trend:]
bear_trend = cbe.iloc[-trend:]
if self.tracker.bullish and any(bull_trend):
candles['cae'] = candles.ta_lib.cross(candles.close, candles.ema)
candles['cbe'] = candles.ta_lib.cross(candles.close, candles.ema, above=False)
current = candles[-1]
if self.tracker.bullish and current.cae:
sl = find_bullish_fractal(candles).low
self.tracker.update(snooze=self.ttf.time, order_type=OrderType.BUY, sl=sl)
elif self.tracker.bearish and any(bear_trend):
elif self.tracker.bearish and current.cbe:
sl = find_bearish_fractal(candles).high
self.tracker.update(snooze=self.ttf.time, order_type=OrderType.SELL, sl=sl)
else:
+20
View File
@@ -38,6 +38,26 @@ class Order(TradeRequest):
"""
return await self.mt5.orders_total()
async def get_order(self, *, ticket: int, retries: int = 3) -> TradeOrder:
"""
Get the order by ticket number.
Args:
ticket (int): Order ticket number
retries (int): Number of retries
Returns:
"""
if retries < 1:
raise OrderError(f'Failed to get orders for {self.symbol}: {self.mt5.error}')
orders = await self.mt5.orders_get(ticket=ticket)
if orders is not None:
order = TradeOrder(**orders[0]._asdict())
assert order.ticket == ticket, f'Order ticket mismatch {order.ticket} != {ticket}'
return order
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_order(ticket=ticket, retries=retries-1)
raise OrderError(f'Failed to get orders for {self.symbol}: {self.mt5.error}')
async def get_orders(self, *, ticket: int = 0, symbol: str = '', group: str = '', retries=3)\
-> tuple[TradeOrder, ...]:
"""Get the list of active orders for the current symbol.
+30 -3
View File
@@ -68,9 +68,30 @@ class Positions:
logger.warning(f'Failed to get positions for {symbol or self.symbol}. {self.mt5.error}')
return []
async def close(self, *, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType):
"""Close an open position for the trading account."""
async def position_get(self, *, ticket: int) -> TradePosition:
"""Get an open position by ticket.
Args:
ticket (int): Position ticket.
Returns:
TradePosition: Return an open position
"""
positions = await self.positions_get(ticket=ticket)
position = positions[0] if positions else None
if position is None:
raise ValueError(f'Position with ticket {ticket} not found')
assert position.ticket == ticket, f'Position with ticket {ticket} not found'
return position
async def close(self, *, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType):
"""Close an open position for the trading account using the ticket and other parameters.
Args:
ticket (int): Position ticket.
symbol (str): Financial instrument name.
price (float): Closing price.
volume (float): Volume to close.
order_type (OrderType): Order type.
"""
order = Order(action=TradeAction.DEAL, price=price, position=ticket, symbol=symbol, volume=volume,
type=order_type.opposite)
return await order.send()
@@ -81,6 +102,12 @@ class Positions:
price=pos.price_current)
return await order.send()
async def close_position(self, *, position: TradePosition):
"""Close an open position for the trading account. Using a position object."""
order = Order(position=position.ticket, symbol=position.symbol, volume=position.volume,
type=position.type.opposite, price=position.price_current)
return await order.send()
async def close_all(self, symbol: str = '', group: str = '') -> int:
"""Close all open positions for the trading account. Specify a symbol or group to filter positions.
@@ -94,6 +121,6 @@ class Positions:
symbol = symbol or self.symbol
group = group or self.group
positions = [pos for pos in await self.positions_get(symbol=symbol, group=group)]
orders = [self.close_by(pos) for pos in positions]
orders = [self.close_position(position=pos) for pos in positions]
results = await asyncio.gather(*[order for order in orders], return_exceptions=True)
return len([res for res in results if (res and res.retcode) == 10009])
+29 -13
View File
@@ -9,10 +9,11 @@ class RAM:
risk: float
points: float
pips: float
min_amount: float
max_amount: float
balance_level: float = 10
min_amount: float = 0
max_amount: float = 0
risk_level: float = 50
loss_limit: int = 3
open_limit: int = 6
def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, **kwargs):
"""Initialize Risk Assessment and Management with the provided keyword arguments.
@@ -28,23 +29,38 @@ class RAM:
[setattr(self, key, value) for key, value in kwargs.items()]
async def get_amount(self) -> float:
"""Calculate the amount to risk per trade as a percentage of balance.
"""Calculate the amount to risk per trade as a percentage of equity.
Returns:
float: Amount to risk per trade
"""
await self.account.refresh()
return self.account.balance * self.risk
amount = self.account.margin_free * self.risk
if self.min_amount and self.max_amount:
return max(self.min_amount, min(self.max_amount, amount))
return amount
async def check_losing_positions(self) -> bool:
"""Check if the number of losing positions is greater than or equal the loss limit."""
positions = await Positions().positions_get()
positions.sort(key=lambda pos: pos.time_msc)
async def check_losing_positions(self, *, symbol: str = '') -> bool:
"""Check if the number of losing positions is greater than or equal the loss limit.
Args:
symbol (str): Symbol to check. Defaults to ''.
"""
positions = await Positions().positions_get(symbol=symbol)
loosing = [trade for trade in positions if trade.profit <= 0]
return len(loosing) >= self.loss_limit
async def check_balance_level(self) -> bool:
"""Check if the balance level is greater than or equal to the balance level."""
async def check_open_positions(self, *, symbol: str = '') -> bool:
"""Check if the number of open positions is greater than or equal the loss limit.
Args:
symbol (str): Symbol to check. Defaults to ''.
"""
positions = await Positions().positions_get(symbol=symbol)
return len(positions) >= self.open_limit
async def check_risk_level(self) -> bool:
"""Check the risk level."""
await self.account.refresh()
balance_level = (self.account.margin / self.account.balance) * 100
return balance_level >= self.balance_level
risk_level = (1 - (self.account.margin_free / self.account.equity)) * 100
return risk_level >= self.risk_level
+2 -1
View File
@@ -4,6 +4,7 @@ import asyncio
from pathlib import Path
import csv
import logging
from typing import Iterable
from .core import Config, MetaTrader
@@ -50,7 +51,7 @@ class Records:
"""
try:
fr = open(file, mode='r', newline='')
reader = csv.DictReader(fr)
reader: Iterable[dict] | csv.DictReader = csv.DictReader(fr)
rows = [row for row in reader]
rows = await self.update_rows(rows)
fr.close()
+51 -13
View File
@@ -1,7 +1,7 @@
import csv
import json
from logging import getLogger
from threading import RLock
from pathlib import Path
from typing import Iterable, Literal
from .core import Config
from .core.models import OrderSendResult
@@ -31,26 +31,64 @@ class Result:
self.parameters = parameters or {}
self.result = result
self.name = name or parameters.get('name', 'Trades')
if not Path(self.config.records_dir).exists():
Path(self.config.records_dir).mkdir(parents=True, exist_ok=True)
def get_data(self) -> dict:
res = self.result.get_dict(exclude={'retcode', 'comment', 'retcode_external', 'request_id', 'request'})
return self.parameters | res | {'actual_profit': 0, 'closed': False, 'win': False}
async def save(self, *, trade_record_mode: Literal['csv', 'json'] = None):
"""Record trade results as a csv or json file
Args:
trade_record_mode (Literal['csv'|'json']): Mode of saving trade records
"""
trade_record_mode = trade_record_mode or self.config.trade_record_mode
if trade_record_mode == 'csv':
await self.to_csv()
else:
await self.to_json()
async def to_csv(self):
"""Record trade results and associated parameters as a csv file
"""
try:
data = self.get_data()
file = self.config.records_dir / f"{self.name}.csv"
exists = file.exists()
with RLock():
with open(file, 'a', newline='') as fh:
f_names = sorted(list(data.keys()))
writer = csv.DictWriter(fh, fieldnames=f_names, extrasaction='ignore', restval=None)
if not exists:
writer.writeheader()
writer.writerow(data)
file.touch(exist_ok=True) if not file.exists() else ...
reader: Iterable[dict] = csv.DictReader(file.open('r', newline=''))
rows: list[dict] = []
headers = set()
[(rows.append(row), headers.update(row.keys())) for row in reader]
rows.append(data)
headers.update(data.keys())
writer = csv.DictWriter(file.open('w', newline=''), fieldnames=headers, restval=None,
extrasaction='ignore')
writer.writeheader()
writer.writerows(rows)
except Exception as err:
logger.error(f'Error: {err}. Unable to save trade results')
logger.error(f'Unable to save to csv: {err}')
@staticmethod
def serialize(value) -> str:
"""Serialize the trade records and strategy parameters
"""
try:
return str(value)
except (ValueError, TypeError) as _:
return ""
async def to_json(self):
"""Save trades and strategy parameters in a json file
"""
try:
file = self.config.records_dir / f"{self.name}.json"
data = self.get_data()
exists = file.touch(exist_ok=True) if not file.exists() else True
if not exists:
json.dump([], file.open('w'))
with file.open('r') as fh:
rows = json.load(fh)
rows.append(data)
with file.open('w') as fh:
json.dump(rows, fh, indent=2, skipkeys=True, default=self.serialize)
except Exception as err:
logger.error(f"Unable to save as json file: {err}")
+52
View File
@@ -4,6 +4,8 @@ from typing import TypeVar, Iterable
from pandas import DataFrame, Series
import pandas_ta as ta
import mplfinance as mplt
import pandas as pd
from .core.constants import TickFlag
@@ -47,6 +49,21 @@ class Tick:
% {"class": self.__class__.__name__, "time": self.time, "bid": self.bid,
"ask": self.ask, "last": self.last, "volume": self.volume, 'Index': self.Index})
def dict(self, exclude: set = None, include: set = None) -> dict:
"""
Returns a dictionary of the instance attributes.
Args:
exclude: A set of attributes to exclude from the dictionary. Defaults to None.
include: A set of attributes to include in the dictionary. Defaults to None.
Returns: dict
"""
exclude = exclude or set()
include = include or set()
keys = include or set(self.__dict__.keys()).difference(exclude)
return {k: v for k, v in self.__dict__.items() if k in keys}
def set_attributes(self, **kwargs):
"""Set attributes from keyword arguments"""
for key, value in kwargs.items():
@@ -158,3 +175,38 @@ class Ticks:
"""
res = self._data.rename(columns=kwargs, inplace=inplace)
return res if inplace else self.__class__(data=res)
def make_addplot(self, *, count: int = 50, columns: list = None, **kwargs) -> dict:
"""
Make subplots for adding to the main plot
Args:
count (int): The numbers of candles to make the addplot for. Defaults to 50.
columns (list[str]): The columns to make the plot from. Defaults to None.
**kwargs: Valid arguments for the mplfinance make_addplot function
"""
columns = columns or []
data = self._data[-count:]
data.index = pd.to_datetime(data['time'], unit='s')
return mplt.make_addplot(data[columns], **kwargs)
def visualize(self, *, count: int = 50, type='candle', savefig: str | dict = None, addplot: dict = None,
style: str = 'charles', ylabel: str = 'Price', title: str = 'Chart', **kwargs):
"""Visualize the candles using the mplfinance library.
Args:
count (int): The number of candles to visualize, counting from behind, i.e the most recent candles.
Defaults to 50.
type: Type of chart, defaults to candle
savefig (str|dict): The path to save the figure or a dictionary of parameters to pass to the savefig method.
addplot: Additional plots to add to the chart. Defaults to None. They should match the dimension of the
original data which is specified via the count parameter.
style (str): The style of the chart. Defaults to 'charles'.
ylabel (str): The label of the y-axis. Defaults to 'Price'.
title (str): The title of the chart. Defaults to 'Chart'.
kwargs: valid kwargs for the plot function.
"""
kwargs |= {key: arg for key, arg in (('savefig', savefig), ('addplot', addplot), ('style', style),
('ylabel', ylabel), ('title', title), ('type', type)) if arg}
data = self._data[-count:]
data.index = pd.to_datetime(data['time'], unit='s')
mplt.plot(data, **kwargs)
+154
View File
@@ -0,0 +1,154 @@
"""This module contains the Records class, which is used to read and update trade records from csv files."""
import asyncio
import json
from pathlib import Path
import csv
import logging
from typing import Iterable
from .core import Config, MetaTrader
logger = logging.getLogger(__name__)
class TradeRecords:
"""This utility class read trade records from csv files, and update them based on their closing positions.
Attributes:
config: Config object
records_dir(Path): Absolute path to directory containing record of placed trades, If not given takes the default
from the config
"""
config: Config
mt5: MetaTrader
def __init__(self, *, records_dir: Path | str = ''):
"""Initialize the Records class. The main method of this class is update_records which you should call to update
all the records specified in the records_dir.
Keyword Args:
records_dir (Path): Absolute path to directory containing record of placed trades.
"""
self.config = Config()
self.mt5 = MetaTrader()
self.records_dir = records_dir or self.config.records_dir
async def get_csv_records(self):
"""Get trade records saved as csv from records_dir folder
Yields:
files: Trade record files
"""
for file in self.records_dir.iterdir():
if file.is_file() and file.name.endswith('.csv'):
yield file
async def get_json_records(self):
"""Get trade records from records_dir folder
Yields:
files: Trade record files
"""
for file in self.records_dir.iterdir():
if file.is_file() and file.name.endswith('.json'):
yield file
async def read_update_csv(self, *, file: Path):
"""Read and update csv trade records
Args:
file: Trade record file in csv format
"""
try:
fr = open(file, mode='r', newline='')
reader: Iterable[dict] | csv.DictReader = csv.DictReader(fr)
rows = [row for row in reader]
rows = await self.update_rows(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 csv trade records')
async def read_update_json(self, *, file: Path):
"""Read and update json trade records
Args:
file: Trade record file in csv format
"""
try:
fh = open(file, mode='r')
data = json.load(fh)
rows = [row for row in data]
rows = await self.update_rows(rows=rows)
fh.close()
fh = open(file, mode='w')
json.dump(rows, fh, indent=2)
fh.close()
except Exception as err:
logger.error(f'Error: {err}. Unable to read and update json trade records')
async def update_row(self, *, row: dict) -> dict:
"""Update a single row of entered trade in the csv or json 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 or json file with the actual profit.
Args:
rows: A list of dictionaries.
Returns:
list[dict]: A list of dictionaries with the actual profit and win status.
"""
closed, unclosed = [], []
for row in rows:
closed_ = row.get('closed', False)
closed_ = closed_.title() == 'True' if isinstance(closed_, str) else closed_
if closed_:
closed.append(row)
else:
unclosed.append(row)
unclosed = await asyncio.gather(*[self.update_row(row=row) for row in unclosed])
return closed + list(unclosed)
async def update_csv_records(self):
"""Update csv trade records in the records_dir folder."""
records = [self.read_update_csv(file=record) async for record in self.get_csv_records()]
await asyncio.gather(*records)
async def update_json_records(self):
"""Update json trade records in the records_dir folder."""
records = [self.read_update_json(file=record) async for record in self.get_json_records()]
await asyncio.gather(*records)
async def update_csv_record(self, *, file: Path | str):
"""Update a single trade record csv file."""
await self.read_update_csv(file=file)
async def update_json_record(self, *, file: Path | str):
"""Update a single json trade record file"""
await self.read_update_json(file=file)
+5 -4
View File
@@ -107,17 +107,18 @@ class Trader(ABC):
await self.record_trade(result, parameters=self.parameters.copy())
return result
async def record_trade(self, result: OrderSendResult, parameters: dict = None, name: str = ''):
"""Record the trade in a csv file.
async def record_trade(self, result: OrderSendResult, parameters: dict = None, name: str = '', exclude: set = None):
"""Record the trade in csv or json.
Args:
result (OrderSendResult): Result of the order send
parameters: parameters of the trading strategy used to place the trade
name: Name of the trading strategy
exclude: Exclude these fields from the recorded trade
"""
if result.retcode != 10009 or not self.config.record_trades:
return
params = parameters or self.parameters.copy()
params = {k: v for k, v in params.items() if k not in (exclude or set())}
profit = await self.order.calc_profit()
params["expected_profit"] = profit
date = datetime.utcnow()
@@ -125,7 +126,7 @@ class Trader(ABC):
params["date"] = str(date.date())
params["time"] = str(date.time())
res = Result(result=result, parameters=params, name=name)
self.config.task_queue.add_task(res.to_csv)
self.config.task_queue.add_task(res.save)
@abstractmethod
async def place_trade(self, *args, **kwargs):