diff --git a/pyproject.toml b/pyproject.toml index 5d79eae..aab16dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "aiomql" -version = "3.0.4" +version = "3.0.5" readme = "README.md" requires-python = ">=3.10" classifiers = [ diff --git a/src/aiomql/bot_builder.py b/src/aiomql/bot_builder.py index 3f60c58..228bfac 100644 --- a/src/aiomql/bot_builder.py +++ b/src/aiomql/bot_builder.py @@ -19,7 +19,7 @@ class Bot: Attributes: account (Account): Account Object. executor: The default thread executor. - symbols (set[Symbols]): A set of symbols for the trading session + symbols (list[Symbols]): A set of symbols for the trading session """ account: Account = Account() @@ -82,7 +82,6 @@ class Bot: Notes: Make sure the symbol has been added to the market """ - self.symbols.add(strategy.symbol) self.executor.add_worker(strategy) def add_strategies(self, strategies: Iterable[Strategy]): @@ -104,9 +103,8 @@ class Bot: [self.add_strategy(strategy(symbol=symbol, params=params)) for symbol in self.symbols] async def init_symbols(self): - """Initialize the symbols for the current trading session. This method is called internally by the bot. - """ - syms = [self.init_symbol(symbol) for symbol in self.symbols] + """Initialize the symbols for the current trading session. This method is called internally by the bot.""" + syms = [self.init_symbol(strategy.symbol) for strategy in self.executor.workers] await asyncio.gather(*syms, return_exceptions=True) async def init_symbol(self, symbol: Symbol) -> Symbol: @@ -123,9 +121,7 @@ class Bot: if self.account.has_symbol(symbol): init = await symbol.init() if init: + self.symbols.add(symbol) return symbol - self.symbols.discard(symbol) logger.warning(f'Unable to initialize symbol {symbol}') - - self.symbols.remove(symbol) logger.warning(f'{symbol} not a available for this market') diff --git a/src/aiomql/executor.py b/src/aiomql/executor.py index f58c88f..38c1687 100644 --- a/src/aiomql/executor.py +++ b/src/aiomql/executor.py @@ -13,7 +13,6 @@ class Executor: workers (list): List of strategies. coroutines (dict[Coroutine, dict]): A dictionary of coroutines to run in the executor functions (dict[Callable, dict]): A dictionary of functions to run in the executor - """ def __init__(self, bot=None): diff --git a/src/aiomql/strategy.py b/src/aiomql/strategy.py index 729296e..9e76a27 100644 --- a/src/aiomql/strategy.py +++ b/src/aiomql/strategy.py @@ -47,7 +47,8 @@ class Strategy(ABC): self.parameters = params.copy() if isinstance(params, dict) else {} self.parameters['symbol'] = symbol.name self.parameters['name'] = self.name or self.__class__.__name__ - self.sessions = sessions or Sessions(Session(start=0, end=23)) + self.sessions = sessions or Sessions(Session(start=0, end=dtime(hour=23, minute=59, second=59, + microsecond=999999))) def __repr__(self): return f"{self.name}({self.symbol!r})" diff --git a/src/aiomql/symbol.py b/src/aiomql/symbol.py index 98350b6..fb2ef5e 100644 --- a/src/aiomql/symbol.py +++ b/src/aiomql/symbol.py @@ -1,6 +1,7 @@ """Symbol class for handling a financial instrument.""" from datetime import datetime from logging import getLogger +from math import log10, ceil from .core.constants import TimeFrame, CopyTicks from .core.models import SymbolInfo, BookInfo @@ -135,7 +136,20 @@ class Symbol(SymbolInfo): """ return await self.mt5.market_book_release(self.name) - async def compute_volume(self, *, amount: float, pips: float, use_limits: bool = True) -> float: + def check_volume(self, volume) -> tuple[bool, float]: + check = self.volume_min <= volume <= self.volume_max + if check: + return check, volume + if not check and volume < self.volume_min: + return check, self.volume_min + else: + return check, self.volume_max + + def round_off_volume(self, volume): + step = ceil(abs(log10(self.volume_step))) + return round(volume, step) + + async def compute_volume(self, *, amount: float, pips: float, use_limits: bool = False) -> float: """Computes the volume of a trade based on the amount and the number of pips to target. 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. diff --git a/src/aiomql/trader.py b/src/aiomql/trader.py index b70cde9..9e45c58 100644 --- a/src/aiomql/trader.py +++ b/src/aiomql/trader.py @@ -54,7 +54,8 @@ class Trader: """ # check if pips is passed in as a keyword argument, if not use the pips attribute of the ram instance pips = kwargs.get('pips', 0) or self.ram.pips - self.order.volume = kwargs.get('volume', self.ram.volume) or await self.ram.get_volume(symbol=self.symbol, pips=pips) + self.order.volume = kwargs.get('volume', self.ram.volume) or await self.ram.get_volume(symbol=self.symbol, + pips=pips) self.order.type = order_type await self.set_order_limits(pips=pips) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..7e57de2 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,10 @@ +from aiomql import MetaTrader +import pytest + +from .fixtures import * + + +@pytest.mark.asyncio +class BaseTest: + """""" + mt5 = MetaTrader() diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 0000000..41fab9f --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,31 @@ +import json +import os +from aiomql import MetaTrader as mt5, Config +import pytest + + +@pytest.fixture(scope="session") +def get_default_config(): + data = {"win_percentage": 0.90, "record_dir": "Trade Records"} + obj = open('mt5.json', 'w') + json.dump(data, obj) + obj.close() + yield + os.remove('mt5.json') + + +@pytest.fixture(scope="session") +def get_config(): + data = {"win_percentage": 0.8, "record_dir": "Trade_Records"} + obj = open('config.json', 'w') + json.dump(data, obj) + obj.close() + yield + os.remove('config.json') + + +@pytest.fixture(autouse=True, scope="session") +def init(): + config = Config(filename="test_config.json") + mt5._initialize() + mt5._login(login=config.account_number, password=config.password, server=config.server) diff --git a/tests/test_config.json b/tests/test_config.json new file mode 100644 index 0000000..b5b54f6 --- /dev/null +++ b/tests/test_config.json @@ -0,0 +1,5 @@ +{ + "account_number": 160286827, + "password": "TheN@me0fTheW!nd", + "server": "ForexTimeFXTM-Demo01" +} \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..8866ccb --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,13 @@ +from aiomql import config + +from . import get_config, get_default_config + + +def test_default_config_file(get_default_config): + conf = config.Config() + assert conf.win_percentage == 0.90 + + +def test_config_file_name(get_config): + conf = config.Config(filename='config.json') + assert conf.win_percentage == 0.8 diff --git a/tests/test_constants.py b/tests/test_constants.py new file mode 100644 index 0000000..b07d8a2 --- /dev/null +++ b/tests/test_constants.py @@ -0,0 +1,7 @@ +from aiomql import TradeAction + + +class TestConstants: + + def test_trade_action(self): + assert TradeAction.DEAL == 1 diff --git a/tests/test_symbol.py b/tests/test_symbol.py new file mode 100644 index 0000000..f5e3b44 --- /dev/null +++ b/tests/test_symbol.py @@ -0,0 +1,11 @@ +from aiomql.symbol import Symbol + +from . import * + + +class TestSymbol(BaseTest): + sym = Symbol(name="EURJPY") + + async def test_init(self): + await self.sym.init() + assert self.sym.select is True diff --git a/tests/test_terminal.py b/tests/test_terminal.py new file mode 100644 index 0000000..80568c8 --- /dev/null +++ b/tests/test_terminal.py @@ -0,0 +1,22 @@ +from . import * +from aiomql import Terminal + + +class TestTerminal(BaseTest): + terminal = Terminal() + async def test_version(self): + res = await self.terminal.version + assert len(res) == 3 + + async def test_info(self): + res = await self.terminal.info() + assert res.connected is True + + async def test_error(self): + res = await self.terminal.last_error() + assert res.code == 1 + + async def test_symbols_get(self): + res = await self.terminal.symbols_get() + sym = next(res) + assert isinstance(sym.name, str) diff --git a/utils/meta_tester.py b/utils/meta_tester.py new file mode 100644 index 0000000..6079a5d --- /dev/null +++ b/utils/meta_tester.py @@ -0,0 +1,91 @@ +# from datetime import datetime +# from collections import defaultdict +# from pickle import HIGHEST_PROTOCOL +# import _pickle as pickle +# import lzma +# import asyncio +# from itertools import product +# from typing import Iterable, TypeAlias +# +# from .meta_trader import MetaTrader +# from .constants import TimeFrame +# from .. import account, Account, Ticks, Symbol, Candles +# +# Rates: TypeAlias = dict[Symbol, dict[TimeFrame, Candles]] +# PriceTicks: TypeAlias = dict[Symbol, Ticks] +# +# +# class MetaTester(MetaTrader): + +# def __init__(self, *, file=None, data: 'TestData' = None): +# self.file = file +# +# @property +# def data(self): +# return TestData.load(self.file) +# +# +# class TestData: +# rates: Rates +# ticks: PriceTicks +# account: Account +# +# def __init__(self, symbols: Iterable[Symbol], timeframes: Iterable[TimeFrame], start: datetime, end: datetime, file: str): +# self.symbols = symbols +# self.timeframes = timeframes +# self.start = start +# self.end = end +# self.file = file +# +# @property +# async def _account(self) -> Account: +# await account.refresh() +# return account +# +# @property +# async def _ticks(self) -> PriceTicks: +# tasks = [] +# symbols = [] +# for symbol in self.symbols: +# coro = symbol.copy_ticks_range(date_from=self.start, date_to=self.end) +# symbols.append(symbol) +# tasks.append(asyncio.create_task(coro)) +# ticks = await asyncio.gather(*tasks) +# return {symbol: ticks for symbol, ticks in zip(symbols, ticks)} +# +# @property +# async def _rates(self) -> Rates: +# _data = {'tasks': [], 'symbols': [], 'timeframes': []} +# args: Iterable[tuple[Symbol, TimeFrame]] = product(self.symbols, self.timeframes) +# for symbol, timeframe in args: +# coro = symbol.copy_rates_range(date_from=self.start, date_to=self.end, timeframe=timeframe) +# _data['tasks'].append(asyncio.create_task(coro)) +# _data['symbols'].append(symbol) +# _data['timeframes'].append(timeframe) +# _data['rates'] = await asyncio.gather(*_data['tasks']) +# +# data = defaultdict(dict) +# for rates, symbol, timeframe in zip(_data['rates'], _data['symbols'], _data['timeframes']): +# data[symbol] |= {timeframe: rates} +# return data +# +# async def copy_data(self): +# self.rates, self.ticks, self.account = await asyncio.gather(self._rates, self._ticks, self._account) +# +# async def dumps(self): +# return pickle.dumps(self, protocol=HIGHEST_PROTOCOL) +# +# async def dump(self): +# await self.copy_data() +# with lzma.open(self.file, 'wb') as fh: +# pickle.dump(self, fh, protocol=HIGHEST_PROTOCOL) +# +# @classmethod +# def load(cls, file) -> 'TestData': +# with lzma.open(file, 'rb') as fh: +# return pickle.load(fh) +# +# @classmethod +# def loads(cls, obj): +# return pickle.loads(obj) +#