mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-07-27 20:27:43 +00:00
v4.1.0
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
# 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
|
||||
|
||||
Extends [`Symbol`](../../lib/symbol.md) with forex-specific logic for pip size,
|
||||
pip value, and volume calculations based on currency pairs.
|
||||
Extends [`Symbol`](../../lib/symbol.md) with forex-specific logic for pip size
|
||||
and volume calculations based on price movements and stop-loss levels.
|
||||
|
||||
## Classes
|
||||
|
||||
@@ -17,13 +17,12 @@ Inherits from [`Symbol`](../../lib/symbol.md).
|
||||
|
||||
| Attribute | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `pip` | `float` | Pip size for the pair |
|
||||
| `pip` | `float` | Pip size for the pair (`point * 10`) |
|
||||
|
||||
#### Methods
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `pip_value(volume)` | `float` | Value of one pip for a given lot size |
|
||||
| `pips_to_price(pips)` | `float` | Converts a pip count to a price delta |
|
||||
| `price_to_pips(price_delta)` | `float` | Converts a price delta to pips |
|
||||
| `calc_volume(amount, pips)` | `float` | Calculates lot size from risk amount and pip distance |
|
||||
| `compute_points(amount, volume)` | `float` | Computes points of price movement needed for a given amount and volume |
|
||||
| `compute_volume_points(amount, points, round_down=False)` | `float` | Computes lot size from risk amount and point distance |
|
||||
| `compute_volume_sl(amount, price, sl, round_down=False)` | `float` | Computes lot size from risk amount, entry price, and stop-loss price |
|
||||
|
||||
+31
-9
@@ -7,7 +7,7 @@
|
||||
The `Order` class creates and manages trade orders for the MetaTrader 5 terminal. It handles
|
||||
margin calculations, profit projections, order validation, modification, and cancellation.
|
||||
|
||||
Inherits from [`_Base`](../core/base.md).
|
||||
Inherits from [`_Base`](../core/base.md) and `TradeRequest`.
|
||||
|
||||
## Classes
|
||||
|
||||
@@ -30,32 +30,54 @@ Inherits from [`_Base`](../core/base.md).
|
||||
| `type_filling` | `OrderFilling` | Filling 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)*
|
||||
|
||||
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
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `check()` | `OrderCheckResult` | Validates the order, raises `OrderError` on failure |
|
||||
| `check(**kwargs)` | `OrderCheckResult` | Validates the order; raises `OrderError` on failure |
|
||||
|
||||
#### Execution
|
||||
|
||||
| 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
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `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
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `modify(**kwargs)` | Modifies a pending order's parameters |
|
||||
| `cancel()` | Cancels a pending order |
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `modify(**kwargs)` | — | Updates the order's attributes |
|
||||
| `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
|
||||
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
|
||||
|
||||
@@ -19,33 +19,76 @@ Inherits from [`_Base`](../core/base.md).
|
||||
|-----------|------|-------------|
|
||||
| `name` | `str` | Symbol name (e.g. `"EURUSD"`) |
|
||||
| `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.
|
||||
|
||||
#### Initialisation
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `init()` | Fetches symbol info from the terminal and sets all attributes |
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `__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
|
||||
|
||||
| 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_pos(timeframe, start_pos, count)` | `Candles` | Historical bars from a position |
|
||||
| `copy_rates_range(timeframe, date_from, date_to)` | `Candles` | Historical bars in a range |
|
||||
| `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 date range |
|
||||
| `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 |
|
||||
|----------|---------|-------------|
|
||||
| `pip` | `float` | The pip size for the symbol |
|
||||
| `spread` | `float` | Current bid-ask spread |
|
||||
| `pip` | `float` | Pip size (`point * 10`) |
|
||||
|
||||
#### 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
|
||||
|
||||
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
|
||||
# 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,
|
||||
'timeout': 120, "macd": 87, "sma": 90}
|
||||
'timeout': 120}
|
||||
|
||||
def __init__(self, *, symbol: ForexSymbol, params: dict | None = None, trader: Trader = None,
|
||||
sessions: Sessions = None, name: str = "EMAXOver"):
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "aiomql"
|
||||
version = "4.0.17"
|
||||
version = "4.1.0"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
classifiers = [
|
||||
|
||||
@@ -85,7 +85,7 @@ class ForexSymbol(Symbol):
|
||||
points = amount / (volume * self.point * self.trade_contract_size)
|
||||
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.
|
||||
|
||||
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)
|
||||
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.
|
||||
|
||||
Calculates the appropriate trade volume to risk a specified amount
|
||||
|
||||
@@ -12,7 +12,6 @@ import asyncio
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar, ClassVar
|
||||
from logging import getLogger
|
||||
import logging
|
||||
|
||||
from ...core import Config, State, sleep
|
||||
from ...lib import Positions
|
||||
|
||||
@@ -41,7 +41,6 @@ async def auto_commit():
|
||||
try:
|
||||
with config.state.conn as conn:
|
||||
while config.shutdown is False:
|
||||
print("committing state to the database")
|
||||
await config.state.acommit(conn=conn, close=False)
|
||||
await sleep(config.db_commit_interval)
|
||||
except Exception as err:
|
||||
|
||||
@@ -151,6 +151,7 @@ class Executor:
|
||||
signum: The signal number received.
|
||||
frame: The current stack frame.
|
||||
"""
|
||||
print("shutting down")
|
||||
self.config.shutdown = True
|
||||
|
||||
def exit(self):
|
||||
@@ -162,12 +163,16 @@ class Executor:
|
||||
"""
|
||||
start = time.time()
|
||||
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):
|
||||
self.config.shutdown = True
|
||||
break
|
||||
timeout = self.timeout or 1
|
||||
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:
|
||||
strategy.running = False
|
||||
self.config.task_queue.cancel()
|
||||
@@ -188,9 +193,11 @@ class Executor:
|
||||
Notes:
|
||||
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 = max(workers, workers_)
|
||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
signal(SIGINT, self.sigint_handle)
|
||||
self.executor = executor
|
||||
[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()]
|
||||
|
||||
@@ -233,7 +233,7 @@ class Symbol(_Base, SymbolInfo):
|
||||
)
|
||||
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.
|
||||
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.
|
||||
@@ -387,10 +387,10 @@ class Symbol(_Base, SymbolInfo):
|
||||
return Ticks(data=ticks)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
|
||||
@@ -237,6 +237,9 @@ class Trader(ABC):
|
||||
else:
|
||||
res.save_sync()
|
||||
|
||||
def reset_order(self):
|
||||
self.order = Order(symbol=self.symbol)
|
||||
|
||||
@abstractmethod
|
||||
def place_trade(self, *args, **kwargs):
|
||||
"""Places a trade based on the order_type."""
|
||||
|
||||
@@ -17,7 +17,6 @@ Example:
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, UTC
|
||||
from typing import TypeVar
|
||||
from logging import getLogger
|
||||
|
||||
@@ -146,7 +145,7 @@ class Trader(ABC):
|
||||
dsl = abs(price - sl)
|
||||
dtp = dsl * (risk_to_reward or self.ram.risk_to_reward)
|
||||
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)
|
||||
|
||||
async def create_order_with_points(
|
||||
@@ -167,7 +166,7 @@ class Trader(ABC):
|
||||
amount = await self.symbol.amount_in_quote_currency(amount=amount)
|
||||
tick = await self.symbol.info_tick()
|
||||
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.set_trade_stop_levels_points(points=points, risk_to_reward=risk_to_reward)
|
||||
|
||||
@@ -238,6 +237,9 @@ class Trader(ABC):
|
||||
else:
|
||||
await res.save()
|
||||
|
||||
def reset_order(self):
|
||||
self.order = Order(symbol=self.symbol)
|
||||
|
||||
@abstractmethod
|
||||
async def place_trade(self, *args, **kwargs):
|
||||
"""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
|
||||
|
||||
import pytest
|
||||
|
||||
from aiomql.lib.symbol import Symbol
|
||||
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:
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
class TestSymbolInit:
|
||||
"""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):
|
||||
symbol = Symbol(name="BTCUSD")
|
||||
if symbol.initialized is False:
|
||||
await symbol.initialize()
|
||||
await symbol.initialize()
|
||||
return symbol
|
||||
|
||||
async def test_symbol_attributes(self, btc):
|
||||
assert btc.name == "BTCUSD"
|
||||
async def test_initialize_returns_true(self, btc):
|
||||
"""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
|
||||
|
||||
async def test_initialize_sets_tick(self, btc):
|
||||
"""Test that initialize() sets the tick attribute."""
|
||||
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
|
||||
success, volume = btc.check_volume(volume=volume)
|
||||
success, result_vol = btc.check_volume(volume=volume)
|
||||
assert success is False
|
||||
volume = btc.volume_min + btc.volume_step * 2
|
||||
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
|
||||
assert result_vol == btc.volume_min
|
||||
|
||||
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)
|
||||
end = start + timedelta(hours=9)
|
||||
rates_from = await btc.copy_rates_from(timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, count=10)
|
||||
assert isinstance(rates_from, Candles)
|
||||
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_from_pos, Candles)
|
||||
assert len(rates_from_pos) == 10
|
||||
rates_range = await btc.copy_rates_range(timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, date_to=end)
|
||||
assert isinstance(rates_range, Candles)
|
||||
assert len(rates_range) == 10
|
||||
ticks_from = await btc.copy_ticks_from(date_from=start, count=10)
|
||||
assert isinstance(ticks_from, Ticks)
|
||||
assert len(ticks_from) == 10
|
||||
rates = await btc.copy_rates_range(
|
||||
timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, date_to=end
|
||||
)
|
||||
assert isinstance(rates, Candles)
|
||||
assert len(rates) == 10
|
||||
|
||||
async def test_copy_ticks_from(self, btc):
|
||||
"""Test copy_ticks_from returns Ticks."""
|
||||
start = datetime(year=2023, month=10, day=5)
|
||||
ticks = await btc.copy_ticks_from(date_from=start, count=10)
|
||||
assert isinstance(ticks, Ticks)
|
||||
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)
|
||||
ticks_from_pos = await btc.copy_ticks_range(date_from=start, date_to=end)
|
||||
assert isinstance(ticks_from_pos, Ticks)
|
||||
assert len(ticks_from_pos) >= 10
|
||||
ticks = await btc.copy_ticks_range(date_from=start, date_to=end)
|
||||
assert isinstance(ticks, Ticks)
|
||||
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]]
|
||||
name = "aiomql"
|
||||
version = "4.0.17"
|
||||
version = "4.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "metatrader5" },
|
||||
@@ -22,6 +22,12 @@ dependencies = [
|
||||
|
||||
[package.optional-dependencies]
|
||||
all = [
|
||||
{ name = "cython" },
|
||||
{ name = "numba" },
|
||||
{ name = "ta-lib" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
optional = [
|
||||
{ name = "cython" },
|
||||
{ name = "numba" },
|
||||
{ name = "tqdm" },
|
||||
@@ -40,15 +46,16 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
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 = "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 = "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]
|
||||
dev = [
|
||||
|
||||
Reference in New Issue
Block a user