mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-20 15:28:08 +00:00
v4.1.0
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
# forex_symbol
|
# forex_symbol
|
||||||
|
|
||||||
`aiomql.contrib.symbols.forex_symbol` — Forex-specific symbol with pip calculations.
|
`aiomql.contrib.symbols.forex_symbol` — Forex-specific symbol with pip and volume calculations.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Extends [`Symbol`](../../lib/symbol.md) with forex-specific logic for pip size,
|
Extends [`Symbol`](../../lib/symbol.md) with forex-specific logic for pip size
|
||||||
pip value, and volume calculations based on currency pairs.
|
and volume calculations based on price movements and stop-loss levels.
|
||||||
|
|
||||||
## Classes
|
## Classes
|
||||||
|
|
||||||
@@ -17,13 +17,12 @@ Inherits from [`Symbol`](../../lib/symbol.md).
|
|||||||
|
|
||||||
| Attribute | Type | Description |
|
| Attribute | Type | Description |
|
||||||
|-----------|------|-------------|
|
|-----------|------|-------------|
|
||||||
| `pip` | `float` | Pip size for the pair |
|
| `pip` | `float` | Pip size for the pair (`point * 10`) |
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
| Method | Returns | Description |
|
| Method | Returns | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `pip_value(volume)` | `float` | Value of one pip for a given lot size |
|
| `compute_points(amount, volume)` | `float` | Computes points of price movement needed for a given amount and volume |
|
||||||
| `pips_to_price(pips)` | `float` | Converts a pip count to a price delta |
|
| `compute_volume_points(amount, points, round_down=False)` | `float` | Computes lot size from risk amount and point distance |
|
||||||
| `price_to_pips(price_delta)` | `float` | Converts a price delta to pips |
|
| `compute_volume_sl(amount, price, sl, round_down=False)` | `float` | Computes lot size from risk amount, entry price, and stop-loss price |
|
||||||
| `calc_volume(amount, pips)` | `float` | Calculates lot size from risk amount and pip distance |
|
|
||||||
|
|||||||
+31
-9
@@ -7,7 +7,7 @@
|
|||||||
The `Order` class creates and manages trade orders for the MetaTrader 5 terminal. It handles
|
The `Order` class creates and manages trade orders for the MetaTrader 5 terminal. It handles
|
||||||
margin calculations, profit projections, order validation, modification, and cancellation.
|
margin calculations, profit projections, order validation, modification, and cancellation.
|
||||||
|
|
||||||
Inherits from [`_Base`](../core/base.md).
|
Inherits from [`_Base`](../core/base.md) and `TradeRequest`.
|
||||||
|
|
||||||
## Classes
|
## Classes
|
||||||
|
|
||||||
@@ -30,32 +30,54 @@ Inherits from [`_Base`](../core/base.md).
|
|||||||
| `type_filling` | `OrderFilling` | Filling policy |
|
| `type_filling` | `OrderFilling` | Filling policy |
|
||||||
| `type_time` | `OrderTime` | Time-in-force policy |
|
| `type_time` | `OrderTime` | Time-in-force policy |
|
||||||
|
|
||||||
|
#### Initialisation
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `__init__(**kwargs)` | Requires `symbol`. Defaults: `action=TradeAction.DEAL`, `type_time=OrderTime.DAY`, `type_filling=OrderFilling.FOK` |
|
||||||
|
|
||||||
#### `request` *(property)*
|
#### `request` *(property)*
|
||||||
|
|
||||||
Returns the trade request as a dict, filtering out `None` values.
|
Returns the trade request as a dict, filtering to keys valid for `TradeRequest`.
|
||||||
|
|
||||||
#### Validation
|
#### Validation
|
||||||
|
|
||||||
| Method | Returns | Description |
|
| Method | Returns | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `check()` | `OrderCheckResult` | Validates the order, raises `OrderError` on failure |
|
| `check(**kwargs)` | `OrderCheckResult` | Validates the order; raises `OrderError` on failure |
|
||||||
|
|
||||||
#### Execution
|
#### Execution
|
||||||
|
|
||||||
| Method | Returns | Description |
|
| Method | Returns | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `send()` | `OrderSendResult` | Sends the order; retries on requote/timeout |
|
| `send()` | `OrderSendResult` | Sends the order via `send_order()` |
|
||||||
|
| `send_order(request, connection_retries=0)` | `OrderSendResult` | Class method. Sends a trade request; retries up to 3 times on connection loss (retcode 10031). Raises `OrderError` on failure |
|
||||||
|
|
||||||
#### Calculations
|
#### Calculations
|
||||||
|
|
||||||
| Method | Returns | Description |
|
| Method | Returns | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `calc_margin()` | `float \| None` | Required margin for the order |
|
| `calc_margin()` | `float \| None` | Required margin for the order |
|
||||||
| `calc_profit(close_price)` | `float \| None` | Projected profit at a given close price |
|
| `calc_profit()` | `float \| None` | Projected profit at `tp` (take profit) price |
|
||||||
|
| `calc_loss()` | `float \| None` | Projected loss at `sl` (stop loss) price |
|
||||||
|
| `profit_to_price(profit, order_type, volume, symbol, price_open)` | `float` | Class method. Reverse-calculates the close price needed to achieve a target profit |
|
||||||
|
|
||||||
#### Modification
|
#### Modification
|
||||||
|
|
||||||
| Method | Description |
|
| Method | Returns | Description |
|
||||||
|--------|-------------|
|
|--------|---------|-------------|
|
||||||
| `modify(**kwargs)` | Modifies a pending order's parameters |
|
| `modify(**kwargs)` | — | Updates the order's attributes |
|
||||||
| `cancel()` | Cancels a pending order |
|
| `cancel_order(order, symbol="")` | `OrderSendResult` | Class method. Cancels a pending order by ticket; raises `OrderError` on failure |
|
||||||
|
|
||||||
|
#### Pending & Historical Orders
|
||||||
|
|
||||||
|
| Method | Returns | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `orders_total()` | `int` | Class method. Total number of active pending orders |
|
||||||
|
| `get_pending_order(ticket)` | `TradeOrder \| None` | Class method. Gets a single pending order by ticket |
|
||||||
|
| `get_pending_orders(ticket, symbol, group)` | `tuple[TradeOrder, ...]` | Class method. Gets pending orders filtered by ticket, symbol, or group |
|
||||||
|
| `get_history_order_by_ticket(ticket)` | `TradeOrder \| None` | Class method. Gets a historical order by ticket |
|
||||||
|
|
||||||
|
## Synchronous API
|
||||||
|
|
||||||
|
Available in `aiomql.lib.sync.order`. All async methods become synchronous with the same signatures.
|
||||||
|
|||||||
+55
-12
@@ -7,7 +7,7 @@
|
|||||||
The `Symbol` class represents a financial instrument (forex pair, stock, etc.) and provides
|
The `Symbol` class represents a financial instrument (forex pair, stock, etc.) and provides
|
||||||
methods for querying market data, selecting symbols, and retrieving rates and ticks.
|
methods for querying market data, selecting symbols, and retrieving rates and ticks.
|
||||||
|
|
||||||
Inherits from [`_Base`](../core/base.md).
|
Inherits from [`_Base`](../core/base.md) and `SymbolInfo`.
|
||||||
|
|
||||||
## Classes
|
## Classes
|
||||||
|
|
||||||
@@ -19,33 +19,76 @@ Inherits from [`_Base`](../core/base.md).
|
|||||||
|-----------|------|-------------|
|
|-----------|------|-------------|
|
||||||
| `name` | `str` | Symbol name (e.g. `"EURUSD"`) |
|
| `name` | `str` | Symbol name (e.g. `"EURUSD"`) |
|
||||||
| `select` | `bool` | Whether the symbol is selected in Market Watch |
|
| `select` | `bool` | Whether the symbol is selected in Market Watch |
|
||||||
|
| `tick` | `Tick` | Current price tick for the instrument |
|
||||||
|
| `account` | `Account` | Trading account instance |
|
||||||
|
| `initialized` | `bool` | Whether the symbol has been successfully initialized |
|
||||||
|
|
||||||
All `SymbolInfo` fields are available as instance attributes after initialisation.
|
All `SymbolInfo` fields are available as instance attributes after initialisation.
|
||||||
|
|
||||||
#### Initialisation
|
#### Initialisation
|
||||||
|
|
||||||
| Method | Description |
|
| Method | Returns | Description |
|
||||||
|--------|-------------|
|
|--------|---------|-------------|
|
||||||
| `init()` | Fetches symbol info from the terminal and sets all attributes |
|
| `__init__(**kwargs)` | — | Requires `name` keyword argument |
|
||||||
|
| `initialize()` | `bool` | Async. Fetches symbol info, tick, and selects the symbol |
|
||||||
|
| `initialize_sync()` | `bool` | Synchronous version of `initialize()` |
|
||||||
|
|
||||||
|
#### Symbol Info
|
||||||
|
|
||||||
|
| Method | Returns | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `info()` | `SymbolInfo \| None` | Fetches and updates all symbol properties |
|
||||||
|
| `info_tick(name="")` | `Tick \| None` | Gets the current price tick |
|
||||||
|
| `symbol_select(enable=True)` | `bool` | Selects or removes the symbol from Market Watch |
|
||||||
|
|
||||||
|
#### Market Depth
|
||||||
|
|
||||||
|
| Method | Returns | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `book_add()` | `bool` | Subscribes to Market Depth events |
|
||||||
|
| `book_get()` | `tuple[BookInfo, ...]` | Returns Market Depth entries |
|
||||||
|
| `book_release()` | `bool` | Cancels Market Depth subscription |
|
||||||
|
|
||||||
|
#### Volume Helpers
|
||||||
|
|
||||||
|
| Method | Returns | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `check_volume(volume)` | `tuple[bool, float]` | Checks if volume is within min/max limits |
|
||||||
|
| `round_off_volume(volume, round_down=False)` | `float` | Rounds volume to nearest volume step |
|
||||||
|
| `compute_volume(*args, **kwargs)` | `float` | Returns `volume_min` (override in subclasses) |
|
||||||
|
|
||||||
|
#### Currency Conversion
|
||||||
|
|
||||||
|
| Method | Returns | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `amount_in_quote_currency(amount)` | `float` | Converts amount to the symbol's quote currency |
|
||||||
|
| `convert_currency(amount, from_currency, to_currency)` | `float \| None` | Converts between two currencies via tick data |
|
||||||
|
|
||||||
#### Market Data
|
#### Market Data
|
||||||
|
|
||||||
| Method | Returns | Description |
|
| Method | Returns | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `info_tick()` | `Tick` | Current tick for the symbol |
|
|
||||||
| `copy_rates_from(timeframe, date_from, count)` | `Candles` | Historical bars from a date |
|
| `copy_rates_from(timeframe, date_from, count)` | `Candles` | Historical bars from a date |
|
||||||
| `copy_rates_from_pos(timeframe, start_pos, count)` | `Candles` | Historical bars from a position |
|
| `copy_rates_from_pos(timeframe, count, start_position)` | `Candles` | Historical bars from a position |
|
||||||
| `copy_rates_range(timeframe, date_from, date_to)` | `Candles` | Historical bars in a range |
|
| `copy_rates_range(timeframe, date_from, date_to)` | `Candles` | Historical bars in a date range |
|
||||||
| `copy_ticks_from(date_from, count, flags)` | `Ticks` | Historical ticks from a date |
|
| `copy_ticks_from(date_from, count, flags)` | `Ticks` | Historical ticks from a date |
|
||||||
| `copy_ticks_range(date_from, date_to, flags)` | `Ticks` | Historical ticks in a range |
|
| `copy_ticks_range(date_from, date_to, flags)` | `Ticks` | Historical ticks in a date range |
|
||||||
|
|
||||||
#### Helpers
|
#### Properties
|
||||||
|
|
||||||
| Property | Returns | Description |
|
| Property | Returns | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `pip` | `float` | The pip size for the symbol |
|
| `pip` | `float` | Pip size (`point * 10`) |
|
||||||
| `spread` | `float` | Current bid-ask spread |
|
|
||||||
|
#### Overridable Methods
|
||||||
|
|
||||||
|
These methods raise `NotImplementedError` in the base `Symbol` and are meant to be implemented by subclasses such as [`ForexSymbol`](../contrib/symbols/forex_symbol.md).
|
||||||
|
|
||||||
|
| Method | Returns | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `compute_volume_sl(amount, price, sl, round_down)` | `float` | Compute volume from stop-loss distance |
|
||||||
|
| `compute_volume_points(amount, points, round_down)` | `float` | Compute volume from point distance |
|
||||||
|
|
||||||
## Synchronous API
|
## Synchronous API
|
||||||
|
|
||||||
Available in `aiomql.lib.sync.symbol`.
|
Available in `aiomql.lib.sync.symbol`. All async methods become synchronous with the same signatures.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class EMAXOver(Strategy):
|
|||||||
# default parameters for the strategy
|
# default parameters for the strategy
|
||||||
# they are set as attributes. You can override them in the constructor via the params argument.
|
# they are set as attributes. You can override them in the constructor via the params argument.
|
||||||
parameters = {'ttf': TimeFrame.M10, 'tcc': 3000, 'fast_ema': 34, 'slow_ema': 55, 'interval': TimeFrame.M5,
|
parameters = {'ttf': TimeFrame.M10, 'tcc': 3000, 'fast_ema': 34, 'slow_ema': 55, 'interval': TimeFrame.M5,
|
||||||
'timeout': 120, "macd": 87, "sma": 90}
|
'timeout': 120}
|
||||||
|
|
||||||
def __init__(self, *, symbol: ForexSymbol, params: dict | None = None, trader: Trader = None,
|
def __init__(self, *, symbol: ForexSymbol, params: dict | None = None, trader: Trader = None,
|
||||||
sessions: Sessions = None, name: str = "EMAXOver"):
|
sessions: Sessions = None, name: str = "EMAXOver"):
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "aiomql"
|
name = "aiomql"
|
||||||
version = "4.0.17"
|
version = "4.1.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class ForexSymbol(Symbol):
|
|||||||
points = amount / (volume * self.point * self.trade_contract_size)
|
points = amount / (volume * self.point * self.trade_contract_size)
|
||||||
return points
|
return points
|
||||||
|
|
||||||
def compute_volume_points(self, *, amount: float, points: float, round_down: bool = False) -> float:
|
async def compute_volume_points(self, *, amount: float, points: float, round_down: bool = False) -> float:
|
||||||
"""Compute the volume required for a trade based on points.
|
"""Compute the volume required for a trade based on points.
|
||||||
|
|
||||||
Calculates the appropriate trade volume to risk a specified amount
|
Calculates the appropriate trade volume to risk a specified amount
|
||||||
@@ -116,7 +116,7 @@ class ForexSymbol(Symbol):
|
|||||||
volume = amount / (self.point * points * self.trade_contract_size)
|
volume = amount / (self.point * points * self.trade_contract_size)
|
||||||
return self.round_off_volume(volume=volume, round_down=round_down)
|
return self.round_off_volume(volume=volume, round_down=round_down)
|
||||||
|
|
||||||
def compute_volume_sl(self, *, amount: float, price: float, sl: float, round_down: bool = False) -> float:
|
async def compute_volume_sl(self, *, amount: float, price: float, sl: float, round_down: bool = False) -> float:
|
||||||
"""Compute the volume required for a trade based on stop loss.
|
"""Compute the volume required for a trade based on stop loss.
|
||||||
|
|
||||||
Calculates the appropriate trade volume to risk a specified amount
|
Calculates the appropriate trade volume to risk a specified amount
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import asyncio
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any, TypeVar, ClassVar
|
from typing import Any, TypeVar, ClassVar
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import logging
|
|
||||||
|
|
||||||
from ...core import Config, State, sleep
|
from ...core import Config, State, sleep
|
||||||
from ...lib import Positions
|
from ...lib import Positions
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ async def auto_commit():
|
|||||||
try:
|
try:
|
||||||
with config.state.conn as conn:
|
with config.state.conn as conn:
|
||||||
while config.shutdown is False:
|
while config.shutdown is False:
|
||||||
print("committing state to the database")
|
|
||||||
await config.state.acommit(conn=conn, close=False)
|
await config.state.acommit(conn=conn, close=False)
|
||||||
await sleep(config.db_commit_interval)
|
await sleep(config.db_commit_interval)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ class Executor:
|
|||||||
signum: The signal number received.
|
signum: The signal number received.
|
||||||
frame: The current stack frame.
|
frame: The current stack frame.
|
||||||
"""
|
"""
|
||||||
|
print("shutting down")
|
||||||
self.config.shutdown = True
|
self.config.shutdown = True
|
||||||
|
|
||||||
def exit(self):
|
def exit(self):
|
||||||
@@ -162,12 +163,16 @@ class Executor:
|
|||||||
"""
|
"""
|
||||||
start = time.time()
|
start = time.time()
|
||||||
try:
|
try:
|
||||||
while self.config.shutdown is False and self.config.force_shutdown is False:
|
while True:
|
||||||
if self.timeout is not None and self.timeout < (time.time() - start):
|
if self.timeout is not None and self.timeout < (time.time() - start):
|
||||||
self.config.shutdown = True
|
self.config.shutdown = True
|
||||||
break
|
break
|
||||||
timeout = self.timeout or 1
|
timeout = self.timeout or 1
|
||||||
time.sleep(timeout)
|
time.sleep(timeout)
|
||||||
|
if self.config.shutdown or self.config.force_shutdown:
|
||||||
|
break
|
||||||
|
if all(strategy.running is False for strategy in self.strategy_runners):
|
||||||
|
break
|
||||||
for strategy in self.strategy_runners:
|
for strategy in self.strategy_runners:
|
||||||
strategy.running = False
|
strategy.running = False
|
||||||
self.config.task_queue.cancel()
|
self.config.task_queue.cancel()
|
||||||
@@ -188,9 +193,11 @@ class Executor:
|
|||||||
Notes:
|
Notes:
|
||||||
No matter the number specified, the executor will always use a minimum of 5 workers.
|
No matter the number specified, the executor will always use a minimum of 5 workers.
|
||||||
"""
|
"""
|
||||||
|
signal(SIGINT, self.sigint_handle)
|
||||||
workers_ = len(self.strategy_runners) + len(self.functions) + len(self.coroutine_threads) + 3
|
workers_ = len(self.strategy_runners) + len(self.functions) + len(self.coroutine_threads) + 3
|
||||||
workers = max(workers, workers_)
|
workers = max(workers, workers_)
|
||||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
|
signal(SIGINT, self.sigint_handle)
|
||||||
self.executor = executor
|
self.executor = executor
|
||||||
[self.executor.submit(self.run_strategy, strategy) for strategy in self.strategy_runners]
|
[self.executor.submit(self.run_strategy, strategy) for strategy in self.strategy_runners]
|
||||||
[self.executor.submit(function, **kwargs) for function, kwargs in self.functions.items()]
|
[self.executor.submit(function, **kwargs) for function, kwargs in self.functions.items()]
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
)
|
)
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
def compute_volume(self) -> float:
|
async def compute_volume(self, *args, **kwargs) -> float:
|
||||||
"""Computes the volume required for a trade usually based on the amount and any other keyword arguments.
|
"""Computes the volume required for a trade usually based on the amount and any other keyword arguments.
|
||||||
This is a dummy method that returns the minimum volume of the symbol. It is meant to be overridden by a subclass
|
This is a dummy method that returns the minimum volume of the symbol. It is meant to be overridden by a subclass
|
||||||
that implements the computation of volume.
|
that implements the computation of volume.
|
||||||
@@ -387,10 +387,10 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
return Ticks(data=ticks)
|
return Ticks(data=ticks)
|
||||||
raise ValueError(f"Could not get ticks for {self.name}.")
|
raise ValueError(f"Could not get ticks for {self.name}.")
|
||||||
|
|
||||||
def compute_volume_sl(self, *, amount: float, price: float, sl: float, round_down: bool = False) -> float:
|
async def compute_volume_sl(self, *, amount: float, price: float, sl: float, round_down: bool = False) -> float:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def compute_volume_points(self, *, amount: float, points: float, round_down: bool = False) -> float:
|
async def compute_volume_points(self, *, amount: float, points: float, round_down: bool = False) -> float:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -237,6 +237,9 @@ class Trader(ABC):
|
|||||||
else:
|
else:
|
||||||
res.save_sync()
|
res.save_sync()
|
||||||
|
|
||||||
|
def reset_order(self):
|
||||||
|
self.order = Order(symbol=self.symbol)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def place_trade(self, *args, **kwargs):
|
def place_trade(self, *args, **kwargs):
|
||||||
"""Places a trade based on the order_type."""
|
"""Places a trade based on the order_type."""
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ Example:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import datetime, UTC
|
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
@@ -146,7 +145,7 @@ class Trader(ABC):
|
|||||||
dsl = abs(price - sl)
|
dsl = abs(price - sl)
|
||||||
dtp = dsl * (risk_to_reward or self.ram.risk_to_reward)
|
dtp = dsl * (risk_to_reward or self.ram.risk_to_reward)
|
||||||
tp = price + dtp if order_type == OrderType.BUY else price - dtp
|
tp = price + dtp if order_type == OrderType.BUY else price - dtp
|
||||||
volume = self.symbol.compute_volume_sl(amount=amount, price=price, sl=sl)
|
volume = await self.symbol.compute_volume_sl(amount=amount, price=price, sl=sl)
|
||||||
self.order.set_attributes(sl=sl, tp=tp, volume=volume, price=price, type=order_type)
|
self.order.set_attributes(sl=sl, tp=tp, volume=volume, price=price, type=order_type)
|
||||||
|
|
||||||
async def create_order_with_points(
|
async def create_order_with_points(
|
||||||
@@ -167,7 +166,7 @@ class Trader(ABC):
|
|||||||
amount = await self.symbol.amount_in_quote_currency(amount=amount)
|
amount = await self.symbol.amount_in_quote_currency(amount=amount)
|
||||||
tick = await self.symbol.info_tick()
|
tick = await self.symbol.info_tick()
|
||||||
self.order.price = tick.ask if order_type == OrderType.BUY else tick.bid
|
self.order.price = tick.ask if order_type == OrderType.BUY else tick.bid
|
||||||
volume = self.symbol.compute_volume_points(amount=amount, points=points)
|
volume = await self.symbol.compute_volume_points(amount=amount, points=points)
|
||||||
self.order.volume = volume
|
self.order.volume = volume
|
||||||
self.set_trade_stop_levels_points(points=points, risk_to_reward=risk_to_reward)
|
self.set_trade_stop_levels_points(points=points, risk_to_reward=risk_to_reward)
|
||||||
|
|
||||||
@@ -238,6 +237,9 @@ class Trader(ABC):
|
|||||||
else:
|
else:
|
||||||
await res.save()
|
await res.save()
|
||||||
|
|
||||||
|
def reset_order(self):
|
||||||
|
self.order = Order(symbol=self.symbol)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def place_trade(self, *args, **kwargs):
|
async def place_trade(self, *args, **kwargs):
|
||||||
"""Places a trade based on the order_type."""
|
"""Places a trade based on the order_type."""
|
||||||
|
|||||||
@@ -1,52 +1,323 @@
|
|||||||
|
"""Comprehensive tests for the Symbol class.
|
||||||
|
|
||||||
|
This module contains live tests for the Symbol class, which provides
|
||||||
|
the interface for interacting with trading instruments in MetaTrader 5.
|
||||||
|
"""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiomql.lib.symbol import Symbol
|
from aiomql.lib.symbol import Symbol
|
||||||
from aiomql.lib.candle import Candles
|
from aiomql.lib.candle import Candles
|
||||||
from aiomql.lib.ticks import Ticks
|
from aiomql.lib.ticks import Tick, Ticks
|
||||||
|
from aiomql.core.models import SymbolInfo, BookInfo
|
||||||
|
|
||||||
|
|
||||||
class TestSymbol:
|
class TestSymbolInit:
|
||||||
@pytest.fixture(scope="class", autouse=True)
|
"""Tests for Symbol initialisation."""
|
||||||
|
|
||||||
|
async def test_requires_name(self):
|
||||||
|
"""Test that Symbol raises AssertionError without a name."""
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
Symbol()
|
||||||
|
|
||||||
|
async def test_initialized_starts_false(self):
|
||||||
|
"""Test that a newly created Symbol is not initialized."""
|
||||||
|
sym = Symbol(name="BTCUSD")
|
||||||
|
assert sym.initialized is False
|
||||||
|
|
||||||
|
async def test_has_account(self):
|
||||||
|
"""Test that a newly created Symbol has an account attribute."""
|
||||||
|
sym = Symbol(name="BTCUSD")
|
||||||
|
assert sym.account is not None
|
||||||
|
|
||||||
|
async def test_name_attribute(self):
|
||||||
|
"""Test that the name attribute is set correctly."""
|
||||||
|
sym = Symbol(name="BTCUSD")
|
||||||
|
assert sym.name == "BTCUSD"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbolInitialize:
|
||||||
|
"""Tests for Symbol.initialize() and Symbol.initialize_sync()."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
async def btc(self):
|
async def btc(self):
|
||||||
symbol = Symbol(name="BTCUSD")
|
symbol = Symbol(name="BTCUSD")
|
||||||
if symbol.initialized is False:
|
await symbol.initialize()
|
||||||
await symbol.initialize()
|
|
||||||
return symbol
|
return symbol
|
||||||
|
|
||||||
async def test_symbol_attributes(self, btc):
|
async def test_initialize_returns_true(self, btc):
|
||||||
assert btc.name == "BTCUSD"
|
"""Test that initialize() returns True for a valid symbol."""
|
||||||
|
assert btc.initialized is True
|
||||||
|
|
||||||
|
async def test_initialize_sets_select(self, btc):
|
||||||
|
"""Test that initialize() sets the select attribute."""
|
||||||
assert btc.select is True
|
assert btc.select is True
|
||||||
|
|
||||||
|
async def test_initialize_sets_tick(self, btc):
|
||||||
|
"""Test that initialize() sets the tick attribute."""
|
||||||
assert btc.tick is not None
|
assert btc.tick is not None
|
||||||
|
assert isinstance(btc.tick, Tick)
|
||||||
|
|
||||||
async def test_volume(self, btc):
|
async def test_initialize_sets_symbol_properties(self, btc):
|
||||||
|
"""Test that initialize() populates SymbolInfo properties."""
|
||||||
|
assert btc.volume_min > 0
|
||||||
|
assert btc.volume_max > 0
|
||||||
|
assert btc.volume_step > 0
|
||||||
|
assert btc.point > 0
|
||||||
|
|
||||||
|
async def test_initialize_sync(self):
|
||||||
|
"""Test that initialize_sync() works correctly."""
|
||||||
|
sym = Symbol(name="BTCUSD")
|
||||||
|
result = sym.initialize_sync()
|
||||||
|
assert result is True
|
||||||
|
assert sym.initialized is True
|
||||||
|
assert sym.tick is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbolInfo:
|
||||||
|
"""Tests for Symbol.info() and Symbol.info_tick()."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
async def btc(self):
|
||||||
|
symbol = Symbol(name="BTCUSD")
|
||||||
|
await symbol.initialize()
|
||||||
|
return symbol
|
||||||
|
|
||||||
|
async def test_info_returns_symbol_info(self, btc):
|
||||||
|
"""Test that info() returns a SymbolInfo object."""
|
||||||
|
result = await btc.info()
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, SymbolInfo)
|
||||||
|
|
||||||
|
async def test_info_tick_returns_tick(self, btc):
|
||||||
|
"""Test that info_tick() returns a Tick object."""
|
||||||
|
tick = await btc.info_tick()
|
||||||
|
assert tick is not None
|
||||||
|
assert isinstance(tick, Tick)
|
||||||
|
|
||||||
|
async def test_info_tick_updates_tick_attribute(self, btc):
|
||||||
|
"""Test that info_tick() updates the symbol's tick attribute."""
|
||||||
|
tick = await btc.info_tick()
|
||||||
|
assert btc.tick is tick
|
||||||
|
|
||||||
|
async def test_info_tick_with_name(self, btc):
|
||||||
|
"""Test info_tick() with an explicit symbol name."""
|
||||||
|
tick = await btc.info_tick(name="ETHUSD")
|
||||||
|
assert tick is not None
|
||||||
|
assert isinstance(tick, Tick)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbolSelect:
|
||||||
|
"""Tests for Symbol.symbol_select()."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
async def btc(self):
|
||||||
|
symbol = Symbol(name="BTCUSD")
|
||||||
|
await symbol.initialize()
|
||||||
|
return symbol
|
||||||
|
|
||||||
|
async def test_symbol_select_enable(self, btc):
|
||||||
|
"""Test that symbol_select(enable=True) selects the symbol."""
|
||||||
|
result = await btc.symbol_select(enable=True)
|
||||||
|
assert result is True
|
||||||
|
assert btc.select is True
|
||||||
|
|
||||||
|
async def test_symbol_select_disable_and_renable(self, btc):
|
||||||
|
"""Test toggling symbol selection off and on."""
|
||||||
|
await btc.symbol_select(enable=False)
|
||||||
|
assert btc.select is False
|
||||||
|
await btc.symbol_select(enable=True)
|
||||||
|
assert btc.select is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbolBook:
|
||||||
|
"""Tests for Symbol market depth (book) methods."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
async def btc(self):
|
||||||
|
symbol = Symbol(name="BTCUSD")
|
||||||
|
await symbol.initialize()
|
||||||
|
return symbol
|
||||||
|
|
||||||
|
async def test_book_add(self, btc):
|
||||||
|
"""Test that book_add() returns a boolean."""
|
||||||
|
result = await btc.book_add()
|
||||||
|
assert isinstance(result, bool)
|
||||||
|
|
||||||
|
async def test_book_get(self, btc):
|
||||||
|
"""Test that book_get() returns a tuple of BookInfo."""
|
||||||
|
await btc.book_add()
|
||||||
|
try:
|
||||||
|
books = await btc.book_get()
|
||||||
|
assert isinstance(books, tuple)
|
||||||
|
if len(books) > 0:
|
||||||
|
assert isinstance(books[0], BookInfo)
|
||||||
|
except ValueError:
|
||||||
|
# Market depth may not be available for all symbols
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def test_book_release(self, btc):
|
||||||
|
"""Test that book_release() returns a boolean."""
|
||||||
|
result = await btc.book_release()
|
||||||
|
assert isinstance(result, bool)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbolVolume:
|
||||||
|
"""Tests for Symbol volume-related methods."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
async def btc(self):
|
||||||
|
symbol = Symbol(name="BTCUSD")
|
||||||
|
await symbol.initialize()
|
||||||
|
return symbol
|
||||||
|
|
||||||
|
async def test_check_volume_below_min(self, btc):
|
||||||
|
"""Test check_volume with volume below minimum."""
|
||||||
volume = btc.volume_min - btc.volume_step
|
volume = btc.volume_min - btc.volume_step
|
||||||
success, volume = btc.check_volume(volume=volume)
|
success, result_vol = btc.check_volume(volume=volume)
|
||||||
assert success is False
|
assert success is False
|
||||||
volume = btc.volume_min + btc.volume_step * 2
|
assert result_vol == btc.volume_min
|
||||||
success, volume = btc.check_volume(volume=volume)
|
|
||||||
assert success is True
|
|
||||||
volume = btc.volume_min + btc.volume_step * 2.5
|
|
||||||
volume = btc.round_off_volume(volume=volume, round_down=True)
|
|
||||||
assert volume == btc.volume_min + btc.volume_step * 2
|
|
||||||
|
|
||||||
async def test_rates(self, btc):
|
async def test_check_volume_valid(self, btc):
|
||||||
|
"""Test check_volume with a valid volume."""
|
||||||
|
volume = btc.volume_min + btc.volume_step * 2
|
||||||
|
success, result_vol = btc.check_volume(volume=volume)
|
||||||
|
assert success is True
|
||||||
|
assert result_vol == volume
|
||||||
|
|
||||||
|
async def test_check_volume_above_max(self, btc):
|
||||||
|
"""Test check_volume with volume above maximum."""
|
||||||
|
volume = btc.volume_max + btc.volume_step
|
||||||
|
success, result_vol = btc.check_volume(volume=volume)
|
||||||
|
assert success is False
|
||||||
|
assert result_vol == btc.volume_max
|
||||||
|
|
||||||
|
async def test_round_off_volume(self, btc):
|
||||||
|
"""Test round_off_volume rounds to nearest step."""
|
||||||
|
volume = btc.volume_min + btc.volume_step * 2.5
|
||||||
|
rounded = btc.round_off_volume(volume=volume, round_down=True)
|
||||||
|
assert rounded == btc.volume_min + btc.volume_step * 2
|
||||||
|
|
||||||
|
async def test_round_off_volume_up(self, btc):
|
||||||
|
"""Test round_off_volume rounding up."""
|
||||||
|
volume = btc.volume_min + btc.volume_step * 2.5
|
||||||
|
rounded = btc.round_off_volume(volume=volume, round_down=False)
|
||||||
|
assert rounded == btc.volume_min + btc.volume_step * 3
|
||||||
|
|
||||||
|
async def test_compute_volume_returns_min(self, btc):
|
||||||
|
"""Test that the base compute_volume() returns volume_min."""
|
||||||
|
result = await btc.compute_volume()
|
||||||
|
assert result == btc.volume_min
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbolCurrency:
|
||||||
|
"""Tests for Symbol currency conversion methods."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
async def btc(self):
|
||||||
|
symbol = Symbol(name="BTCUSD")
|
||||||
|
await symbol.initialize()
|
||||||
|
return symbol
|
||||||
|
|
||||||
|
async def test_amount_in_quote_currency(self, btc):
|
||||||
|
"""Test amount_in_quote_currency returns a float."""
|
||||||
|
result = await btc.amount_in_quote_currency(amount=100.0)
|
||||||
|
assert isinstance(result, (int, float))
|
||||||
|
|
||||||
|
async def test_convert_currency(self, btc):
|
||||||
|
"""Test convert_currency between two currencies."""
|
||||||
|
result = await btc.convert_currency(
|
||||||
|
amount=100.0, from_currency="USD", to_currency="EUR"
|
||||||
|
)
|
||||||
|
# Result may be None if the conversion pair is not available
|
||||||
|
if result is not None:
|
||||||
|
assert isinstance(result, float)
|
||||||
|
assert result > 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbolRates:
|
||||||
|
"""Tests for Symbol copy rates and ticks methods."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
async def btc(self):
|
||||||
|
symbol = Symbol(name="BTCUSD")
|
||||||
|
await symbol.initialize()
|
||||||
|
return symbol
|
||||||
|
|
||||||
|
async def test_copy_rates_from(self, btc):
|
||||||
|
"""Test copy_rates_from returns Candles."""
|
||||||
|
start = datetime(year=2023, month=10, day=5)
|
||||||
|
rates = await btc.copy_rates_from(
|
||||||
|
timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, count=10
|
||||||
|
)
|
||||||
|
assert isinstance(rates, Candles)
|
||||||
|
assert len(rates) == 10
|
||||||
|
|
||||||
|
async def test_copy_rates_from_pos(self, btc):
|
||||||
|
"""Test copy_rates_from_pos returns Candles."""
|
||||||
|
rates = await btc.copy_rates_from_pos(
|
||||||
|
timeframe=btc.mt5.TIMEFRAME_H1, count=10, start_position=0
|
||||||
|
)
|
||||||
|
assert isinstance(rates, Candles)
|
||||||
|
assert len(rates) == 10
|
||||||
|
|
||||||
|
async def test_copy_rates_range(self, btc):
|
||||||
|
"""Test copy_rates_range returns Candles."""
|
||||||
start = datetime(year=2023, month=10, day=5)
|
start = datetime(year=2023, month=10, day=5)
|
||||||
end = start + timedelta(hours=9)
|
end = start + timedelta(hours=9)
|
||||||
rates_from = await btc.copy_rates_from(timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, count=10)
|
rates = await btc.copy_rates_range(
|
||||||
assert isinstance(rates_from, Candles)
|
timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, date_to=end
|
||||||
assert len(rates_from) == 10
|
)
|
||||||
rates_from_pos = await btc.copy_rates_from_pos(timeframe=btc.mt5.TIMEFRAME_H1, count=10, start_position=0)
|
assert isinstance(rates, Candles)
|
||||||
assert isinstance(rates_from_pos, Candles)
|
assert len(rates) == 10
|
||||||
assert len(rates_from_pos) == 10
|
|
||||||
rates_range = await btc.copy_rates_range(timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, date_to=end)
|
async def test_copy_ticks_from(self, btc):
|
||||||
assert isinstance(rates_range, Candles)
|
"""Test copy_ticks_from returns Ticks."""
|
||||||
assert len(rates_range) == 10
|
start = datetime(year=2023, month=10, day=5)
|
||||||
ticks_from = await btc.copy_ticks_from(date_from=start, count=10)
|
ticks = await btc.copy_ticks_from(date_from=start, count=10)
|
||||||
assert isinstance(ticks_from, Ticks)
|
assert isinstance(ticks, Ticks)
|
||||||
assert len(ticks_from) == 10
|
assert len(ticks) == 10
|
||||||
|
|
||||||
|
async def test_copy_ticks_range(self, btc):
|
||||||
|
"""Test copy_ticks_range returns Ticks."""
|
||||||
|
start = datetime(year=2023, month=10, day=5)
|
||||||
end = start + timedelta(seconds=20)
|
end = start + timedelta(seconds=20)
|
||||||
ticks_from_pos = await btc.copy_ticks_range(date_from=start, date_to=end)
|
ticks = await btc.copy_ticks_range(date_from=start, date_to=end)
|
||||||
assert isinstance(ticks_from_pos, Ticks)
|
assert isinstance(ticks, Ticks)
|
||||||
assert len(ticks_from_pos) >= 10
|
assert len(ticks) >= 10
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbolProperties:
|
||||||
|
"""Tests for Symbol properties."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
async def btc(self):
|
||||||
|
symbol = Symbol(name="BTCUSD")
|
||||||
|
await symbol.initialize()
|
||||||
|
return symbol
|
||||||
|
|
||||||
|
async def test_pip_property(self, btc):
|
||||||
|
"""Test that pip equals point * 10."""
|
||||||
|
assert btc.pip == btc.point * 10
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbolAbstract:
|
||||||
|
"""Tests for Symbol abstract/overridable methods."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
async def btc(self):
|
||||||
|
symbol = Symbol(name="BTCUSD")
|
||||||
|
await symbol.initialize()
|
||||||
|
return symbol
|
||||||
|
|
||||||
|
async def test_compute_volume_sl_raises(self, btc):
|
||||||
|
"""Test that compute_volume_sl raises NotImplementedError."""
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
await btc.compute_volume_sl(amount=100.0, price=50000.0, sl=49500.0)
|
||||||
|
|
||||||
|
async def test_compute_volume_points_raises(self, btc):
|
||||||
|
"""Test that compute_volume_points raises NotImplementedError."""
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
await btc.compute_volume_points(amount=100.0, points=500.0)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ resolution-markers = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiomql"
|
name = "aiomql"
|
||||||
version = "4.0.17"
|
version = "4.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "metatrader5" },
|
{ name = "metatrader5" },
|
||||||
@@ -22,6 +22,12 @@ dependencies = [
|
|||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
all = [
|
all = [
|
||||||
|
{ name = "cython" },
|
||||||
|
{ name = "numba" },
|
||||||
|
{ name = "ta-lib" },
|
||||||
|
{ name = "tqdm" },
|
||||||
|
]
|
||||||
|
optional = [
|
||||||
{ name = "cython" },
|
{ name = "cython" },
|
||||||
{ name = "numba" },
|
{ name = "numba" },
|
||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
@@ -40,15 +46,16 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "cython", marker = "extra == 'all'", specifier = ">=3.2.4" },
|
{ name = "aiomql", extras = ["talib", "optional"], marker = "extra == 'all'" },
|
||||||
|
{ name = "cython", marker = "extra == 'optional'", specifier = ">=3.2.4" },
|
||||||
{ name = "metatrader5", specifier = ">=5.0.5640" },
|
{ name = "metatrader5", specifier = ">=5.0.5640" },
|
||||||
{ name = "mplfinance", specifier = ">=0.12.10b0" },
|
{ name = "mplfinance", specifier = ">=0.12.10b0" },
|
||||||
{ name = "numba", marker = "extra == 'all'", specifier = ">=0.64.0" },
|
{ name = "numba", marker = "extra == 'optional'", specifier = ">=0.64.0" },
|
||||||
{ name = "pandas", specifier = ">=3.0.1" },
|
{ name = "pandas", specifier = ">=3.0.1" },
|
||||||
{ name = "ta-lib", marker = "extra == 'talib'", specifier = ">=0.6.8" },
|
{ name = "ta-lib", marker = "extra == 'talib'", specifier = ">=0.6.8" },
|
||||||
{ name = "tqdm", marker = "extra == 'all'", specifier = ">=4.67.3" },
|
{ name = "tqdm", marker = "extra == 'optional'", specifier = ">=4.67.3" },
|
||||||
]
|
]
|
||||||
provides-extras = ["all", "talib"]
|
provides-extras = ["optional", "talib", "all"]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
Reference in New Issue
Block a user