This commit is contained in:
Ichinga Samuel
2026-02-27 04:11:38 +01:00
parent cb2c700752
commit eed7e80c0d
15 changed files with 430 additions and 78 deletions
View File
+7 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -1,6 +1,6 @@
[project]
name = "aiomql"
version = "4.0.17"
version = "4.1.0"
readme = "README.md"
requires-python = ">=3.13"
classifiers = [
+2 -2
View File
@@ -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
-1
View File
@@ -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:
+8 -1
View File
@@ -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()]
+3 -3
View File
@@ -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
+3
View File
@@ -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."""
+5 -3
View File
@@ -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."""
+302 -31
View File
@@ -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)
Generated
+12 -5
View File
@@ -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 = [