mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-17 05:48:07 +00:00
v3.21
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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,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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user