diff --git a/backtesting/backtest_data_01_05_24_06_05_24.json b/backtesting/backtest_data_01_05_24_06_05_24.json new file mode 100644 index 0000000..7c255b0 --- /dev/null +++ b/backtesting/backtest_data_01_05_24_06_05_24.json @@ -0,0 +1,17 @@ +{ + "balance": 281.16, + "profit": 0, + "equity": 281.16, + "margin": 0.0, + "margin_free": 281.16, + "margin_level": 0, + "wins": 85, + "losses": 95, + "total": 180, + "win_percentage": 47.22, + "win": 877.56, + "loss": -946.4, + "net_profit": -68.84, + "profit_factor": 0.93, + "profitability": -19.67 +} \ No newline at end of file diff --git a/backtesting/backtest_data_01_11_24.json b/backtesting/backtest_data_01_11_24.json new file mode 100644 index 0000000..3514c28 --- /dev/null +++ b/backtesting/backtest_data_01_11_24.json @@ -0,0 +1 @@ +{"balance": 100, "profit": -11.76, "equity": 88.24, "margin": 88.87000000000005, "margin_free": -0.6300000000000523, "margin_level": 99.29109935861365} \ No newline at end of file diff --git a/backtesting/backtest_data_02_11_24.json b/backtesting/backtest_data_02_11_24.json new file mode 100644 index 0000000..5e4ff4c --- /dev/null +++ b/backtesting/backtest_data_02_11_24.json @@ -0,0 +1,8 @@ +{ + "balance": 131.67, + "profit": 0, + "equity": 131.67, + "margin": 0.0, + "margin_free": 131.67, + "margin_level": 0 +} \ No newline at end of file diff --git a/backtesting/backtest_data_03_11_24.json b/backtesting/backtest_data_03_11_24.json new file mode 100644 index 0000000..f02ead8 --- /dev/null +++ b/backtesting/backtest_data_03_11_24.json @@ -0,0 +1,8 @@ +{ + "balance": 590.45, + "profit": 0, + "equity": 590.45, + "margin": 0.0, + "margin_free": 590.45, + "margin_level": 0 +} \ No newline at end of file diff --git a/backtesting/backtest_data_30_10_24.json b/backtesting/backtest_data_30_10_24.json new file mode 100644 index 0000000..a22645f --- /dev/null +++ b/backtesting/backtest_data_30_10_24.json @@ -0,0 +1 @@ +{"balance": 0, "profit": -0.53, "equity": -0.53, "margin": 2.03, "margin_free": -2.5599999999999996, "margin_level": -26.108374384236456} \ No newline at end of file diff --git a/check.py b/check.py deleted file mode 100644 index de6c57d..0000000 --- a/check.py +++ /dev/null @@ -1,62 +0,0 @@ -import asyncio -from datetime import datetime, UTC - -from aiomql.contrib.backtesting.get_data import BackTestData, GetData -from aiomql.contrib.backtesting.backtest_engine import BackTestEngine -from aiomql.core.constants import TimeFrame -from aiomql.core.meta_trader import MetaTrader - - -async def get_data(): - mt = MetaTrader() - await mt.login() - start = datetime(2024, 2, 1, tzinfo=UTC) - end = datetime(2024, 2, 3, tzinfo=UTC) - symbols = ['BTCUSD', "ETHUSD", "SOLUSD"] - timeframes = [TimeFrame.H1, TimeFrame.H2, TimeFrame.M5] - g_data = GetData(start=start, end=end, symbols=symbols, timeframes=timeframes, - name='test_data') - s = datetime.now().timestamp() - await g_data.get_data(workers=500) - e = datetime.now().timestamp() - print(e - s, 'for getting data') - - s = datetime.now().timestamp() - g_data.save_data() - e = datetime.now().timestamp() - print(e - s, 'for saving data') - - -async def test_data(): - start = datetime.now().timestamp() - td = GetData.load_data(name='backtesting/test_data.pkl') - end = datetime.now().timestamp() - print(end-start, 'seconds') - print(td.name) - print(td.version) - print(len(td.ticks.keys())) - print(len(td.symbols.keys())) - print(td.rates.keys()) - -async def back_test_engine(): - td = GetData.load_data(name='backtesting/test_data.pkl') - bt = BackTestEngine(name='test_data_2', start=datetime(2024, 2, 1), - end=datetime(2024, 2, 7)) - # bt.next() - bt.next() - # print(bt.cursor) - # now = bt.cursor.time - # r = bt.cursor.index - # bt.fast_forward(steps=20) - # print(bt.cursor.time == now + 20) - # print(bt.cursor.index == r + 20) - # print(bt.cursor, r) - # print(bt.cursor) - print(bt.range, bt.span) - bt.go_to(time=datetime(2024, 2, 13)) - print(bt.cursor) - print(datetime.fromtimestamp(bt.cursor.time)) - print(bt.range, bt.span) - - -asyncio.run(get_data()) diff --git a/google_pylintrc b/google_pylintrc deleted file mode 100644 index 5c771e6..0000000 --- a/google_pylintrc +++ /dev/null @@ -1,399 +0,0 @@ -# This Pylint rcfile contains a best-effort configuration to uphold the -# best-practices and style described in the Google Python style guide: -# https://google.github.io/styleguide/pyguide.html -# -# Its canonical open-source location is: -# https://google.github.io/styleguide/pylintrc - -[MAIN] - -# Files or directories to be skipped. They should be base names, not paths. -ignore=third_party - -# Files or directories matching the regex patterns are skipped. The regex -# matches against base names, not paths. -ignore-patterns= - -# Pickle collected data for later comparisons. -persistent=no - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -jobs=4 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=R, - abstract-method, - apply-builtin, - arguments-differ, - attribute-defined-outside-init, - backtick, - bad-option-value, - basestring-builtin, - buffer-builtin, - c-extension-no-member, - consider-using-enumerate, - cmp-builtin, - cmp-method, - coerce-builtin, - coerce-method, - delslice-method, - div-method, - eq-without-hash, - execfile-builtin, - file-builtin, - filter-builtin-not-iterating, - fixme, - getslice-method, - global-statement, - hex-method, - idiv-method, - implicit-str-concat, - import-error, - import-self, - import-star-module-level, - input-builtin, - intern-builtin, - invalid-str-codec, - locally-disabled, - long-builtin, - long-suffix, - map-builtin-not-iterating, - misplaced-comparison-constant, - missing-function-docstring, - metaclass-assignment, - next-method-called, - next-method-defined, - no-absolute-import, - no-init, # added - no-member, - no-name-in-module, - no-self-use, - nonzero-method, - oct-method, - old-division, - old-ne-operator, - old-octal-literal, - old-raise-syntax, - parameter-unpacking, - print-statement, - raising-string, - range-builtin-not-iterating, - raw_input-builtin, - rdiv-method, - reduce-builtin, - relative-import, - reload-builtin, - round-builtin, - setslice-method, - signature-differs, - standarderror-builtin, - suppressed-message, - sys-max-int, - trailing-newlines, - unichr-builtin, - unicode-builtin, - unnecessary-pass, - unpacking-in-except, - useless-else-on-loop, - useless-suppression, - using-cmp-argument, - wrong-import-order, - xrange-builtin, - zip-builtin-not-iterating, - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages -reports=no - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[BASIC] - -# Good variable names which should always be accepted, separated by a comma -good-names=main,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl - -# Regular expression matching correct function names -function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ - -# Regular expression matching correct variable names -variable-rgx=^[a-z][a-z0-9_]*$ - -# Regular expression matching correct constant names -const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ - -# Regular expression matching correct attribute names -attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ - -# Regular expression matching correct argument names -argument-rgx=^[a-z][a-z0-9_]*$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=^[a-z][a-z0-9_]*$ - -# Regular expression matching correct class names -class-rgx=^_?[A-Z][a-zA-Z0-9]*$ - -# Regular expression matching correct module names -module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ - -# Regular expression matching correct method names -method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=12 - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=80 - -# TODO(https://github.com/pylint-dev/pylint/issues/3352): Direct pylint to exempt -# lines made too long by directives to pytype. - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=(?x)( - ^\s*(\#\ )??$| - ^\s*(from\s+\S+\s+)?import\s+.+$) - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=yes - -# Maximum number of lines in a module -max-module-lines=99999 - -# String used as indentation unit. The internal Google style guide mandates 2 -# spaces. Google's externaly-published style guide says 4, consistent with -# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google -# projects (like TensorFlow). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=TODO - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=yes - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging,absl.logging,tensorflow.io.logging - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub, - TERMIOS, - Bastion, - rexec, - sets - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant, absl - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls, - class_ - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs diff --git a/sample_backtest.py b/sample_backtest.py index 122f158..413a20d 100644 --- a/sample_backtest.py +++ b/sample_backtest.py @@ -2,27 +2,44 @@ import asyncio import logging from datetime import datetime, UTC -from aiomql.lib.backtest_runner import BackTestRunner +from aiomql.lib.backtester import BackTester from aiomql.core import Config -from aiomql.contrib.strategies import FingerTrap, Chaos +from aiomql.contrib.strategies import Chaos from aiomql.contrib.symbols import ForexSymbol -from aiomql.contrib.backtesting import BackTestEngine +from aiomql.core.backtesting import BackTestEngine async def back_tester(): config = Config() - config.mode = 'backtest' - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - syms = ['Volatility 75 Index', 'Volatility 100 Index', 'Volatility 50 Index'] + config.mode = "backtest" + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + syms = [ + "Volatility 75 Index", + "Volatility 100 Index", + "Volatility 25 Index", + "Volatility 10 Index", + ] symbols = [ForexSymbol(name=sym) for sym in syms] - stgs = [Chaos(symbol=symbol) for symbol in symbols] - start = datetime(2021, 1, 1, tzinfo=UTC) - end = datetime(2021, 1, 7, tzinfo=UTC) - back_test_engine = BackTestEngine(start=start, end=end) - await back_test_engine.setup_account(balance=100) - back_test_runner = BackTestRunner(strategies=stgs, backtest_engine=back_test_engine) - await back_test_runner.run() + strategies = [Chaos(symbol=symbol) for symbol in symbols] + start = datetime(2024, 5, 1, tzinfo=UTC) + stop_time = datetime(2024, 5, 2, tzinfo=UTC) + end = datetime(2024, 5, 7, tzinfo=UTC) + back_test_engine = BackTestEngine( + start=start, + end=end, + speed=300, + stop_time=stop_time, + close_open_positions_on_exit=True, + assign_to_config=True, + preload=True, + ) + await back_test_engine.setup_account(balance=350) + backtester = BackTester(backtest_engine=back_test_engine) + backtester.add_strategies(strategies=strategies) + await backtester.start() asyncio.run(back_tester()) -print("Bot executed successfully") diff --git a/sample_bot.py b/sample_bot.py index f7bad54..e821f98 100644 --- a/sample_bot.py +++ b/sample_bot.py @@ -1,22 +1,22 @@ import logging from aiomql.lib.bot import Bot -from aiomql.core import Config -from aiomql.contrib.strategies import FingerTrap, Chaos +from aiomql.contrib.strategies import Chaos from aiomql.contrib.symbols import ForexSymbol -def bot(): - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - syms = ['Volatility 75 Index', 'Volatility 100 Index', 'Volatility 50 Index'] +def chaos_bot(): + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 50 Index"] symbols = [ForexSymbol(name=sym) for sym in syms] - stgs = [Chaos(symbol=symbol) for symbol in symbols] - config = Config() + strategies = [Chaos(symbol=symbol) for symbol in symbols] bot = Bot() - bot.executor.timeout = 60 - bot.add_strategies(strategies=stgs) + bot.executor.timeout = 10 + bot.add_strategies(strategies=strategies) bot.execute() -bot() -print("Bot executed successfully") +chaos_bot() diff --git a/src/aiomql/_utils.py b/src/aiomql/_utils.py index 71f03f9..51745d2 100644 --- a/src/aiomql/_utils.py +++ b/src/aiomql/_utils.py @@ -3,10 +3,14 @@ import decimal import random from functools import wraps, partial import asyncio +from threading import RLock from logging import getLogger +from .core.config import Config logger = getLogger(__name__) +config = Config() + def dict_to_string(data: dict, multi=False) -> str: """Convert a dict to a string. Useful for logging. @@ -18,13 +22,17 @@ def dict_to_string(data: dict, multi=False) -> str: Returns: str: The string representation of the dict. """ - sep = '\n' if multi else ', ' + sep = "\n" if multi else ", " return f"{sep}".join(f"{key}: {value}" for key, value in data.items()) -def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, error='') -> callable: +def backoff_decorator( + func=None, *, max_retries: int = 2, retries: int = 0, error="" +) -> callable: if func is None: - return partial(backoff_decorator, max_retries=max_retries, retries=retries, error=error) + return partial( + backoff_decorator, max_retries=max_retries, retries=retries, error=error + ) @wraps(func) async def wrapper(*args, **kwargs): @@ -35,22 +43,31 @@ def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, erro try: retries += 1 res = await func(*args, **kwargs) - if error != '' and res == error: - raise TypeError('Invalid return type') + if error != "" and res == error: + raise TypeError("Invalid return type") else: retries = 0 return res except Exception as err: - logger.error('Error in %s: %s', func.__name__, err) - await asyncio.sleep(2**retries + random.uniform(0, 1)) - await wrapper(*args, **kwargs) + logger.error("Error in %s: %s", func.__name__, err) + if config.mode != "backtest": + await asyncio.sleep(2**retries + random.uniform(0, max_retries)) + await wrapper(*args, **kwargs) return wrapper -def error_handler(func=None, *, msg='', exe = Exception, response=None, log_error_msg=True): +def error_handler( + func=None, *, msg="", exe=Exception, response=None, log_error_msg=True +): if func is None: - return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg) + return partial( + error_handler, + msg=msg, + exe=exe, + response=response, + log_error_msg=log_error_msg, + ) @wraps(func) async def wrapper(*args, **kwargs): @@ -59,14 +76,23 @@ def error_handler(func=None, *, msg='', exe = Exception, response=None, log_erro return res except exe as err: if log_error_msg: - logger.error(f'Error in {func.__name__}: {msg or err}') + logger.error(f"Error in {func.__name__}: {msg or err}") return response return wrapper -def error_handler_sync(func=None, *, msg='', exe=Exception, response=None, log_error_msg=True): + +def error_handler_sync( + func=None, *, msg="", exe=Exception, response=None, log_error_msg=True +): if func is None: - return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg) + return partial( + error_handler, + msg=msg, + exe=exe, + response=response, + log_error_msg=log_error_msg, + ) @wraps(func) def wrapper(*args, **kwargs): @@ -75,13 +101,14 @@ def error_handler_sync(func=None, *, msg='', exe=Exception, response=None, log_e return res except exe as err: if log_error_msg: - logger.error(f'Error in {func.__name__}: {msg or err}') + logger.error(f"Error in {func.__name__}: {msg or err}") return response return wrapper + def round_down(value: int | float, base: int) -> int: - return int(value) if value % base == 0 else int(value - (value % base)) + return int(value) if value % base == 0 else int(value - (value % base)) def round_up(value: int | float, base: int) -> int: @@ -100,11 +127,12 @@ def async_cache(fun): @wraps(fun) async def wrapper(*args, **kwargs): key = (args, frozenset(kwargs.items())) - async with wrapper.lock: + with wrapper.lock: if key not in wrapper.cache: + # print(key) wrapper.cache[key] = await fun(*args, **kwargs) - return wrapper.cache[key] + return wrapper.cache[key] - wrapper.lock = asyncio.Lock() + wrapper.lock = RLock() wrapper.cache = {} return wrapper diff --git a/src/aiomql/contrib/__init__.py b/src/aiomql/contrib/__init__.py index 020221f..b573057 100644 --- a/src/aiomql/contrib/__init__.py +++ b/src/aiomql/contrib/__init__.py @@ -1,4 +1,3 @@ -from .backtesting import * from .strategies import * from .candle_patterns import * from .symbols import * diff --git a/src/aiomql/contrib/backtesting/backtest_engine.py b/src/aiomql/contrib/backtesting/backtest_engine.py deleted file mode 100644 index 671e6c0..0000000 --- a/src/aiomql/contrib/backtesting/backtest_engine.py +++ /dev/null @@ -1,819 +0,0 @@ -import asyncio -from datetime import datetime, UTC -from typing import Literal -from itertools import zip_longest -import random -from functools import cached_property -from logging import getLogger -from math import ceil - -import pandas as pd -import numpy as np -from pandas import DataFrame -from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TradeOrder, TradePosition, TradeDeal, - TradeRequest, OrderCheckResult, OrderSendResult, TerminalInfo) - -from ...core.meta_trader import MetaTrader -from ...core.constants import (TimeFrame, OrderType, TradeAction, AccountStopOutMode, PositionReason, - DealType, DealReason, DealEntry, OrderReason, CopyTicks) - -from ..._utils import round_down, round_up, error_handler, error_handler_sync, async_cache - -from .get_data import BackTestData, GetData, Cursor -from .backtest_account import BackTestAccount -from .trades_manager import PositionsManager, OrdersManager, DealsManager - -logger = getLogger(__name__) - - -class BackTestEngine: - mt5: MetaTrader - span: range - range: range - speed: int - cursor: Cursor - iter: zip_longest - rates: dict[str, dict[int, DataFrame]] - ticks: dict[str, DataFrame] - prices: dict[str, DataFrame] - orders: OrdersManager - deals: DealsManager - positions: PositionsManager - _account: BackTestAccount - stop_testing: bool - use_terminal: bool - restart: bool - stop_time: int | None - - def __init__(self, *, data: BackTestData = None, speed: int = 1, start: float | datetime = 0, - end: float | datetime = 0, restart: bool = True, use_terminal: bool = None, name: str = '', - stop_time: float | datetime = None): - self._data = data or BackTestData() - self.mt5 = MetaTrader() - self.config = self.mt5.config - self.config.backtest_engine = self - self.setup_test_range(start=start, end=end, speed=speed, restart=restart) - self.setup_data(restart=restart) - start, end = (self.span[0], self.span[-1]) if len(self.span) >= 2 else ((now := datetime.now(UTC).timestamp()), now) - self.name = name or self._data.name or f"backtest_data_{datetime.now(tz=UTC):%d_%m_%y}" - self.stop_testing = False - self.use_terminal = self.config.use_terminal_for_backtesting if use_terminal is None else use_terminal - if stop_time is not None: - val = stop_time.astimezone(tz=UTC) if isinstance(start, datetime) else datetime.fromtimestamp(start, tz=UTC) - stop_time = int(val.timestamp()) - self.stop_time = stop_time - - def __next__(self) -> Cursor: - try: - index, time = next(self.iter) - if self.stop_time and time >= self.stop_time: - raise StopIteration - self.cursor = Cursor(index=index, time=time) - return self.cursor - except StopIteration: - logger.critical("End of the test range") - self.stop_testing = True - - def __repr__(self): - return f"{self.__class__.__name__}()" - - def setup_test_range(self, *, start: float | datetime = None, end: float | datetime = None, speed: int = 1, - restart: bool = True): - if self._data.span and self._data.range: - start = start or self._data.span[0] - end = end or self._data.span[-1] + 1 - start = start.astimezone(tz=UTC) if isinstance(start, datetime) else datetime.fromtimestamp(start, tz=UTC) - end = end.astimezone(tz=UTC) if isinstance(end, datetime) else datetime.fromtimestamp(end, tz=UTC) - span_start = int(start.timestamp()) - span_end = int(end.timestamp()) - self.speed = speed - self.span = range(span_start, span_end, speed) - self.range = range(0, span_end - span_start, speed) - self.iter = zip_longest(self.range, self.span) - - if restart is False and self._data.cursor is not None: - self.cursor = self._data.cursor - self.go_to(time=self.cursor.time) - else: - self.cursor = Cursor(index=self.range.start, time=self.span.start) - - def setup_data(self, *, restart: bool = True): - if restart is True: - self.orders = OrdersManager() - self.positions = PositionsManager() - self.deals = DealsManager() - self._account = BackTestAccount() - return - - orders = {} - for ticket, order in self._data.orders.items(): - orders[ticket] = TradeOrder((order.get(k) for k in TradeOrder.__match_args__)) - self.orders = OrdersManager(data=orders) - - positions = {} - for ticket, position in self._data.positions.items(): - positions[ticket] = TradePosition((position.get(k) for k in TradePosition.__match_args__)) - self.positions = PositionsManager(data=positions, open_positions=self._data.open_positions, - margins=self._data.margins) - - deals = {} - for ticket, deal in self._data.deals.items(): - deals[ticket] = TradeDeal((deal.get(k) for k in TradeDeal.__match_args__)) - self.deals = DealsManager(data=deals) - - self._account = BackTestAccount(**self._data.account) - - def next(self) -> Cursor: - return next(self) - - @property - def data(self): - return self._data - - def reset(self, clear_data: bool = False): - self.iter = zip_longest(self.range, self.span) - self.cursor = Cursor(index=self.range.start, time=self.span.start) - if clear_data: - self.setup_data(restart=True) - - def go_to(self, *, time: datetime | float): - time = time.astimezone(tz=UTC) if isinstance(time, datetime) else datetime.fromtimestamp(time, tz=UTC) - time = int(time.timestamp()) - steps = time - self.cursor.time - if 0 <= steps < (len(self.range) - 1): - self.fast_forward(steps=steps) - return - raise ValueError("Can't go back in time or beyond the limits of the range") - - def fast_forward(self, *, steps: int): - for _ in range(steps): - self.next() - - @staticmethod - def get_dtype(*, df: DataFrame) -> list[tuple[str, str]]: - return [(c, t) for c, t in zip(df.columns, df.dtypes)] - - async def tracker(self): - pos_tasks = [self.check_position(ticket=ticket) for ticket in self.positions._open_positions] - await asyncio.gather(*pos_tasks) - profit = sum(pos.profit for pos in self.positions.open_positions) - profit = round(profit, self._account.currency_digits) - self.update_account(profit=profit) - self.check_account() - - def wrap_up(self): - try: - self._data.deals = self.deals.to_dict() - self._data.orders = self.orders.to_dict() - self._data.positions = self.positions.to_dict() - self._data.open_positions = self.positions._open_positions - self._data.margins = self.positions.margins - self._data.account = self._account.asdict() - self._data.cursor = self.cursor - self._data.span = self.span - self._data.range = self.range - self._data.account = self._account.asdict() - name = self._data.name or self.name - self._data.name = name - path = self.config.backtest_dir/f"{name}.pkl" - GetData.pickle_data(data=self._data, name=path) - except Exception as err: - logger.error("Error in wrap_up: %s", err) - - @async_cache - async def get_price_tick(self, *, symbol: str, time: int) -> Tick | None: - try: - if self.use_terminal: - time = datetime.fromtimestamp(time, tz=UTC) - tick = await self.mt5.copy_ticks_from(symbol, time, 1, CopyTicks.ALL) - return Tick(tick[-1]) if tick else None - - tick = self.prices[symbol].loc[time] - return Tick(tick) - except Exception as exe: - logger.error("Error Getting Price Tick: %s", exe) - - @error_handler - async def check_order(self, *, ticket: int): - """" - Check if the order has reached its take profit or stop loss levels and close the order if it has. - Checks only **OrderType.BUY** and **OrderType.SELL** orders that have reached their take profit or stop loss levels. - - Args: - ticket (int): Order ticket - """ - order = self.orders[ticket] - order_type, symbol = order.type, order.symbol - tick = await self.get_price_tick(symbol=symbol, time=self.cursor.time) - tp, sl = order.tp, order.sl - - if not (tp and sl): - return - - deal = {'ticket': random.randint(100_000_000, 999_999_999), 'order': ticket, - 'symbol': symbol, 'commission': 0, 'swap': 0, 'position_id': ticket, - 'fee': 0, 'time': self.cursor.time, 'time_msc': self.cursor.time * 1000, - 'price': tick.bid, 'type': DealType(order_type), - 'reason': DealReason.EXPERT, 'entry': DealEntry.OUT, 'profit': 0} - - match order_type: - case OrderType.BUY: - if tick.bid >= tp or tick.bid <= sl: - res = self.close_position(ticket=ticket) - if res: - pos = self.positions.get(ticket) - deal.update({'profit': pos.profit, 'volume': pos.volume}) - self.deals[deal['ticket']] = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) - - - case OrderType.SELL: - if tick.ask <= tp or tick.ask >= sl: - res = self.close_position(ticket=ticket) - if res: - pos = self.positions.get(ticket) - deal.update({'profit': pos.profit, 'volume': pos.volume}) - self.deals[deal['ticket']] = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) - case _: - ... - - def check_account(self): - account = self._account - level = account.margin_level if account.margin_so_mode == AccountStopOutMode.PERCENT else account.margin_so_call - if level < account.margin_so_call and level != 0: - logger.critical("Account has burned out!!! Please top up to continue trading") - self.stop_testing = True - - @error_handler - async def check_position(self, *, ticket: int): - """ - Update the profit of an open position based on the current price of the symbol. - - Args: - ticket (int): Position ticket - """ - pos = self.positions[ticket] - order_type, symbol, volume, price_open, prev_profit = (pos.type, pos.symbol, pos.volume, pos.price_open, - pos.profit) - tick = await self.get_price_tick(symbol=symbol, time=self.cursor.time) - price_current = tick.bid if order_type == OrderType.BUY else tick.ask - profit = await self.order_calc_profit(action=order_type, symbol=symbol, volume=volume, - price_open=price_open, price_close=price_current) - self.positions.update(ticket=pos.ticket, profit=profit, price_current=price_current, - time_update=self.cursor.time) - await self.check_order(ticket=ticket) - - # @error_handler_sync(response=False) - def close_position(self, *, ticket: int) -> bool: - """ - Close an open position for the trading account using the position ticket. - - Args: - ticket: Position ticket - - Returns: - bool: True if the position is closed successfully, False otherwise - """ - try: - position = self.positions[ticket] - margin = self.positions.get_margin(ticket=ticket) - self.positions.delete_margin(ticket=ticket) - self.positions.close(ticket=ticket) - self.orders.update(ticket=ticket, time_done=self.cursor.time, time_done_msc=self.cursor.time*1000) - gain = round(position.profit, self._account.currency_digits) - self.update_account(gain=gain, margin=-margin) - return True - except Exception as exe: - logger.error("Error Closing Position %d: %s", ticket, exe) - return False - - @error_handler(response=False) - def modify_stops(self, *, ticket: int, sl: int, tp: int) -> bool: - """ - Modify the stop loss and take profit levels of an open position. - - Args: - ticket: Position ticket - sl: stop loss level - tp: Take profit level - - Returns: - bool: True if the stops are modified successfully, False otherwise - """ - self.positions.update(ticket=ticket, sl=sl, tp=tp, time_update=self.cursor.time, - time_update_msc=self.cursor.time*1000) - return True - - def update_account(self, *, profit: float = None, margin: float = 0, gain: float = 0): - self._account.balance += gain - self._account.profit = profit if profit is not None else self._account.profit - self._account.equity = self._account.balance + self._account.profit - self._account.margin += margin - self._account.margin_free = self._account.equity - self._account.margin - - if self._account.margin == 0: - self._account.margin_level = 0 - else: - mode = self._account.margin_so_mode - level = self._account.equity / self._account.margin * 100 - self._account.margin_level = level if mode == AccountStopOutMode.PERCENT else self._account.margin_free - - def deposit(self, *, amount: float): - self.update_account(gain=amount) - - def withdraw(self, *, amount: float): - assert amount <= self._account.balance, 'Insufficient funds' - self.update_account(gain=-amount) - - @error_handler - async def setup_account(self, **kwargs): - default = {'profit': self._account.profit, 'margin': self._account.margin, 'equity': self._account.equity, - 'margin_free': self._account.margin_free, 'margin_level': self._account.margin_level, - 'balance': self._account.balance, - **{k: v for k, v in kwargs.items() if k in self._account.__match_args__}} - - if self.use_terminal: - acc_info = await self.mt5.account_info() - default = {**acc_info._asdict(), **default} - - self._account.set_attrs(**default) - self.update_account() - - @cached_property - def prices(self) -> dict[str, DataFrame]: - prices = {} - for symbol in self._data.ticks.keys(): - res = self._data.ticks[symbol] - res = pd.DataFrame(res) - res.drop_duplicates(subset=['time'], keep='last', inplace=True) - res.set_index('time', inplace=True, drop=False) - res = res.reindex(self.span, copy=True, method='ffill') # fill in missing values with NaN - prices[symbol] = res - return prices - - @cached_property - def ticks(self) -> dict[str, DataFrame]: - ticks = {} - for symbol in self._data.ticks.keys(): - res = self._data.ticks[symbol] - res = pd.DataFrame(res) - ticks[symbol] = res - return ticks - - @cached_property - def rates(self) -> dict[str, dict[int, DataFrame]]: - rates = {} - for symbol in self._data.rates.keys(): - for timeframe in self._data.rates[symbol].keys(): - res = self._data.rates[symbol][timeframe] - res = pd.DataFrame(res) - rates.setdefault(symbol, {})[timeframe] = res - return rates - - @cached_property - def symbols(self) -> dict[str, SymbolInfo]: - symbols = {} - for symbol, info in self._data.symbols.items(): - symbols[symbol] = SymbolInfo((info.get(key) for key in SymbolInfo.__match_args__)) - return symbols - - @error_handler - async def order_send(self, *, request: dict, use_terminal=False) -> OrderSendResult: - osr = {'retcode': 10013, 'comment': 'Invalid request', - 'request': TradeRequest(request.get(k, (0 if k != 'comment' else '')) for k in - TradeRequest.__match_args__)} - current_tick = await self.get_price_tick(symbol=request.get('symbol'), time=self.cursor.time) - if current_tick is None: - osr['comment'] = 'Market is closed' - osr['retcode'] = 10018 - return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) - - trade_order = {'external_id': '', 'comment': '', - **{k: v for k, v in request.items() if k in TradeOrder.__match_args__}} - order_type, symbol = request.get('type'), request.get('symbol', '') - action, position_id = request.get('action'), request.get('position') - sl, tp, volume, symbol = request.get('sl'), request.get('tp'), request.get('volume'), request.get('symbol') - order_type = OrderType(order_type) - current_position = self.positions.get(position_id) - order_ticket = random.randint(100_000_000, 999_999_999) - deal_ticket = random.randint(100_000_000, 999_999_999) - - # closing an order by an opposite order using a position ticket and Deal action - if action == TradeAction.DEAL and current_position and order_type.opposite == current_position.type: - res = self.close_position(ticket=current_position.ticket) - if res: - price_current = current_tick.ask if order_type == OrderType.BUY else current_tick.bid - # self.orders.update(ticket=ticket, time_done=self.cursor.time, time_done_msc=self.cursor.time * 1000) - trade_order.update({'position_id': current_position.ticket, 'ticket': order_ticket, - 'time_setup': current_tick.time, 'time_setup_msc': current_tick.time_msc, - 'time_done': current_tick.time, 'time_done_msc': current_tick.time_msc, - 'type': order_type, 'symbol': symbol, 'sl': current_position.sl, - 'tp': current_position.tp, - 'price_current': price_current, 'reason': OrderReason.EXPERT, - 'volume_initial': current_position.volume}) - - # TODO: calculate commission and swap if possible or necessary - deal = {'ticket': deal_ticket, 'position_id': current_position.ticket, 'order': order_ticket, - 'symbol': symbol, 'time': current_tick.time, 'profit': current_position.profit, - 'time_msc': current_tick.time_msc, 'volume': current_position.volume, - 'price': price_current, 'type': DealType(order_type), 'reason': DealReason.EXPERT, - 'entry': DealEntry.OUT, 'comment': '', 'external_id': ''} - - order = TradeOrder((trade_order.get(k, 0) for k in TradeOrder.__match_args__)) - self.orders[order.ticket] = order - deal = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) - self.deals[deal.ticket] = deal - osr.update({'comment': 'Request completed', 'retcode': 10009, - 'order': order_ticket, 'deal': deal_ticket,}) - return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) - - if action == TradeAction.SLTP and current_position: - check = await self.order_check(request=request, use_terminal=use_terminal) - if check.retcode != 0: - osr = {'retcode': check.retcode, 'comment': check.comment, 'request': check.request} - return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) - res = self.modify_stops(ticket=position_id, sl=sl, tp=tp) - if res: - osr.update({'comment': 'Request completed', 'retcode': 10009, 'order': order_ticket, - 'deal': deal_ticket}) - return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) - - if action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL): - check = await self.order_check(request=request, use_terminal=use_terminal) - if check.retcode != 0: - osr = {'retcode': check.retcode, 'comment': check.comment, 'request': check.request} - return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) - - price = current_tick.ask if order_type == OrderType.BUY else current_tick.bid - position = {'ticket': order_ticket, 'symbol': symbol, 'volume': volume, - 'price_open': price, 'price_current': price, 'type': order_type, 'profit': 0, - 'reason': PositionReason.EXPERT, 'identifier': order_ticket, - 'sl': sl, 'tp': tp, 'time': current_tick.time, 'time_msc': current_tick.time_msc, - 'time_update': current_tick.time, 'time_update_msc': current_tick.time_msc} - - deal = {'ticket': deal_ticket, 'order': order_ticket, 'symbol': symbol, 'commission': 0, 'swap': 0, - 'position_id': order_ticket, 'fee': 0, 'time': current_tick.time, 'time_msc': current_tick.time_msc, - 'volume': volume, 'price': price, 'type': DealType(order_type), 'reason': DealReason.EXPERT, - 'entry': DealEntry.IN, 'profit': 0} - - # ToDo: set time_expiration based on order_type_time - trade_order.update({'ticket': order_ticket, 'symbol': symbol, 'volume': volume, 'price': price, - 'price_current': price, 'sl': sl, 'time_setup_msc': current_tick.time_msc, - 'tp': tp, 'price_open': price, 'type': order_type, 'time_setup': current_tick.time, - 'volume_current': volume, 'volume_initial': volume, 'position_id': order_ticket}) - - pos = TradePosition((position.get(k, 0) for k in TradePosition.__match_args__)) - order = TradeOrder((trade_order.get(k, 0) for k in TradeOrder.__match_args__)) - deal = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) - self.deals[deal_ticket] = deal - self.positions[order.ticket] = pos - self.orders[order.ticket] = order - osr.update({'comment': 'Request completed', 'retcode': 10009, 'order': order_ticket, 'price': price, - 'volume': volume, 'bid': current_tick.bid, - 'ask': current_tick.ask, 'deal': deal_ticket}) - margin = await self.order_calc_margin(action=action, symbol=symbol, volume=volume, price=price, - use_terminal=use_terminal) - self.positions.set_margin(ticket=order_ticket, margin=margin) - self.update_account(margin=margin) - return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) - - @error_handler - async def order_check(self, *, request: dict, use_terminal: bool = False) -> OrderCheckResult: - ocr = {'retcode': 10013, 'balance': 0, 'profit': 0, 'margin': 0, 'equity': 0, 'margin_free': 0, - 'margin_level': 0, 'comment': 'Invalid request', - 'request': TradeRequest(request.get(k, (0 if k != 'comment' else '')) for k in - TradeRequest.__match_args__)} - - action, symbol, volume = request.get('action'), request.get('symbol'), request.get('volume') - price, order_type, position_id = request.get('price'), request.get('type'), request.get('position') - - # check margin and confirm order can go through for a deal action and buy or sell order type - if action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL) and position_id is None: - margin = await self.order_calc_margin(action=action, symbol=symbol, volume=volume, - price=price, use_terminal=use_terminal) - if margin is None: - return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) - - used_margin = self._account.margin + margin - free_margin = self._account.margin_free - margin - - level = self._account.equity / used_margin * 100 if used_margin else float('inf') - margin_level = level if self._account.margin_so_mode == AccountStopOutMode.PERCENT else free_margin - ocr.update({'margin_level': margin_level, 'margin': margin, 'margin_free': free_margin}) - - # check if the account has enough money - if margin_level < self._account.margin_so_call: - ocr['retcode'] = 10019 - ocr['comment'] = 'No money' - return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) - - # check if the stops level is valid - sym = await self.get_symbol_info(symbol=symbol) - sl, tp = request.get('sl'), request.get('tp') - current_price = price - if tp and sl: - if action == TradeAction.SLTP: - pos = self.positions.get(request.get('position')) - sym = await self.get_symbol_info(pos.symbol) - current_tick = sym or await self.get_price_tick(pos.symbol, self.cursor.time) - current_price = current_tick.bid if pos.type == OrderType.BUY else current_tick.ask - - min_sl = min(sl, tp) - dsl = abs(current_price - min_sl) / sym.point - tsl = sym.trade_stops_level + sym.spread - if dsl < tsl: - ocr['retcode'] = 10016 - ocr['comment'] = 'Invalid stops' - return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) - - elif action == TradeAction.SLTP: - ocr['comment'] = 'Done' - ocr['retcode'] = 0 - return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) - - if use_terminal or self.use_terminal: - ocr_t = await self.mt5.order_check(request) - if ocr_t.retcode in (10013, 10014): - return ocr_t - elif action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL): - # check volume - if volume < sym.volume_min or volume > sym.volume_max: - ocr['retcode'] = 10014 - ocr['comment'] = 'Invalid volume' - return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) - - ocr.update({'balance': self._account.balance, 'profit': self._account.profit, 'equity': self._account.equity, - 'comment': 'Done', 'retcode': 0}) - - return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) - - @error_handler - async def get_terminal_info(self) -> TerminalInfo: - if self.use_terminal: - res = await self.mt5.terminal_info() - return res - return TerminalInfo(self._data.terminal) - - @error_handler - async def get_version(self) -> tuple[int, int, str]: - if self.use_terminal: - res = await self.mt5.version() - return res - return self._data.version - - @error_handler - async def get_symbols_total(self) -> int: - if self.use_terminal: - syms = await self.mt5.symbols_total() - return syms - return len(self.symbols) - - @error_handler - async def get_symbols(self, *, group: str = '') -> tuple[SymbolInfo, ...]: - if self.use_terminal: - syms = await self.mt5.symbols_get(group=group) - return syms - return tuple(list(self.symbols.values())) - - @error_handler_sync - def get_account_info(self) -> AccountInfo: - return AccountInfo(self._account.asdict().values()) - - @error_handler - async def get_symbol_info_tick(self, *, symbol: str) -> Tick | None: - tick = await self.get_price_tick(symbol=symbol, time=self.cursor.time) - return tick - - @error_handler - async def get_symbol_info(self, *, symbol: str) -> SymbolInfo: - if self.use_terminal: - info = await self.mt5.symbol_info(symbol) - else: - info = self.symbols[symbol] - tick = await self.get_symbol_info_tick(symbol=symbol) - info = info._asdict() | {'bid': tick.bid, 'bidhigh': tick.bid, 'bidlow': tick.bid, 'ask': tick.ask, - 'askhigh': tick.ask, 'asklow': tick.bid, 'last': tick.last, 'volume_real': tick.volume_real} - return SymbolInfo((info.get(key) for key in SymbolInfo.__match_args__)) - - @error_handler - async def get_rates_from(self, *, symbol: str, timeframe: TimeFrame, date_from: - datetime | float, count: int) -> np.ndarray: - date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp( - date_from, tz=UTC) - if self.use_terminal: - rates = await self.mt5.copy_rates_from(symbol, timeframe, date_from, count) - return rates - - rates = self.rates[symbol][timeframe] - start = int(date_from.timestamp()) - start = round_down(start, timeframe.seconds) - rates = rates[rates.time <= start].iloc[-count:] - return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) - - # @error_handler - async def get_rates_from_pos(self, *, symbol: str, timeframe: TimeFrame, start_pos: int, count: int) -> np.ndarray: - if self.use_terminal: - current_time = self.cursor.time if start_pos == 0 else self.cursor.time - start_pos * timeframe.seconds - current_time = round_up(current_time, timeframe.seconds) - start = datetime.fromtimestamp(current_time, tz=UTC) - rates = await self.mt5.copy_rates_from(symbol, timeframe, start, count) - return rates - - rates = self.rates[symbol][timeframe] - - # the current time rounded up to a multiple of the timeframe in seconds and then subtracted by the start_pos - # multiplied by the timeframe in seconds gives the time of the last candlestick in the range when using - # copy_rates_from_pos - end = int(round_down(self.cursor.time, timeframe.seconds)) - start_pos * timeframe.seconds - start = end - count * timeframe.seconds - rates = rates[(rates.time > start) & (rates.time <= end)] - return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) - - @error_handler - async def get_rates_range(self, *, symbol: str, timeframe: TimeFrame, date_from: datetime | float, - date_to: datetime | float) -> np.ndarray: - date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp( - date_from, tz=UTC) - date_to = date_to.astimezone(tz=UTC) if isinstance(date_to, datetime) else datetime.fromtimestamp( - date_to, tz=UTC) - if self.use_terminal: - rates = await self.mt5.copy_rates_range(symbol, timeframe, date_from, date_to) - return rates - - rates = self.rates[symbol][timeframe] - start = round_up(int(date_from.timestamp()), timeframe.seconds) - end = round_up(int(date_to.timestamp()), timeframe.seconds) - rates = rates[(rates.time >= start) & (rates.time <= end)] - return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) - - @error_handler - async def get_ticks_from(self, *, symbol: str, date_from: datetime | float, count: int, - flags: CopyTicks = CopyTicks.ALL) -> np.ndarray: - date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp( - date_from, tz=UTC) - if self.use_terminal: - ticks = await self.mt5.copy_ticks_from(symbol, date_from, count, flags) - return ticks - - ticks = self.ticks[symbol] - start = int(date_from.timestamp()) - rates = ticks[ticks.time <= start].iloc[-count:] - return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) - - @error_handler - async def get_ticks_range(self, *, symbol: str, date_from: datetime | float, - date_to: datetime | float, flags: CopyTicks = CopyTicks.ALL) -> np.ndarray: - date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp( - date_from, tz=UTC) - date_to = date_to.astimezone(tz=UTC) if isinstance(date_to, datetime) else datetime.fromtimestamp( - date_to, tz=UTC) - if self.use_terminal: - ticks = await self.mt5.copy_ticks_range(symbol, date_from, date_to, flags) - return ticks - - ticks = self.ticks[symbol] - start = int(date_from.timestamp()) - end = int(date_to.timestamp()) - rates = ticks[(ticks.time >= start) & (ticks.time <= end)] - return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=ticks)) - - @error_handler - async def order_calc_margin(self, *, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, - price: float, use_terminal: bool = False): - if use_terminal or self.use_terminal: - return await self.mt5.order_calc_margin(action, symbol, volume, price) - - sym = self.symbols[symbol] - margin = (volume * sym.trade_contract_size * price) / (self._account.leverage / (sym.margin_initial or 1)) - return round(margin, self._account.currency_digits) - - @error_handler - async def order_calc_profit(self, *, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, - price_open: float, price_close: float, use_terminal = False): - - if use_terminal or self.use_terminal: - return await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close) - - sym = self.symbols[symbol] - profit = (volume * sym.trade_contract_size * - ((price_close - price_open) if action == OrderType.BUY else (price_open - price_close))) - return round(profit, self._account.currency_digits) - - @error_handler_sync - def get_orders_total(self) -> int: - """ - Get the total number of pending orders. - - Returns: - int: Total number of pending orders - """ - return 0 - - @error_handler_sync - def get_orders(self, *, symbol: str = '', group: str = '', ticket: int = None) -> tuple[TradeOrder, ...]: - """ - Get pending orders from the terminal history. This has to do with pending orders, which this backtester - doesn't support yet. - - Args: - symbol: Symbol name - group: Group name - ticket: Order ticket - - Returns: - tuple[TradeOrder] - """ - if symbol and group and ticket: - return tuple() - return () - - @error_handler_sync - def get_positions_total(self) -> int: - """ - Get the total number of open positions. - - Returns: - int: Total number of open positions - """ - return self.positions.positions_total() - - @error_handler_sync - def get_positions(self, *, symbol: str = None, group: str = None, ticket: int = None) -> tuple[TradePosition, ...]: - """ - Get open positions from the terminal history. - - Keyword Args: - symbol: The symbol name - group: Group argument to filter by - ticket: Position ticket - - Returns: - tuple[TradePosition]: Open positions - """ - return self.positions.positions_get(ticket=ticket, symbol=symbol, group=group) - - @error_handler_sync - def get_history_orders_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: - """ - Get the total number of orders in the terminal history. - - Args: - date_from: The start date of the history - - date_to: The end date of the history - - Returns: - int: Total number of orders in the history - """ - return self.orders.history_orders_total(date_from=date_from, date_to=date_to) - - @error_handler_sync - def get_history_orders(self, *, date_from: datetime | float = None, date_to: datetime | float = None, - group: str = '', ticket: int = None, position: int = None) -> tuple[TradeOrder, ...]: - """ - Get orders from the terminal history. - - Keyword Args: - date_from: Date from which to start the history - date_to: Date to which to end the history - group: group keyword to filter by - ticket: ticket id to filter by - position: position id to filter by - - Returns: - tuple[TradeOrder]: Orders in the history - """ - return self.orders.history_orders_get(date_from=date_from, date_to=date_to, group=group, ticket=ticket, - position=position) - - @error_handler_sync - def get_history_deals_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: - """ - Get the total number of deals in the terminal history. - - Args: - date_from: Date from which to start the history - date_to: Date to which to end the history - - Returns: - int: Total number of deals in the history - """ - return self.deals.history_deals_total(date_from=date_from, date_to=date_to) - - @error_handler_sync - def get_history_deals(self, *, date_from: datetime | float = None, date_to: datetime | float = None, - group: str = None, position: int = None, ticket: int = None) -> tuple[TradeDeal, ...]: - """ - Get deals from the terminal history. - - Keyword Args: - date_from: Date from which to start the history - date_to: Date to which to end the history - group: group keyword to filter by - position: position id to filter by - ticket: ticket id to filter by - - Returns: - tuple[TradeDeal, ...]: Deals in the history - """ - return self.deals.history_deals_get(date_from=date_from, date_to=date_to, group=group, position=position, - ticket=ticket) diff --git a/src/aiomql/contrib/candle_patterns/fractals.py b/src/aiomql/contrib/candle_patterns/fractals.py index 013c3d4..b5e131f 100644 --- a/src/aiomql/contrib/candle_patterns/fractals.py +++ b/src/aiomql/contrib/candle_patterns/fractals.py @@ -3,11 +3,21 @@ from ...lib.candle import Candle, Candles def find_bearish_fractal(candles: Candles) -> Candle | None: for i in range(len(candles) - 3, 1, -1): - if candles[i].high > max(candles[i - 1].high, candles[i + 1].high, candles[i - 2].high, candles[i + 2].high): + if candles[i].high > max( + candles[i - 1].high, + candles[i + 1].high, + candles[i - 2].high, + candles[i + 2].high, + ): return candles[i] def find_bullish_fractal(candles: Candles) -> Candle | None: for i in range(len(candles) - 3, 1, -1): - if candles[i].low < min(candles[i - 1].low, candles[i + 1].low, candles[i - 2].low, candles[i + 2].low): + if candles[i].low < min( + candles[i - 1].low, + candles[i + 1].low, + candles[i - 2].low, + candles[i + 2].low, + ): return candles[i] diff --git a/src/aiomql/contrib/strategies/chaos.py b/src/aiomql/contrib/strategies/chaos.py index f1dbf31..2f8685a 100644 --- a/src/aiomql/contrib/strategies/chaos.py +++ b/src/aiomql/contrib/strategies/chaos.py @@ -12,6 +12,7 @@ logger = getLogger(__name__) class Chaos(Strategy): """A chaotic strategy that buys and sells randomly.""" + ltf: TimeFrame htf: TimeFrame lcc: int @@ -19,39 +20,70 @@ class Chaos(Strategy): fast_ema: int slow_ema: int tracker: Tracker - parameters = {"fast_ema": 8, "slow_ema": 20, "ltf": TimeFrame.M1, "htf": TimeFrame.M2, "lcc": 100, "hcc": 100} + parameters = { + "fast_ema": 8, + "slow_ema": 20, + "ltf": TimeFrame.M1, + "htf": TimeFrame.M2, + "lcc": 100, + "hcc": 100, + } - def __init__(self, *, symbol: ForexSymbol, params: dict = None, sessions=None, name='Chaos'): + def __init__( + self, *, symbol: ForexSymbol, params: dict = None, sessions=None, name="Chaos" + ): super().__init__(symbol=symbol, params=params, sessions=sessions, name=name) self.tracker = Tracker(snooze=self.ltf.seconds) self.trader = ScalpTrader(symbol=self.symbol) async def check_trend(self): try: - candles = await self.symbol.copy_rates_from_pos(timeframe=self.htf, count=self.hcc) - if ((current := candles[-1]) and current.time < self.tracker.trend_time - and current.close == self.tracker.last_trend_price): + candles = await self.symbol.copy_rates_from_pos( + timeframe=self.htf, count=self.hcc + ) + if ( + (current := candles[-1]) + and current.time < self.tracker.trend_time + and current.close == self.tracker.last_trend_price + ): self.tracker.update(new=False, order_type=None, snooze=5) return - self.tracker.update(new=True, trend_time=current.time, last_trend_price=current.close) + self.tracker.update( + new=True, trend_time=current.time, last_trend_price=current.close + ) candles.ta.ema(length=self.slow_ema, append=True, fillna=0) candles.ta.ema(length=self.fast_ema, append=True, fillna=0) - candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"}) + candles.rename( + inplace=True, + **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"}, + ) order_type = random.choice([OrderType.BUY, OrderType.SELL]) if order_type == OrderType.BUY: - self.tracker.update(trend="bullish", snooze=self.htf.seconds, order_type=OrderType.BUY) + self.tracker.update( + trend="bullish", snooze=self.htf.seconds, order_type=OrderType.BUY + ) else: - self.tracker.update(trend="bearish", snooze=self.htf.seconds, order_type=OrderType.SELL) + self.tracker.update( + trend="bearish", snooze=self.htf.seconds, order_type=OrderType.SELL + ) except Exception as err: logger.error(f"{err}. Failed to check trend") - self.tracker.update(trend="ranging", snooze=self.ltf.seconds, order_type=None) + self.tracker.update( + trend="ranging", snooze=self.ltf.seconds, order_type=None + ) async def trade(self): - print(f"Trading {self.symbol.name} with {self.__class__.__name__}") - await self.check_trend() - if self.tracker.order_type is not None: - await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters) - self.tracker.update(order_type=None) - await self.sleep(secs=self.tracker.snooze) - else: - await self.sleep(secs=self.tracker.snooze) + try: + await self.check_trend() + if self.tracker.order_type is not None: + await self.trader.place_trade( + order_type=self.tracker.order_type, parameters=self.parameters + ) + self.tracker.update(order_type=None) + await self.sleep(secs=self.tracker.snooze) + else: + await self.sleep(secs=self.tracker.snooze) + except Exception as err: + logger.error( + f"{err}. Failed to trade {self.symbol.name} with {self.__class__.__name__}" + ) diff --git a/src/aiomql/contrib/strategies/finger_trap.py b/src/aiomql/contrib/strategies/finger_trap.py index 72eeaaa..63fe1ed 100644 --- a/src/aiomql/contrib/strategies/finger_trap.py +++ b/src/aiomql/contrib/strategies/finger_trap.py @@ -24,25 +24,46 @@ class FingerTrap(Strategy): tcc: int trader: Trader tracker: Tracker - parameters = {"fast_ema": 8, "slow_ema": 20, "etf": TimeFrame.M5, - "ttf": TimeFrame.H1, "entry_ema": 5, "tcc": 672, "ecc": 3360} + parameters = { + "fast_ema": 8, + "slow_ema": 20, + "etf": TimeFrame.M5, + "ttf": TimeFrame.H1, + "entry_ema": 5, + "tcc": 672, + "ecc": 3360, + } - def __init__(self, *, symbol: Symbol, params: dict | None = None, trader: Trader = None, sessions: Sessions = None, - name: str = 'FingerTrap'): + def __init__( + self, + *, + symbol: Symbol, + params: dict | None = None, + trader: Trader = None, + sessions: Sessions = None, + name: str = "FingerTrap", + ): super().__init__(symbol=symbol, params=params, sessions=sessions, name=name) self.trader = trader or SimpleTrader(symbol=self.symbol) self.tracker: Tracker = Tracker(snooze=self.ttf.seconds) async def check_trend(self): try: - candles: Candles = await self.symbol.copy_rates_from_pos(timeframe=self.ttf, count=self.tcc) + candles: Candles = await self.symbol.copy_rates_from_pos( + timeframe=self.ttf, count=self.tcc + ) if (current := candles[-1]) and current.time < self.tracker.trend_time: self.tracker.update(new=False, order_type=None) return - self.tracker.update(new=True, trend_time=current.time, last_trend_price=current.close) + self.tracker.update( + new=True, trend_time=current.time, last_trend_price=current.close + ) candles.ta.ema(length=self.slow_ema, append=True, fillna=0) candles.ta.ema(length=self.fast_ema, append=True, fillna=0) - candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"}) + candles.rename( + inplace=True, + **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"}, + ) fas = candles.ta_lib.above(candles.fast, candles.slow) fbs = candles.ta_lib.below(candles.fast, candles.slow) @@ -54,34 +75,52 @@ class FingerTrap(Strategy): elif fbs.iloc[-1] and cbf.iloc[-1] and current.is_bearish(): self.tracker.update(trend="bearish") else: - self.tracker.update(trend="ranging", snooze=self.ttf.seconds, order_type=None) - self.tracker.update(trend="bullish") # remove this line + self.tracker.update( + trend="ranging", snooze=self.ttf.seconds, order_type=None + ) + self.tracker.update(trend="bullish") # remove this line except Exception as err: - logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.check_trend") + logger.error( + f"{err} for {self.symbol} in {self.__class__.__name__}.check_trend" + ) self.tracker.update(snooze=self.ttf.seconds, order_type=None) async def confirm_trend(self): try: - candles = await self.symbol.copy_rates_from_pos(timeframe=self.etf, count=self.ecc) + candles = await self.symbol.copy_rates_from_pos( + timeframe=self.etf, count=self.ecc + ) if (current := candles[-1]) and current.time < self.tracker.entry_time: self.tracker.update(new=False, order_type=None) return - self.tracker.update(new=True, trend_time=current.time, last_entry_price=current.close) + self.tracker.update( + new=True, trend_time=current.time, last_entry_price=current.close + ) candles.ta.ema(length=self.entry_ema, append=True) candles.rename(**{f"EMA_{self.entry_ema}": "ema"}) - candles['cae'] = candles.ta_lib.cross(candles.close, candles.ema) - candles['cbe'] = candles.ta_lib.cross(candles.close, candles.ema, above=False) + candles["cae"] = candles.ta_lib.cross(candles.close, candles.ema) + candles["cbe"] = candles.ta_lib.cross( + candles.close, candles.ema, above=False + ) current = candles[-1] - if self.tracker.bullish and True or current.cae: # change True to current.cae + if ( + self.tracker.bullish and True or current.cae + ): # change True to current.cae sl = find_bullish_fractal(candles).low - self.tracker.update(snooze=self.ttf.seconds, order_type=OrderType.BUY, sl=sl) + self.tracker.update( + snooze=self.ttf.seconds, order_type=OrderType.BUY, sl=sl + ) elif self.tracker.bearish and current.cbe: sl = find_bearish_fractal(candles).high - self.tracker.update(snooze=self.ttf.seconds, order_type=OrderType.SELL, sl=sl) + self.tracker.update( + snooze=self.ttf.seconds, order_type=OrderType.SELL, sl=sl + ) else: self.tracker.update(snooze=self.etf.seconds, order_type=None) except Exception as err: - logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.confirm_trend") + logger.error( + f"{err} for {self.symbol} in {self.__class__.__name__}.confirm_trend" + ) self.tracker.update(snooze=self.etf.seconds, order_type=None) async def watch_market(self): @@ -99,8 +138,11 @@ class FingerTrap(Strategy): if self.tracker.order_type is None: await self.sleep(secs=self.tracker.snooze) return - await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters, - sl=self.tracker.sl) + await self.trader.place_trade( + order_type=self.tracker.order_type, + parameters=self.parameters, + sl=self.tracker.sl, + ) await self.sleep(secs=self.tracker.snooze) except Exception as err: logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade") diff --git a/src/aiomql/contrib/symbols/forex_symbol.py b/src/aiomql/contrib/symbols/forex_symbol.py index e23fa2f..b63abfc 100644 --- a/src/aiomql/contrib/symbols/forex_symbol.py +++ b/src/aiomql/contrib/symbols/forex_symbol.py @@ -23,8 +23,10 @@ class ForexSymbol(Symbol): """ points = amount / (volume * self.point * self.trade_contract_size) return points - - async 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. Given the amount and the number of points. Args: @@ -35,6 +37,8 @@ class ForexSymbol(Symbol): volume = amount / (self.point * points * self.trade_contract_size) return self.round_off_volume(volume=volume, round_down=round_down) - async 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: volume = amount / (abs(price - sl) * self.trade_contract_size) return self.round_off_volume(volume=volume, round_down=round_down) diff --git a/src/aiomql/contrib/traders/scalp_trader.py b/src/aiomql/contrib/traders/scalp_trader.py index fcc0220..3325136 100644 --- a/src/aiomql/contrib/traders/scalp_trader.py +++ b/src/aiomql/contrib/traders/scalp_trader.py @@ -7,7 +7,9 @@ logger = getLogger(__name__) class ScalpTrader(Trader): - async def place_trade(self, *, order_type: OrderType, volume: float = None, parameters: dict = None): + async def place_trade( + self, *, order_type: OrderType, volume: float = None, parameters: dict = None + ): """Places a trade based on the order_type and a given stop_loss Args: @@ -21,9 +23,11 @@ class ScalpTrader(Trader): await self.create_order_no_stops(order_type=order_type, volume=volume) if not await self.check_order(): return - self.order.comment = self.parameters.get('name', self.__class__.__name__) + self.order.comment = self.parameters.get("name", self.__class__.__name__) res = await self.send_order() if res is not None: await self.record_trade(result=res, parameters=self.parameters) except Exception as err: - logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}") + logger.error( + f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}" + ) diff --git a/src/aiomql/contrib/traders/simple_trader.py b/src/aiomql/contrib/traders/simple_trader.py index f7f13a4..846d17a 100644 --- a/src/aiomql/contrib/traders/simple_trader.py +++ b/src/aiomql/contrib/traders/simple_trader.py @@ -7,7 +7,9 @@ logger = getLogger(__name__) class SimpleTrader(Trader): - async def place_trade(self, *, order_type: OrderType, sl: float, parameters: dict = None): + async def place_trade( + self, *, order_type: OrderType, sl: float, parameters: dict = None + ): """Places a trade based on the order_type and a given stop_loss Args: @@ -20,7 +22,9 @@ class SimpleTrader(Trader): await self.create_order_with_sl(order_type=order_type, sl=sl) if not await self.check_order(): return - self.order.comment = self.parameters.get('name', self.__class__.__name__) + self.order.comment = self.parameters.get("name", self.__class__.__name__) await self.send_order() except Exception as err: - logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}") + logger.error( + f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}" + ) diff --git a/src/aiomql/contrib/utils/tracker.py b/src/aiomql/contrib/utils/tracker.py index 83ff312..74cf237 100644 --- a/src/aiomql/contrib/utils/tracker.py +++ b/src/aiomql/contrib/utils/tracker.py @@ -1,12 +1,13 @@ from dataclasses import dataclass from typing import Literal -from aiomql.core.constants import OrderType +from ...core.constants import OrderType @dataclass class Tracker: """Keeps track of a strategy's data and state""" + trend: Literal["ranging", "bullish", "bearish"] = "ranging" bullish: bool = False bearish: bool = False @@ -26,7 +27,7 @@ class Tracker: for key in kwargs: if key in fields: setattr(self, key, kwargs[key]) - if 'trend' in kwargs: + if "trend" in kwargs: match self.trend: case "ranging": self.ranging = True diff --git a/src/aiomql/core/__init__.py b/src/aiomql/core/__init__.py index a62c5af..37e8d34 100644 --- a/src/aiomql/core/__init__.py +++ b/src/aiomql/core/__init__.py @@ -7,3 +7,4 @@ from .base import Base, _Base from .errors import Error from .exceptions import * from .task_queue import TaskQueue +from .backtesting import * diff --git a/src/aiomql/core/_core.py b/src/aiomql/core/_core.py index 342f48c..43c3149 100644 --- a/src/aiomql/core/_core.py +++ b/src/aiomql/core/_core.py @@ -1,91 +1,320 @@ from typing import Callable import MetaTrader5 -from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TerminalInfo, TradeOrder, TradePosition, TradeDeal, - OrderCheckResult, OrderSendResult, BookInfo, TradeRequest) +from MetaTrader5 import ( + Tick, + SymbolInfo, + AccountInfo, + TerminalInfo, + TradeOrder, + TradePosition, + TradeDeal, + OrderCheckResult, + OrderSendResult, + BookInfo, + TradeRequest, +) from .config import Config -constants = ('TIMEFRAME_M1', 'TIMEFRAME_M2', 'TIMEFRAME_M3', 'TIMEFRAME_M4', 'TIMEFRAME_M5', 'TIMEFRAME_M6', - 'TIMEFRAME_M10', 'TIMEFRAME_M12', 'TIMEFRAME_M15', 'TIMEFRAME_M20', 'TIMEFRAME_M30', 'TIMEFRAME_H1', - 'TIMEFRAME_H2', 'TIMEFRAME_H4', 'TIMEFRAME_H3', 'TIMEFRAME_H6', 'TIMEFRAME_H8', 'TIMEFRAME_H12', - 'TIMEFRAME_D1', 'TIMEFRAME_W1', 'TIMEFRAME_MN1', 'COPY_TICKS_ALL', 'COPY_TICKS_INFO', 'COPY_TICKS_TRADE', - 'TICK_FLAG_BID', 'TICK_FLAG_ASK', 'TICK_FLAG_LAST', 'TICK_FLAG_VOLUME', 'TICK_FLAG_BUY', 'TICK_FLAG_SELL', - 'POSITION_TYPE_BUY', 'POSITION_TYPE_SELL', 'POSITION_REASON_CLIENT', 'POSITION_REASON_MOBILE', - 'POSITION_REASON_WEB', 'POSITION_REASON_EXPERT', 'ORDER_TYPE_BUY', 'ORDER_TYPE_SELL', - 'ORDER_TYPE_BUY_LIMIT', 'ORDER_TYPE_SELL_LIMIT', 'ORDER_TYPE_BUY_STOP', 'ORDER_TYPE_SELL_STOP', - 'ORDER_TYPE_BUY_STOP_LIMIT', 'ORDER_TYPE_SELL_STOP_LIMIT', 'ORDER_TYPE_CLOSE_BY', 'ORDER_STATE_STARTED', - 'ORDER_STATE_PLACED', 'ORDER_STATE_CANCELED', 'ORDER_STATE_PARTIAL', 'ORDER_STATE_FILLED', - 'ORDER_STATE_REJECTED', 'ORDER_STATE_EXPIRED', 'ORDER_STATE_REQUEST_ADD', 'ORDER_STATE_REQUEST_MODIFY', - 'ORDER_STATE_REQUEST_CANCEL', 'ORDER_FILLING_FOK', 'ORDER_FILLING_IOC', 'ORDER_FILLING_RETURN', - 'ORDER_FILLING_BOC', 'ORDER_TIME_GTC', 'ORDER_TIME_DAY', 'ORDER_TIME_SPECIFIED', - 'ORDER_TIME_SPECIFIED_DAY', 'ORDER_REASON_CLIENT', 'ORDER_REASON_MOBILE', 'ORDER_REASON_WEB', - 'ORDER_REASON_EXPERT', 'ORDER_REASON_SL', 'ORDER_REASON_TP', 'ORDER_REASON_SO', 'DEAL_TYPE_BUY', - 'DEAL_TYPE_SELL', 'DEAL_TYPE_BALANCE', 'DEAL_TYPE_CREDIT', 'DEAL_TYPE_CHARGE', 'DEAL_TYPE_CORRECTION', - 'DEAL_TYPE_BONUS', 'DEAL_TYPE_COMMISSION', 'DEAL_TYPE_COMMISSION_DAILY', 'DEAL_TYPE_COMMISSION_MONTHLY', - 'DEAL_TYPE_COMMISSION_AGENT_DAILY', 'DEAL_TYPE_COMMISSION_AGENT_MONTHLY', 'DEAL_TYPE_INTEREST', - 'DEAL_TYPE_BUY_CANCELED', 'DEAL_TYPE_SELL_CANCELED', 'DEAL_DIVIDEND', 'DEAL_DIVIDEND_FRANKED', 'DEAL_TAX', - 'DEAL_ENTRY_IN', 'DEAL_ENTRY_OUT', 'DEAL_ENTRY_INOUT', 'DEAL_ENTRY_OUT_BY', 'DEAL_REASON_CLIENT', - 'DEAL_REASON_MOBILE', 'DEAL_REASON_WEB', 'DEAL_REASON_EXPERT', 'DEAL_REASON_SL', 'DEAL_REASON_TP', - 'DEAL_REASON_SO', 'DEAL_REASON_ROLLOVER', 'DEAL_REASON_VMARGIN', 'DEAL_REASON_SPLIT', 'TRADE_ACTION_DEAL', - 'TRADE_ACTION_PENDING', 'TRADE_ACTION_SLTP', 'TRADE_ACTION_MODIFY', 'TRADE_ACTION_REMOVE', - 'TRADE_ACTION_CLOSE_BY', 'SYMBOL_CHART_MODE_BID', 'SYMBOL_CHART_MODE_LAST', 'SYMBOL_CALC_MODE_FOREX', - 'SYMBOL_CALC_MODE_FUTURES', 'SYMBOL_CALC_MODE_CFD', 'SYMBOL_CALC_MODE_CFDINDEX', - 'SYMBOL_CALC_MODE_CFDLEVERAGE', 'SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE', 'SYMBOL_CALC_MODE_EXCH_STOCKS', - 'SYMBOL_CALC_MODE_EXCH_FUTURES', 'SYMBOL_CALC_MODE_EXCH_OPTIONS', 'SYMBOL_CALC_MODE_EXCH_OPTIONS_MARGIN', - 'SYMBOL_CALC_MODE_EXCH_BONDS', 'SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX', 'SYMBOL_CALC_MODE_EXCH_BONDS_MOEX', - 'SYMBOL_CALC_MODE_SERV_COLLATERAL', 'SYMBOL_TRADE_MODE_DISABLED', 'SYMBOL_TRADE_MODE_LONGONLY', - 'SYMBOL_TRADE_MODE_SHORTONLY', 'SYMBOL_TRADE_MODE_CLOSEONLY', 'SYMBOL_TRADE_MODE_FULL', - 'SYMBOL_TRADE_EXECUTION_REQUEST', 'SYMBOL_TRADE_EXECUTION_INSTANT', 'SYMBOL_TRADE_EXECUTION_MARKET', - 'SYMBOL_TRADE_EXECUTION_EXCHANGE', 'SYMBOL_SWAP_MODE_DISABLED', 'SYMBOL_SWAP_MODE_POINTS', - 'SYMBOL_SWAP_MODE_CURRENCY_SYMBOL', 'SYMBOL_SWAP_MODE_CURRENCY_MARGIN', - 'SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT', 'SYMBOL_SWAP_MODE_INTEREST_CURRENT', 'SYMBOL_SWAP_MODE_INTEREST_OPEN', - 'SYMBOL_SWAP_MODE_REOPEN_CURRENT', 'SYMBOL_SWAP_MODE_REOPEN_BID', 'DAY_OF_WEEK_SUNDAY', - 'DAY_OF_WEEK_MONDAY', 'DAY_OF_WEEK_TUESDAY', 'DAY_OF_WEEK_WEDNESDAY', 'DAY_OF_WEEK_THURSDAY', - 'DAY_OF_WEEK_FRIDAY', 'DAY_OF_WEEK_SATURDAY', 'SYMBOL_ORDERS_GTC', 'SYMBOL_ORDERS_DAILY', - 'SYMBOL_ORDERS_DAILY_NO_STOPS', 'SYMBOL_OPTION_RIGHT_CALL', 'SYMBOL_OPTION_RIGHT_PUT', - 'SYMBOL_OPTION_MODE_EUROPEAN', 'SYMBOL_OPTION_MODE_AMERICAN', 'ACCOUNT_TRADE_MODE_DEMO', - 'ACCOUNT_TRADE_MODE_CONTEST', 'ACCOUNT_TRADE_MODE_REAL', 'ACCOUNT_STOPOUT_MODE_PERCENT', - 'ACCOUNT_STOPOUT_MODE_MONEY', 'ACCOUNT_MARGIN_MODE_RETAIL_NETTING', 'ACCOUNT_MARGIN_MODE_EXCHANGE', - 'ACCOUNT_MARGIN_MODE_RETAIL_HEDGING', 'BOOK_TYPE_SELL', 'BOOK_TYPE_BUY', 'BOOK_TYPE_SELL_MARKET', - 'BOOK_TYPE_BUY_MARKET', 'TRADE_RETCODE_REQUOTE', 'TRADE_RETCODE_REJECT', 'TRADE_RETCODE_CANCEL', - 'TRADE_RETCODE_PLACED', 'TRADE_RETCODE_DONE', 'TRADE_RETCODE_DONE_PARTIAL', 'TRADE_RETCODE_ERROR', - 'TRADE_RETCODE_TIMEOUT', 'TRADE_RETCODE_INVALID', 'TRADE_RETCODE_INVALID_VOLUME', - 'TRADE_RETCODE_INVALID_PRICE', 'TRADE_RETCODE_INVALID_STOPS', 'TRADE_RETCODE_TRADE_DISABLED', - 'TRADE_RETCODE_MARKET_CLOSED', 'TRADE_RETCODE_NO_MONEY', 'TRADE_RETCODE_PRICE_CHANGED', - 'TRADE_RETCODE_PRICE_OFF', 'TRADE_RETCODE_INVALID_EXPIRATION', 'TRADE_RETCODE_ORDER_CHANGED', - 'TRADE_RETCODE_TOO_MANY_REQUESTS', 'TRADE_RETCODE_NO_CHANGES', 'TRADE_RETCODE_SERVER_DISABLES_AT', - 'TRADE_RETCODE_CLIENT_DISABLES_AT', 'TRADE_RETCODE_LOCKED', 'TRADE_RETCODE_FROZEN', - 'TRADE_RETCODE_INVALID_FILL', 'TRADE_RETCODE_CONNECTION', 'TRADE_RETCODE_ONLY_REAL', - 'TRADE_RETCODE_LIMIT_ORDERS', 'TRADE_RETCODE_LIMIT_VOLUME', 'TRADE_RETCODE_INVALID_ORDER', - 'TRADE_RETCODE_POSITION_CLOSED', 'TRADE_RETCODE_INVALID_CLOSE_VOLUME', 'TRADE_RETCODE_CLOSE_ORDER_EXIST', - 'TRADE_RETCODE_LIMIT_POSITIONS', 'TRADE_RETCODE_REJECT_CANCEL', 'TRADE_RETCODE_LONG_ONLY', - 'TRADE_RETCODE_SHORT_ONLY', 'TRADE_RETCODE_CLOSE_ONLY', 'TRADE_RETCODE_FIFO_CLOSE', 'RES_S_OK', - 'RES_E_FAIL', 'RES_E_INVALID_PARAMS', 'RES_E_NO_MEMORY', 'RES_E_NOT_FOUND', 'RES_E_INVALID_VERSION', - 'RES_E_AUTH_FAILED', 'RES_E_UNSUPPORTED', 'RES_E_AUTO_TRADING_DISABLED', 'RES_E_INTERNAL_FAIL', - 'RES_E_INTERNAL_FAIL_SEND', 'RES_E_INTERNAL_FAIL_RECEIVE', 'RES_E_INTERNAL_FAIL_INIT', - 'RES_E_INTERNAL_FAIL_CONNECT', 'RES_E_INTERNAL_FAIL_TIMEOUT') -core_mt5_functions = ('initialize', 'shutdown', 'login', 'version', 'terminal_info', 'account_info', 'copy_ticks_from', - 'copy_ticks_range', 'copy_rates_from', 'copy_rates_from_pos', 'copy_rates_range', 'positions_total', - 'positions_get', 'orders_total', 'orders_get', 'history_orders_total', 'history_orders_get', - 'history_deals_total', 'history_deals_get', 'order_check', 'order_send', 'order_calc_margin', - 'order_calc_profit', 'symbol_info', 'symbol_info_tick', 'symbol_select', 'symbols_total', 'symbols_get', - 'market_book_add', 'market_book_release', 'market_book_get', 'last_error') -types = ('TradePosition', 'TradeOrder', 'TradeDeal', 'TradeRequest', 'OrderSendResult', 'OrderCheckResult', 'Tick', - 'TerminalInfo', 'SymbolInfo', 'AccountInfo', 'BookInfo') +constants = ( + "TIMEFRAME_M1", + "TIMEFRAME_M2", + "TIMEFRAME_M3", + "TIMEFRAME_M4", + "TIMEFRAME_M5", + "TIMEFRAME_M6", + "TIMEFRAME_M10", + "TIMEFRAME_M12", + "TIMEFRAME_M15", + "TIMEFRAME_M20", + "TIMEFRAME_M30", + "TIMEFRAME_H1", + "TIMEFRAME_H2", + "TIMEFRAME_H4", + "TIMEFRAME_H3", + "TIMEFRAME_H6", + "TIMEFRAME_H8", + "TIMEFRAME_H12", + "TIMEFRAME_D1", + "TIMEFRAME_W1", + "TIMEFRAME_MN1", + "COPY_TICKS_ALL", + "COPY_TICKS_INFO", + "COPY_TICKS_TRADE", + "TICK_FLAG_BID", + "TICK_FLAG_ASK", + "TICK_FLAG_LAST", + "TICK_FLAG_VOLUME", + "TICK_FLAG_BUY", + "TICK_FLAG_SELL", + "POSITION_TYPE_BUY", + "POSITION_TYPE_SELL", + "POSITION_REASON_CLIENT", + "POSITION_REASON_MOBILE", + "POSITION_REASON_WEB", + "POSITION_REASON_EXPERT", + "ORDER_TYPE_BUY", + "ORDER_TYPE_SELL", + "ORDER_TYPE_BUY_LIMIT", + "ORDER_TYPE_SELL_LIMIT", + "ORDER_TYPE_BUY_STOP", + "ORDER_TYPE_SELL_STOP", + "ORDER_TYPE_BUY_STOP_LIMIT", + "ORDER_TYPE_SELL_STOP_LIMIT", + "ORDER_TYPE_CLOSE_BY", + "ORDER_STATE_STARTED", + "ORDER_STATE_PLACED", + "ORDER_STATE_CANCELED", + "ORDER_STATE_PARTIAL", + "ORDER_STATE_FILLED", + "ORDER_STATE_REJECTED", + "ORDER_STATE_EXPIRED", + "ORDER_STATE_REQUEST_ADD", + "ORDER_STATE_REQUEST_MODIFY", + "ORDER_STATE_REQUEST_CANCEL", + "ORDER_FILLING_FOK", + "ORDER_FILLING_IOC", + "ORDER_FILLING_RETURN", + "ORDER_FILLING_BOC", + "ORDER_TIME_GTC", + "ORDER_TIME_DAY", + "ORDER_TIME_SPECIFIED", + "ORDER_TIME_SPECIFIED_DAY", + "ORDER_REASON_CLIENT", + "ORDER_REASON_MOBILE", + "ORDER_REASON_WEB", + "ORDER_REASON_EXPERT", + "ORDER_REASON_SL", + "ORDER_REASON_TP", + "ORDER_REASON_SO", + "DEAL_TYPE_BUY", + "DEAL_TYPE_SELL", + "DEAL_TYPE_BALANCE", + "DEAL_TYPE_CREDIT", + "DEAL_TYPE_CHARGE", + "DEAL_TYPE_CORRECTION", + "DEAL_TYPE_BONUS", + "DEAL_TYPE_COMMISSION", + "DEAL_TYPE_COMMISSION_DAILY", + "DEAL_TYPE_COMMISSION_MONTHLY", + "DEAL_TYPE_COMMISSION_AGENT_DAILY", + "DEAL_TYPE_COMMISSION_AGENT_MONTHLY", + "DEAL_TYPE_INTEREST", + "DEAL_TYPE_BUY_CANCELED", + "DEAL_TYPE_SELL_CANCELED", + "DEAL_DIVIDEND", + "DEAL_DIVIDEND_FRANKED", + "DEAL_TAX", + "DEAL_ENTRY_IN", + "DEAL_ENTRY_OUT", + "DEAL_ENTRY_INOUT", + "DEAL_ENTRY_OUT_BY", + "DEAL_REASON_CLIENT", + "DEAL_REASON_MOBILE", + "DEAL_REASON_WEB", + "DEAL_REASON_EXPERT", + "DEAL_REASON_SL", + "DEAL_REASON_TP", + "DEAL_REASON_SO", + "DEAL_REASON_ROLLOVER", + "DEAL_REASON_VMARGIN", + "DEAL_REASON_SPLIT", + "TRADE_ACTION_DEAL", + "TRADE_ACTION_PENDING", + "TRADE_ACTION_SLTP", + "TRADE_ACTION_MODIFY", + "TRADE_ACTION_REMOVE", + "TRADE_ACTION_CLOSE_BY", + "SYMBOL_CHART_MODE_BID", + "SYMBOL_CHART_MODE_LAST", + "SYMBOL_CALC_MODE_FOREX", + "SYMBOL_CALC_MODE_FUTURES", + "SYMBOL_CALC_MODE_CFD", + "SYMBOL_CALC_MODE_CFDINDEX", + "SYMBOL_CALC_MODE_CFDLEVERAGE", + "SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE", + "SYMBOL_CALC_MODE_EXCH_STOCKS", + "SYMBOL_CALC_MODE_EXCH_FUTURES", + "SYMBOL_CALC_MODE_EXCH_OPTIONS", + "SYMBOL_CALC_MODE_EXCH_OPTIONS_MARGIN", + "SYMBOL_CALC_MODE_EXCH_BONDS", + "SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX", + "SYMBOL_CALC_MODE_EXCH_BONDS_MOEX", + "SYMBOL_CALC_MODE_SERV_COLLATERAL", + "SYMBOL_TRADE_MODE_DISABLED", + "SYMBOL_TRADE_MODE_LONGONLY", + "SYMBOL_TRADE_MODE_SHORTONLY", + "SYMBOL_TRADE_MODE_CLOSEONLY", + "SYMBOL_TRADE_MODE_FULL", + "SYMBOL_TRADE_EXECUTION_REQUEST", + "SYMBOL_TRADE_EXECUTION_INSTANT", + "SYMBOL_TRADE_EXECUTION_MARKET", + "SYMBOL_TRADE_EXECUTION_EXCHANGE", + "SYMBOL_SWAP_MODE_DISABLED", + "SYMBOL_SWAP_MODE_POINTS", + "SYMBOL_SWAP_MODE_CURRENCY_SYMBOL", + "SYMBOL_SWAP_MODE_CURRENCY_MARGIN", + "SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT", + "SYMBOL_SWAP_MODE_INTEREST_CURRENT", + "SYMBOL_SWAP_MODE_INTEREST_OPEN", + "SYMBOL_SWAP_MODE_REOPEN_CURRENT", + "SYMBOL_SWAP_MODE_REOPEN_BID", + "DAY_OF_WEEK_SUNDAY", + "DAY_OF_WEEK_MONDAY", + "DAY_OF_WEEK_TUESDAY", + "DAY_OF_WEEK_WEDNESDAY", + "DAY_OF_WEEK_THURSDAY", + "DAY_OF_WEEK_FRIDAY", + "DAY_OF_WEEK_SATURDAY", + "SYMBOL_ORDERS_GTC", + "SYMBOL_ORDERS_DAILY", + "SYMBOL_ORDERS_DAILY_NO_STOPS", + "SYMBOL_OPTION_RIGHT_CALL", + "SYMBOL_OPTION_RIGHT_PUT", + "SYMBOL_OPTION_MODE_EUROPEAN", + "SYMBOL_OPTION_MODE_AMERICAN", + "ACCOUNT_TRADE_MODE_DEMO", + "ACCOUNT_TRADE_MODE_CONTEST", + "ACCOUNT_TRADE_MODE_REAL", + "ACCOUNT_STOPOUT_MODE_PERCENT", + "ACCOUNT_STOPOUT_MODE_MONEY", + "ACCOUNT_MARGIN_MODE_RETAIL_NETTING", + "ACCOUNT_MARGIN_MODE_EXCHANGE", + "ACCOUNT_MARGIN_MODE_RETAIL_HEDGING", + "BOOK_TYPE_SELL", + "BOOK_TYPE_BUY", + "BOOK_TYPE_SELL_MARKET", + "BOOK_TYPE_BUY_MARKET", + "TRADE_RETCODE_REQUOTE", + "TRADE_RETCODE_REJECT", + "TRADE_RETCODE_CANCEL", + "TRADE_RETCODE_PLACED", + "TRADE_RETCODE_DONE", + "TRADE_RETCODE_DONE_PARTIAL", + "TRADE_RETCODE_ERROR", + "TRADE_RETCODE_TIMEOUT", + "TRADE_RETCODE_INVALID", + "TRADE_RETCODE_INVALID_VOLUME", + "TRADE_RETCODE_INVALID_PRICE", + "TRADE_RETCODE_INVALID_STOPS", + "TRADE_RETCODE_TRADE_DISABLED", + "TRADE_RETCODE_MARKET_CLOSED", + "TRADE_RETCODE_NO_MONEY", + "TRADE_RETCODE_PRICE_CHANGED", + "TRADE_RETCODE_PRICE_OFF", + "TRADE_RETCODE_INVALID_EXPIRATION", + "TRADE_RETCODE_ORDER_CHANGED", + "TRADE_RETCODE_TOO_MANY_REQUESTS", + "TRADE_RETCODE_NO_CHANGES", + "TRADE_RETCODE_SERVER_DISABLES_AT", + "TRADE_RETCODE_CLIENT_DISABLES_AT", + "TRADE_RETCODE_LOCKED", + "TRADE_RETCODE_FROZEN", + "TRADE_RETCODE_INVALID_FILL", + "TRADE_RETCODE_CONNECTION", + "TRADE_RETCODE_ONLY_REAL", + "TRADE_RETCODE_LIMIT_ORDERS", + "TRADE_RETCODE_LIMIT_VOLUME", + "TRADE_RETCODE_INVALID_ORDER", + "TRADE_RETCODE_POSITION_CLOSED", + "TRADE_RETCODE_INVALID_CLOSE_VOLUME", + "TRADE_RETCODE_CLOSE_ORDER_EXIST", + "TRADE_RETCODE_LIMIT_POSITIONS", + "TRADE_RETCODE_REJECT_CANCEL", + "TRADE_RETCODE_LONG_ONLY", + "TRADE_RETCODE_SHORT_ONLY", + "TRADE_RETCODE_CLOSE_ONLY", + "TRADE_RETCODE_FIFO_CLOSE", + "RES_S_OK", + "RES_E_FAIL", + "RES_E_INVALID_PARAMS", + "RES_E_NO_MEMORY", + "RES_E_NOT_FOUND", + "RES_E_INVALID_VERSION", + "RES_E_AUTH_FAILED", + "RES_E_UNSUPPORTED", + "RES_E_AUTO_TRADING_DISABLED", + "RES_E_INTERNAL_FAIL", + "RES_E_INTERNAL_FAIL_SEND", + "RES_E_INTERNAL_FAIL_RECEIVE", + "RES_E_INTERNAL_FAIL_INIT", + "RES_E_INTERNAL_FAIL_CONNECT", + "RES_E_INTERNAL_FAIL_TIMEOUT", +) +core_mt5_functions = ( + "initialize", + "shutdown", + "login", + "version", + "terminal_info", + "account_info", + "copy_ticks_from", + "copy_ticks_range", + "copy_rates_from", + "copy_rates_from_pos", + "copy_rates_range", + "positions_total", + "positions_get", + "orders_total", + "orders_get", + "history_orders_total", + "history_orders_get", + "history_deals_total", + "history_deals_get", + "order_check", + "order_send", + "order_calc_margin", + "order_calc_profit", + "symbol_info", + "symbol_info_tick", + "symbol_select", + "symbols_total", + "symbols_get", + "market_book_add", + "market_book_release", + "market_book_get", + "last_error", +) +types = ( + "TradePosition", + "TradeOrder", + "TradeDeal", + "TradeRequest", + "OrderSendResult", + "OrderCheckResult", + "Tick", + "TerminalInfo", + "SymbolInfo", + "AccountInfo", + "BookInfo", +) class BaseMeta(type): def __new__(mcs, cls_name, bases, cls_dict): - defaults: dict = getattr(MetaTrader5, '__dict__', {}) - callables = {f"_{key}": value for key in core_mt5_functions if (value := defaults.get(key, None)) is not None} - consts = {key: value for key in constants if (value := defaults.get(key, None)) is not None} - types_ = {key: value for key in types if (value := defaults.get(key, None)) is not None} + defaults: dict = getattr(MetaTrader5, "__dict__", {}) + callables = { + f"_{key}": value + for key in core_mt5_functions + if (value := defaults.get(key, None)) is not None + } + consts = { + key: value + for key in constants + if (value := defaults.get(key, None)) is not None + } + types_ = { + key: value + for key in types + if (value := defaults.get(key, None)) is not None + } cls_dict |= callables cls_dict |= consts cls_dict |= types_ return super().__new__(mcs, cls_name, bases, cls_dict) + class MetaCore(metaclass=BaseMeta): TIMEFRAME_M1: int TIMEFRAME_M2: int diff --git a/src/aiomql/contrib/backtesting/__init__.py b/src/aiomql/core/backtesting/__init__.py similarity index 80% rename from src/aiomql/contrib/backtesting/__init__.py rename to src/aiomql/core/backtesting/__init__.py index 3f124fa..ad63b04 100644 --- a/src/aiomql/contrib/backtesting/__init__.py +++ b/src/aiomql/core/backtesting/__init__.py @@ -2,3 +2,4 @@ from .get_data import GetData, BackTestData from .backtest_engine import BackTestEngine from .backtest_account import BackTestAccount from .trades_manager import PositionsManager, OrdersManager, DealsManager +from .backtest_controller import BackTestController diff --git a/src/aiomql/contrib/backtesting/backtest_account.py b/src/aiomql/core/backtesting/backtest_account.py similarity index 72% rename from src/aiomql/contrib/backtesting/backtest_account.py rename to src/aiomql/core/backtesting/backtest_account.py index 186e017..0ec8098 100644 --- a/src/aiomql/contrib/backtesting/backtest_account.py +++ b/src/aiomql/core/backtesting/backtest_account.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import ClassVar -from ...core.constants import AccountTradeMode, AccountMarginMode, AccountStopOutMode +from ..constants import AccountTradeMode, AccountMarginMode, AccountStopOutMode @dataclass @@ -30,13 +30,18 @@ class BackTestAccount: assets: float = 0 liabilities: float = 0 commission_blocked: float = 0 - name: str = '' - server: str = '' + name: str = "" + server: str = "" currency: str = "USD" - company: str = '' + company: str = "" __match_args__: ClassVar[tuple] + def get_dict(self, exclude: set = None, include: set = None): + exclude, include = exclude or set(), include or set() + filter_ = include or set(self.__match_args__).difference(exclude) + return {key: value for key, value in self.asdict().items() if key in filter_} + def asdict(self): res = {key: getattr(self, key) for key in self.__match_args__} return res diff --git a/src/aiomql/core/backtesting/backtest_controller.py b/src/aiomql/core/backtesting/backtest_controller.py new file mode 100644 index 0000000..1a2476c --- /dev/null +++ b/src/aiomql/core/backtesting/backtest_controller.py @@ -0,0 +1,100 @@ +from logging import getLogger +from asyncio import Task +from signal import signal, SIGINT +from threading import Barrier, BrokenBarrierError +from typing import Self +from datetime import datetime + +from ..config import Config +from ..exceptions import StopTrading + +logger = getLogger(__name__) + + +class BackTestController: + _instance: Self + task_tracker: int + config: Config + tasks: list[Task] + + def __new__(cls, *args, **kwargs): + if not hasattr(cls, "_instance"): + cls._instance = super().__new__(cls) + cls._instance.config = Config() + cls._instance.barrier = Barrier(1) + cls._instance.task_tracker = 0 + cls._instance.tasks = [] + return cls._instance + + def __init__(self): + signal(SIGINT, self.sigint_handler) + + @property + def backtest_engine(self): + return self.config.backtest_engine + + def add_tasks(self, *tasks: Task): + self.tasks.extend(tasks) + + def set_parties(self, *, parties: int): + self.barrier._parties = parties + + @property + def parties(self): + return self.barrier.parties + + def sigint_handler(self, sig, frame): + logger.warning("SIGINT received. Stopping the controller.") + self.backtest_engine.stop_testing = True + + async def control(self): + try: + self.backtest_engine.next() + while True: + pending = self.wait() + if ( + pending == 0 + ): # all main tasks have been completed in the current cycle + await self.backtest_engine.tracker() + self.backtest_engine.next() + if self.backtest_engine.cursor.time % 3600 == 0: + logger.info( + datetime.strftime( + datetime.fromtimestamp( + self.backtest_engine.cursor.time + ), + "%Y-%m-%d %H:%M:%S", + ) + ) + if self.backtest_engine.stop_testing: + logger.info( + "Stop trading called in control at %s", + self.backtest_engine.cursor.time, + ) + break + await self.backtest_engine.wrap_up() + self.stop_backtesting() + except BrokenBarrierError: + return + + except Exception as err: + logger.error("Error: %s in controller", err) + await self.backtest_engine.wrap_up() + self.stop_backtesting() + return + + def stop_backtesting(self): + self.abort() + self.config.shutdown = True + + def wait(self): + try: + pending = self.barrier.wait() + return pending + except BrokenBarrierError: + raise StopTrading + except Exception as err: + logger.error("Error: %s in wait", err) + + def abort(self): + self.barrier.abort() diff --git a/src/aiomql/core/backtesting/backtest_engine.py b/src/aiomql/core/backtesting/backtest_engine.py new file mode 100644 index 0000000..410b504 --- /dev/null +++ b/src/aiomql/core/backtesting/backtest_engine.py @@ -0,0 +1,1499 @@ +from threading import RLock +import asyncio +import json +from datetime import datetime, UTC +from typing import Literal +from itertools import zip_longest +import random +from functools import cached_property +from logging import getLogger +from pathlib import Path + +import pandas as pd +import numpy as np +from pandas import DataFrame +from MetaTrader5 import ( + Tick, + SymbolInfo, + AccountInfo, + TradeOrder, + TradePosition, + TradeDeal, + TradeRequest, + OrderCheckResult, + OrderSendResult, + TerminalInfo, +) + +from ..meta_trader import MetaTrader +from ..constants import ( + TimeFrame, + OrderType, + TradeAction, + AccountStopOutMode, + PositionReason, + DealType, + DealReason, + DealEntry, + OrderReason, + CopyTicks, +) + +from ..._utils import ( + round_down, + round_up, + error_handler, + error_handler_sync, + async_cache, +) + +from .get_data import BackTestData, GetData, Cursor +from .backtest_account import BackTestAccount +from .trades_manager import PositionsManager, OrdersManager, DealsManager + +logger = getLogger(__name__) + + +class BackTestEngine: + mt5: MetaTrader + span: range + range: range + speed: int + cursor: Cursor + iter: zip_longest + rates: dict[str, dict[int, DataFrame]] + ticks: dict[str, DataFrame] + prices: dict[str, DataFrame] + orders: OrdersManager + deals: DealsManager + positions: PositionsManager + _account: BackTestAccount + stop_testing: bool + use_terminal: bool + restart: bool + stop_time: int | None + close_open_positions_on_exit: bool + preloaded_ticks: dict[str, DataFrame] + preload: bool + account_lock: RLock + use_termial_in_tracker: bool + + def __init__( + self, + *, + data: BackTestData = None, + speed: int = 60, + start: float | datetime = 0, + end: float | datetime = 0, + restart: bool = True, + use_terminal: bool = None, + name: str = "", + stop_time: float | datetime = None, + close_open_positions_on_exit: bool = False, + preload=True, + assign_to_config: bool = False, + use_termial_in_tracker: bool = False, + ): + self._data = data or BackTestData() + self.mt5 = MetaTrader() + self.config = self.mt5.config + if assign_to_config: + self.config.backtest_engine = self + self.setup_test_range(start=start, end=end, speed=speed, restart=restart) + self.setup_data(restart=restart) + start, end = ( + (self.span[0], self.span[-1]) + if len(self.span) >= 2 + else ((now := datetime.now(UTC).timestamp()), now) + ) + start, end = datetime.fromtimestamp(start, tz=UTC), datetime.fromtimestamp( + end, tz=UTC + ) + self.name = ( + name or self._data.name or f"backtest_data_{start:%d_%m_%y}_{end:%d_%m_%y}" + ) + self.stop_testing = False + self.use_terminal = ( + self.config.use_terminal_for_backtesting + if use_terminal is None + else use_terminal + ) + self.close_open_positions_on_exit = close_open_positions_on_exit + if stop_time is not None: + val = ( + stop_time.astimezone(tz=UTC) + if isinstance(stop_time, datetime) + else datetime.fromtimestamp(stop_time, tz=UTC) + ) + stop_time = int(val.timestamp()) + self.stop_time = stop_time + self.preload = preload + self.preloaded_ticks = {} + self.account_lock = RLock() + + def __next__(self) -> Cursor: + try: + index, time = next(self.iter) + if self.stop_time and time >= self.stop_time: + raise StopIteration + self.cursor = Cursor(index=index, time=time) + return self.cursor + except StopIteration: + logger.critical("End of the test range") + self.stop_testing = True + + def __repr__(self): + return f"{self.__class__.__name__}()" + + def setup_test_range( + self, + *, + start: float | datetime = None, + end: float | datetime = None, + speed: int = 60, + restart: bool = True, + ): + if self._data.span and self._data.range: + start = start or self._data.span[0] + end = end or self._data.span[-1] + 1 + start = ( + start.astimezone(tz=UTC) + if isinstance(start, datetime) + else datetime.fromtimestamp(start, tz=UTC) + ) + end = ( + end.astimezone(tz=UTC) + if isinstance(end, datetime) + else datetime.fromtimestamp(end, tz=UTC) + ) + span_start = int(start.timestamp()) + span_end = int(end.timestamp()) + self.speed = speed + self.span = range(span_start, span_end, speed) + self.range = range(0, span_end - span_start, speed) + self.iter = zip_longest(self.range, self.span) + + if restart is False and self._data.cursor is not None: + self.cursor = self._data.cursor + self.go_to(time=self.cursor.time) + else: + self.cursor = Cursor(index=self.range.start, time=self.span.start) + + def setup_data(self, *, restart: bool = True): + if restart is True: + self.orders = OrdersManager() + self.positions = PositionsManager() + self.deals = DealsManager() + self._account = BackTestAccount() + return + + orders = {} + for ticket, order in self._data.orders.items(): + orders[ticket] = TradeOrder( + (order.get(k) for k in TradeOrder.__match_args__) + ) + self.orders = OrdersManager(data=orders) + + positions = {} + for ticket, position in self._data.positions.items(): + positions[ticket] = TradePosition( + (position.get(k) for k in TradePosition.__match_args__) + ) + self.positions = PositionsManager( + data=positions, + open_positions=self._data.open_positions, + margins=self._data.margins, + ) + + deals = {} + for ticket, deal in self._data.deals.items(): + deals[ticket] = TradeDeal((deal.get(k) for k in TradeDeal.__match_args__)) + self.deals = DealsManager(data=deals) + + self._account = BackTestAccount(**self._data.account) + + def next(self) -> Cursor: + return next(self) + + @property + def data(self): + return self._data + + def reset(self, clear_data: bool = False): + self.iter = zip_longest(self.range, self.span) + self.cursor = Cursor(index=self.range.start, time=self.span.start) + if clear_data: + self.setup_data(restart=True) + + def go_to(self, *, time: datetime | float): + time = ( + time.astimezone(tz=UTC) + if isinstance(time, datetime) + else datetime.fromtimestamp(time, tz=UTC) + ) + time = int(time.timestamp()) + steps = time - self.cursor.time + steps = steps // self.speed + if 0 <= steps < (len(self.range) - 1): + self.fast_forward(steps=steps) + return + raise ValueError("Can't go back in time or beyond the limits of the range") + + def fast_forward(self, *, steps: int): + for _ in range(steps): + self.next() + + @staticmethod + def get_dtype(*, df: DataFrame) -> list[tuple[str, str]]: + return [(c, t) for c, t in zip(df.columns, df.dtypes)] + + async def tracker(self): + try: + pos_tasks = [ + self.check_position(ticket=ticket) + for ticket in self.positions._open_positions + ] + await asyncio.gather(*pos_tasks) + profit = sum(pos.profit for pos in self.positions.open_positions) + self.update_account(profit=profit) + self.check_account() + except Exception as exe: + logger.critical("Error in tracker: %s at %d", exe, self.cursor.time) + + @error_handler_sync + def save_result_to_json(self): + data = self._account.get_dict( + include={ + "balance", + "profit", + "equity", + "margin", + "margin_free", + "margin_level", + } + ) + wins = [ + position + for ticket in self.positions + if (position := self.positions.get(ticket)).profit > 0 + ] + losses = [ + position + for ticket in self.positions + if (position := self.positions.get(ticket)).profit <= 0 + ] + win = round( + sum(position.profit for position in wins), self._account.currency_digits + ) + loss = round( + sum(position.profit for position in losses), self._account.currency_digits + ) + profit_factor = round(abs(win / loss), 2) if loss != 0 else 0 + wins, losses, total = len(wins), len(losses), len(self.positions._data) + win_percentage = round(wins / total * 100, 2) if total > 0 else 0 + net_profit = round(win - abs(loss), self._account.currency_digits) + profitability = ( + net_profit / (self._account.balance - net_profit) * 100 + if net_profit != 0 + else 0 + ) + profitability = round(profitability, 2) + data.update( + { + "wins": wins, + "losses": losses, + "total": total, + "win_percentage": win_percentage, + "win": win, + "loss": loss, + "net_profit": net_profit, + "profit_factor": profit_factor, + "profitability": profitability, + } + ) + path = Path(self.config.backtest_dir / f"{self.name}.json") + with path.open("w") as file: + json.dump(data, file, indent=4) + + async def close_all_open(self): + tasks = [ + self.check_position(ticket=position.ticket) + for position in self.positions.open_positions + ] + await asyncio.gather(*tasks) + for position in self.positions.open_positions: + await self.close_position_manually(ticket=position.ticket) + + @error_handler + async def wrap_up(self): + if self.close_open_positions_on_exit: + await self.close_all_open() + self.save_result_to_json() + self._data.deals = self.deals.to_dict() + self._data.orders = self.orders.to_dict() + self._data.positions = self.positions.to_dict() + self._data.open_positions = self.positions._open_positions + self._data.margins = self.positions.margins + self._data.account = self._account.asdict() + self._data.cursor = self.cursor + self._data.span = self.span + self._data.range = self.range + self._data.account = self._account.asdict() + name = self._data.name or self.name + self._data.name = name + path = self.config.backtest_dir / f"{name}.pkl" + GetData.pickle_data(data=self._data, name=path) + + async def preload_ticks(self, *, symbol: str): + """Pull a month data on ticks from the terminal. Starting from the current time""" + try: + start = self.cursor.time + end = start + (30 * 24 * 60 * 60) + end = end if end < self.span.stop else self.span.stop + span = range(start, end) + start = datetime.fromtimestamp(start, tz=UTC) + end = datetime.fromtimestamp(end, tz=UTC) + ticks = await self.mt5.copy_ticks_range(symbol, start, end, CopyTicks.ALL) + ticks = pd.DataFrame(ticks) + ticks.drop_duplicates(subset=["time"], keep="last", inplace=True) + ticks.set_index("time", inplace=True, drop=False) + ticks = ticks.reindex(span, method="nearest", copy=True) + self.preloaded_ticks[symbol] = ticks + except Exception as exe: + logger.error(f"Error Preloading Ticks: {exe}") + + @async_cache + async def get_price_tick(self, *, symbol: str, time: int) -> Tick | None: + try: + if self.use_terminal and self.preload: + if ( + ticks := self.preloaded_ticks.get(symbol) + ) is not None and time in ticks.index: + return Tick(ticks.loc[time]) + await self.preload_ticks(symbol=symbol) + tick = self.preloaded_ticks[symbol].loc[time] + return Tick(tick) + + elif self.use_terminal and self.preload is False: + time = datetime.fromtimestamp(time, tz=UTC) + tick = await self.mt5.copy_ticks_from(symbol, time, 1, CopyTicks.ALL) + return Tick(tick[-1]) if tick is not None else None + else: + tick = self.prices[symbol].loc[time] + return Tick(tick) + except Exception as exe: + logger.error("Error Getting Price Tick: %s", exe) + + @error_handler + async def check_order(self, *, ticket: int): + """ " + Check if the order has reached its take profit or stop loss levels and close the order if it has. + Checks only **OrderType.BUY** and **OrderType.SELL** orders that have reached their take profit or stop loss levels. + + Args: + ticket (int): Order ticket + """ + order = self.orders[ticket] + order_type, symbol = order.type, order.symbol + tick = await self.get_price_tick(symbol=symbol, time=self.cursor.time) + tp, sl = order.tp, order.sl + + if not (tp and sl): + return + + deal = { + "ticket": random.randint(100_000_000, 199_999_999), + "order": ticket, + "symbol": symbol, + "commission": 0, + "swap": 0, + "position_id": ticket, + "fee": 0, + "time": self.cursor.time, + "time_msc": self.cursor.time * 1000, + "price": tick.bid, + "type": DealType(order_type), + "reason": DealReason.EXPERT, + "entry": DealEntry.OUT, + "profit": 0, + } + + match order_type: + case OrderType.BUY: + if tick.bid >= tp or tick.bid <= sl: + res = await self.close_position(ticket=ticket) + if res: + pos = self.positions.get(ticket) + deal.update({"profit": pos.profit, "volume": pos.volume}) + self.deals[deal["ticket"]] = TradeDeal( + (deal.get(k, 0) for k in TradeDeal.__match_args__) + ) + + case OrderType.SELL: + if tick.ask <= tp or tick.ask >= sl: + res = await self.close_position(ticket=ticket) + if res: + pos = self.positions.get(ticket) + deal.update({"profit": pos.profit, "volume": pos.volume}) + self.deals[deal["ticket"]] = TradeDeal( + (deal.get(k, 0) for k in TradeDeal.__match_args__) + ) + case _: + ... + + def check_account(self): + account = self._account + level = ( + account.margin_level + if account.margin_so_mode == AccountStopOutMode.PERCENT + else account.margin_so_call + ) + if level < account.margin_so_call and level != 0 and account.equity < 0: + logger.critical( + "Account has burned out!!! Please top up to continue trading" + ) + self.stop_testing = True + + async def check_position(self, *, ticket: int): + """ + Update the profit of an open position based on the current price of the symbol. + + Args: + ticket (int): Position ticket + """ + pos = self.positions[ticket] + order_type, symbol, volume, price_open, prev_profit = ( + pos.type, + pos.symbol, + pos.volume, + pos.price_open, + pos.profit, + ) + tick = await self.get_price_tick(symbol=symbol, time=self.cursor.time) + price_current = tick.bid if order_type == OrderType.BUY else tick.ask + profit = await self.order_calc_profit( + action=order_type, + symbol=symbol, + volume=volume, + price_open=price_open, + price_close=price_current, + ) + kwargs = dict(price_current=price_current, time_update=self.cursor.time) + kwargs.update(profit=profit) if profit is not None else ... + self.positions.update(ticket=pos.ticket, **kwargs) + await self.check_order(ticket=ticket) + + @error_handler_sync + async def close_position_manually(self, *, ticket: int): + res = await self.close_position(ticket=ticket) + if not res: + return + position = self.positions.get(ticket) + order_ticket = random.randint(800_000_000, 899_999_999) + deal_ticket = random.randint(100_000_000, 199_999_999) + time = position.time_update + time_msc = position.time_update_msc + order_type = ( + OrderType.BUY if position.type == OrderType.SELL else OrderType.SELL + ) + order = { + "position_id": position.ticket, + "ticket": order_ticket, + "comment": "", + "external_id": "", + "time_setup": time, + "time_setup_msc": time_msc, + "time_done": time, + "time_done_msc": time_msc, + "type": order_type, + "symbol": position.symbol, + "sl": position.sl, + "tp": position.tp, + "price_current": position.price_current, + "reason": OrderReason.EXPERT, + "volume_initial": position.volume, + } + + # TODO: calculate commission and swap if possible or necessary + deal = { + "ticket": deal_ticket, + "position_id": position.ticket, + "order": order_ticket, + "symbol": position.symbol, + "time": time, + "profit": position.profit, + "time_msc": time_msc, + "volume": position.volume, + "price": position.price_current, + "type": DealType(order_type), + "reason": DealReason.EXPERT, + "entry": DealEntry.OUT, + "comment": "", + "external_id": "", + } + order = TradeOrder((order.get(k, 0) for k in TradeOrder.__match_args__)) + deal = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) + self.orders[order.ticket] = order + self.deals[deal.ticket] = deal + + async def close_position(self, *, ticket: int) -> bool: + """ + Close an open position for the trading account using the position ticket. + + Args: + ticket: Position ticket + + Returns: + bool: True if the position is closed successfully, False otherwise + """ + try: + position = self.positions[ticket] + margin = self.positions.get_margin(ticket=ticket) + self.positions.delete_margin(ticket=ticket) + self.positions.close(ticket=ticket) + self.orders.update( + ticket=ticket, + time_done=self.cursor.time, + time_done_msc=self.cursor.time * 1000, + ) + profit = sum( + [position.profit for position in self.positions.open_positions] + ) + profit = round(profit, self._account.currency_digits) + self.update_account(gain=position.profit, profit=profit, margin=-margin) + return True + except Exception as exe: + logger.error("Error Closing Position %d: %s", ticket, exe) + return False + + @error_handler(response=False) + def modify_stops(self, *, ticket: int, sl: int, tp: int) -> bool: + """ + Modify the stop loss and take profit levels of an open position. + + Args: + ticket: Position ticket + sl: stop loss level + tp: Take profit level + + Returns: + bool: True if the stops are modified successfully, False otherwise + """ + self.positions.update( + ticket=ticket, + sl=sl, + tp=tp, + time_update=self.cursor.time, + time_update_msc=self.cursor.time * 1000, + ) + return True + + def update_account( + self, *, profit: float = None, margin: float = 0, gain: float = 0 + ): + self.account_lock.acquire() + try: + self._account.balance += round(gain, self._account.currency_digits) + self._account.profit = ( + round(profit, self._account.currency_digits) + if profit is not None + else self._account.profit + ) + self._account.equity = self._account.balance + self._account.profit + self._account.margin += round(margin, self._account.currency_digits) + self._account.margin_free = round( + self._account.equity - self._account.margin, + self._account.currency_digits, + ) + self._account.balance = round( + self._account.balance, self._account.currency_digits + ) + self._account.equity = round( + self._account.equity, self._account.currency_digits + ) + self._account.margin = round( + self._account.margin, self._account.currency_digits + ) + self._account.margin_free = round( + self._account.margin_free, self._account.currency_digits + ) + self._account.profit = round( + self._account.profit, self._account.currency_digits + ) + + if self._account.margin == 0: + self._account.margin_level = 0 + else: + mode = self._account.margin_so_mode + level = self._account.equity / self._account.margin * 100 + self._account.margin_level = ( + level + if mode == AccountStopOutMode.PERCENT + else self._account.margin_free + ) + except Exception as exe: + logger.critical("Error Updating Account: %s", exe) + + finally: + self.account_lock.release() + + def deposit(self, *, amount: float): + self.update_account(gain=amount) + + def withdraw(self, *, amount: float): + assert amount <= self._account.balance, "Insufficient funds" + self.update_account(gain=-amount) + + @error_handler + async def setup_account(self, **kwargs): + default = { + "profit": self._account.profit, + "margin": self._account.margin, + "equity": self._account.equity, + "margin_free": self._account.margin_free, + "margin_level": self._account.margin_level, + "balance": self._account.balance, + **{k: v for k, v in kwargs.items() if k in self._account.__match_args__}, + } + + if self.use_terminal: + acc_info = await self.mt5.account_info() + default = {**acc_info._asdict(), **default} + + self._account.set_attrs(**default) + self.update_account() + + @cached_property + def prices(self) -> dict[str, DataFrame]: + prices = {} + for symbol in self._data.ticks.keys(): + res = self._data.ticks[symbol] + res = pd.DataFrame(res) + res.drop_duplicates(subset=["time"], keep="last", inplace=True) + res.set_index("time", inplace=True, drop=False) + res = res.reindex( + self.span, copy=True, method="nearest" + ) # fill in missing values with NaN + prices[symbol] = res + return prices + + @cached_property + def ticks(self) -> dict[str, DataFrame]: + ticks = {} + for symbol in self._data.ticks.keys(): + res = self._data.ticks[symbol] + res = pd.DataFrame(res) + ticks[symbol] = res + return ticks + + @cached_property + def rates(self) -> dict[str, dict[int, DataFrame]]: + rates = {} + for symbol in self._data.rates.keys(): + for timeframe in self._data.rates[symbol].keys(): + res = self._data.rates[symbol][timeframe] + res = pd.DataFrame(res) + rates.setdefault(symbol, {})[timeframe] = res + return rates + + @cached_property + def symbols(self) -> dict[str, SymbolInfo]: + symbols = {} + for symbol, info in self._data.symbols.items(): + symbols[symbol] = SymbolInfo( + (info.get(key) for key in SymbolInfo.__match_args__) + ) + return symbols + + @error_handler + async def order_send(self, *, request: dict, use_terminal=False) -> OrderSendResult: + use_terminal = self.use_terminal or use_terminal + osr = { + "retcode": 10013, + "comment": "Invalid request", + "request": TradeRequest( + request.get(k, (0 if k != "comment" else "")) + for k in TradeRequest.__match_args__ + ), + } + current_tick = await self.get_price_tick( + symbol=request.get("symbol"), time=self.cursor.time + ) + if current_tick is None: + osr["comment"] = "Market is closed" + osr["retcode"] = 10018 + return OrderSendResult( + (osr.get(k, 0) for k in OrderSendResult.__match_args__) + ) + + trade_order = { + "external_id": "", + "comment": "", + **{k: v for k, v in request.items() if k in TradeOrder.__match_args__}, + } + order_type, symbol = request.get("type"), request.get("symbol", "") + action, position_id = request.get("action"), request.get("position") + sl, tp, volume, symbol = ( + request.get("sl"), + request.get("tp"), + request.get("volume"), + request.get("symbol"), + ) + order_type = OrderType(order_type) + current_position = self.positions.get(position_id) + order_ticket = random.randint(800_000_000, 899_999_999) + deal_ticket = random.randint(100_000_000, 199_999_999) + + # closing an order by an opposite order using a position ticket and Deal action + if ( + action == TradeAction.DEAL + and current_position + and order_type.opposite == current_position.type + ): + res = await self.close_position(ticket=current_position.ticket) + if res: + price_current = ( + current_tick.ask + if order_type == OrderType.BUY + else current_tick.bid + ) + trade_order.update( + { + "position_id": current_position.ticket, + "ticket": order_ticket, + "time_setup": current_tick.time, + "time_setup_msc": current_tick.time_msc, + "time_done": current_tick.time, + "time_done_msc": current_tick.time_msc, + "type": order_type, + "symbol": symbol, + "sl": current_position.sl, + "tp": current_position.tp, + "price_current": price_current, + "reason": OrderReason.EXPERT, + "volume_initial": current_position.volume, + } + ) + + # TODO: calculate commission and swap if possible or necessary + deal = { + "ticket": deal_ticket, + "position_id": current_position.ticket, + "order": order_ticket, + "symbol": symbol, + "time": current_tick.time, + "profit": current_position.profit, + "time_msc": current_tick.time_msc, + "volume": current_position.volume, + "price": price_current, + "type": DealType(order_type), + "reason": DealReason.EXPERT, + "entry": DealEntry.OUT, + "comment": "", + "external_id": "", + } + + order = TradeOrder( + (trade_order.get(k, 0) for k in TradeOrder.__match_args__) + ) + self.orders[order.ticket] = order + deal = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) + self.deals[deal.ticket] = deal + osr.update( + { + "comment": "Request completed", + "retcode": 10009, + "order": order_ticket, + "deal": deal_ticket, + } + ) + return OrderSendResult( + (osr.get(k, 0) for k in OrderSendResult.__match_args__) + ) + + if action == TradeAction.SLTP and current_position: + check = await self.order_check(request=request, use_terminal=use_terminal) + if check.retcode != 0: + osr = { + "retcode": check.retcode, + "comment": check.comment, + "request": check.request, + } + return OrderSendResult( + (osr.get(k, 0) for k in OrderSendResult.__match_args__) + ) + res = self.modify_stops(ticket=position_id, sl=sl, tp=tp) + if res: + osr.update( + { + "comment": "Request completed", + "retcode": 10009, + "order": order_ticket, + "deal": deal_ticket, + } + ) + return OrderSendResult( + (osr.get(k, 0) for k in OrderSendResult.__match_args__) + ) + + if action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL): + check = await self.order_check(request=request, use_terminal=use_terminal) + if check.retcode != 0: + osr = { + "retcode": check.retcode, + "comment": check.comment, + "request": check.request, + } + return OrderSendResult( + (osr.get(k, 0) for k in OrderSendResult.__match_args__) + ) + + price = ( + current_tick.ask if order_type == OrderType.BUY else current_tick.bid + ) + position = { + "ticket": order_ticket, + "symbol": symbol, + "volume": volume, + "price_open": price, + "price_current": price, + "type": order_type, + "profit": 0, + "reason": PositionReason.EXPERT, + "identifier": order_ticket, + "sl": sl, + "tp": tp, + "time": current_tick.time, + "time_msc": current_tick.time_msc, + "time_update": current_tick.time, + "time_update_msc": current_tick.time_msc, + } + + deal = { + "ticket": deal_ticket, + "order": order_ticket, + "symbol": symbol, + "commission": 0, + "swap": 0, + "position_id": order_ticket, + "fee": 0, + "time": current_tick.time, + "time_msc": current_tick.time_msc, + "volume": volume, + "price": price, + "type": DealType(order_type), + "reason": DealReason.EXPERT, + "entry": DealEntry.IN, + "profit": 0, + } + + # ToDo: set time_expiration based on order_type_time + trade_order.update( + { + "ticket": order_ticket, + "symbol": symbol, + "volume": volume, + "price": price, + "price_current": price, + "sl": sl, + "time_setup_msc": current_tick.time_msc, + "tp": tp, + "price_open": price, + "type": order_type, + "time_setup": current_tick.time, + "volume_current": volume, + "volume_initial": volume, + "position_id": order_ticket, + } + ) + + pos = TradePosition( + (position.get(k, 0) for k in TradePosition.__match_args__) + ) + order = TradeOrder( + (trade_order.get(k, 0) for k in TradeOrder.__match_args__) + ) + deal = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) + self.deals[deal_ticket] = deal + self.positions[order.ticket] = pos + self.orders[order.ticket] = order + osr.update( + { + "comment": "Request completed", + "retcode": 10009, + "order": order_ticket, + "price": price, + "volume": volume, + "bid": current_tick.bid, + "ask": current_tick.ask, + "deal": deal_ticket, + } + ) + margin = await self.order_calc_margin( + action=action, + symbol=symbol, + volume=volume, + price=price, + use_terminal=use_terminal, + ) + self.positions.set_margin(ticket=order_ticket, margin=margin) + self.update_account(margin=margin) + return OrderSendResult( + (osr.get(k, 0) for k in OrderSendResult.__match_args__) + ) + + @error_handler + async def order_check( + self, *, request: dict, use_terminal: bool = False + ) -> OrderCheckResult: + use_terminal = self.use_terminal or use_terminal + ocr = { + "retcode": 10013, + "balance": 0, + "profit": 0, + "margin": 0, + "equity": 0, + "margin_free": 0, + "margin_level": 0, + "comment": "Invalid request", + "request": TradeRequest( + request.get(k, (0 if k != "comment" else "")) + for k in TradeRequest.__match_args__ + ), + } + + action, symbol, volume = ( + request.get("action"), + request.get("symbol"), + request.get("volume"), + ) + price, order_type, position_id = ( + request.get("price"), + request.get("type"), + request.get("position"), + ) + + # check margin and confirm order can go through for a deal action and buy or sell order type + if ( + action == TradeAction.DEAL + and order_type in (OrderType.BUY, OrderType.SELL) + and position_id is None + ): + margin = await self.order_calc_margin( + action=action, + symbol=symbol, + volume=volume, + price=price, + use_terminal=use_terminal, + ) + if margin is None: + return OrderCheckResult( + (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) + ) + + used_margin = self._account.margin + margin + free_margin = self._account.margin_free - margin + + level = ( + self._account.equity / used_margin * 100 + if used_margin + else float("inf") + ) + margin_level = ( + level + if self._account.margin_so_mode == AccountStopOutMode.PERCENT + else free_margin + ) + ocr.update( + { + "margin_level": margin_level, + "margin": margin, + "margin_free": free_margin, + } + ) + + # check if the account has enough money + if margin_level < self._account.margin_so_call: + ocr["retcode"] = 10019 + ocr["comment"] = "No money" + return OrderCheckResult( + (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) + ) + + # check if the stops level is valid + sym = await self.get_symbol_info(symbol=symbol) + sl, tp = request.get("sl"), request.get("tp") + current_price = price + if tp and sl: + if action == TradeAction.SLTP: + pos = self.positions.get(request.get("position")) + sym = await self.get_symbol_info(symbol=pos.symbol) + current_tick = sym or await self.get_price_tick( + pos.symbol, self.cursor.time + ) + current_price = ( + current_tick.bid if pos.type == OrderType.BUY else current_tick.ask + ) + + min_sl = min(sl, tp) + dsl = abs(current_price - min_sl) / sym.point + tsl = sym.trade_stops_level + sym.spread + if dsl < tsl: + ocr["retcode"] = 10016 + ocr["comment"] = "Invalid stops" + return OrderCheckResult( + (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) + ) + + elif action == TradeAction.SLTP: + ocr["comment"] = "Done" + ocr["retcode"] = 0 + return OrderCheckResult( + (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) + ) + + if use_terminal or self.use_terminal: + ocr_t = await self.mt5.order_check(request) + if ocr_t.retcode in (10013, 10014): + return ocr_t + elif action == TradeAction.DEAL and order_type in ( + OrderType.BUY, + OrderType.SELL, + ): + # check volume + if volume < sym.volume_min or volume > sym.volume_max: + ocr["retcode"] = 10014 + ocr["comment"] = "Invalid volume" + return OrderCheckResult( + (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) + ) + + ocr.update( + { + "balance": self._account.balance, + "profit": self._account.profit, + "equity": self._account.equity, + "comment": "Done", + "retcode": 0, + } + ) + + return OrderCheckResult( + (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) + ) + + @error_handler + async def get_terminal_info(self) -> TerminalInfo: + if self.use_terminal: + res = await self.mt5.terminal_info() + return res + return TerminalInfo(self._data.terminal) + + @error_handler + async def get_version(self) -> tuple[int, int, str]: + if self.use_terminal: + res = await self.mt5.version() + return res + return self._data.version + + @error_handler + async def get_symbols_total(self) -> int: + if self.use_terminal: + syms = await self.mt5.symbols_total() + return syms + return len(self.symbols) + + @error_handler + async def get_symbols(self, *, group: str = "") -> tuple[SymbolInfo, ...]: + if self.use_terminal: + syms = await self.mt5.symbols_get(group=group) + return syms + return tuple(list(self.symbols.values())) + + @error_handler_sync + def get_account_info(self) -> AccountInfo: + return AccountInfo(self._account.asdict().values()) + + @error_handler + async def get_symbol_info_tick(self, *, symbol: str) -> Tick | None: + tick = await self.get_price_tick(symbol=symbol, time=self.cursor.time) + return tick + + @async_cache + async def _symbol_info(self, *, symbol: str) -> SymbolInfo: + if self.use_terminal: + info = await self.mt5.symbol_info(symbol) + else: + info = self.symbols[symbol] + return info + + @error_handler + async def get_symbol_info(self, *, symbol: str) -> SymbolInfo: + if self.use_terminal: + info = await self._symbol_info(symbol=symbol) + else: + info = self.symbols[symbol] + tick = await self.get_symbol_info_tick(symbol=symbol) + + info = info._asdict() | { + "bid": tick.bid, + "bidhigh": tick.bid, + "bidlow": tick.bid, + "ask": tick.ask, + "askhigh": tick.ask, + "asklow": tick.bid, + "last": tick.last, + "volume_real": tick.volume_real, + } + return SymbolInfo((info.get(key) for key in SymbolInfo.__match_args__)) + + @error_handler + async def get_rates_from( + self, + *, + symbol: str, + timeframe: TimeFrame, + date_from: datetime | float, + count: int, + ) -> np.ndarray: + date_from = ( + date_from.astimezone(tz=UTC) + if isinstance(date_from, datetime) + else datetime.fromtimestamp(date_from, tz=UTC) + ) + if self.use_terminal: + rates = await self.mt5.copy_rates_from(symbol, timeframe, date_from, count) + return rates + + rates = self.rates[symbol][timeframe] + start = int(date_from.timestamp()) + start = round_down(start, timeframe.seconds) + rates = rates[rates.time <= start].iloc[-count:] + return np.fromiter( + (tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates) + ) + + @error_handler + async def get_rates_from_pos( + self, *, symbol: str, timeframe: TimeFrame, start_pos: int, count: int + ) -> np.ndarray: + if self.use_terminal: + current_time = ( + self.cursor.time + if start_pos == 0 + else self.cursor.time - start_pos * timeframe.seconds + ) + current_time = round_up(current_time, timeframe.seconds) + start = datetime.fromtimestamp(current_time, tz=UTC) + rates = await self.mt5.copy_rates_from(symbol, timeframe, start, count) + return rates + + rates = self.rates[symbol][timeframe] + + # the current time rounded up to a multiple of the timeframe in seconds and then subtracted by the start_pos + # multiplied by the timeframe in seconds gives the time of the last candlestick in the range when using + # copy_rates_from_pos + end = ( + int(round_down(self.cursor.time, timeframe.seconds)) + - start_pos * timeframe.seconds + ) + start = end - count * timeframe.seconds + rates = rates[(rates.time > start) & (rates.time <= end)] + return np.fromiter( + (tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates) + ) + + @error_handler + async def get_rates_range( + self, + *, + symbol: str, + timeframe: TimeFrame, + date_from: datetime | float, + date_to: datetime | float, + ) -> np.ndarray: + date_from = ( + date_from.astimezone(tz=UTC) + if isinstance(date_from, datetime) + else datetime.fromtimestamp(date_from, tz=UTC) + ) + date_to = ( + date_to.astimezone(tz=UTC) + if isinstance(date_to, datetime) + else datetime.fromtimestamp(date_to, tz=UTC) + ) + if self.use_terminal: + rates = await self.mt5.copy_rates_range( + symbol, timeframe, date_from, date_to + ) + return rates + + rates = self.rates[symbol][timeframe] + start = round_up(int(date_from.timestamp()), timeframe.seconds) + end = round_up(int(date_to.timestamp()), timeframe.seconds) + rates = rates[(rates.time >= start) & (rates.time <= end)] + return np.fromiter( + (tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates) + ) + + @error_handler + async def get_ticks_from( + self, + *, + symbol: str, + date_from: datetime | float, + count: int, + flags: CopyTicks = CopyTicks.ALL, + ) -> np.ndarray: + date_from = ( + date_from.astimezone(tz=UTC) + if isinstance(date_from, datetime) + else datetime.fromtimestamp(date_from, tz=UTC) + ) + if self.use_terminal: + ticks = await self.mt5.copy_ticks_from(symbol, date_from, count, flags) + return ticks + + ticks = self.ticks[symbol] + start = int(date_from.timestamp()) + rates = ticks[ticks.time <= start].iloc[-count:] + return np.fromiter( + (tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates) + ) + + @error_handler + async def get_ticks_range( + self, + *, + symbol: str, + date_from: datetime | float, + date_to: datetime | float, + flags: CopyTicks = CopyTicks.ALL, + ) -> np.ndarray: + date_from = ( + date_from.astimezone(tz=UTC) + if isinstance(date_from, datetime) + else datetime.fromtimestamp(date_from, tz=UTC) + ) + date_to = ( + date_to.astimezone(tz=UTC) + if isinstance(date_to, datetime) + else datetime.fromtimestamp(date_to, tz=UTC) + ) + if self.use_terminal: + ticks = await self.mt5.copy_ticks_range(symbol, date_from, date_to, flags) + return ticks + + ticks = self.ticks[symbol] + start = int(date_from.timestamp()) + end = int(date_to.timestamp()) + rates = ticks[(ticks.time >= start) & (ticks.time <= end)] + return np.fromiter( + (tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=ticks) + ) + + @error_handler + async def order_calc_margin( + self, + *, + action: Literal[OrderType.BUY, OrderType.SELL], + symbol: str, + volume: float, + price: float, + use_terminal: bool = None, + ): + use_terminal = use_terminal if use_terminal is not None else self.use_terminal + if use_terminal: + return await self.mt5.order_calc_margin(action, symbol, volume, price) + + sym = self.symbols.get(symbol) + if sym is None and self.use_terminal: + sym = await self._symbol_info(symbol=symbol) + margin = (volume * sym.trade_contract_size * price) / ( + self._account.leverage / (sym.margin_initial or 1) + ) + return round(margin, self._account.currency_digits) + + @error_handler + async def order_calc_profit( + self, + *, + action: Literal[OrderType.BUY, OrderType.SELL], + symbol: str, + volume: float, + price_open: float, + price_close: float, + use_terminal=None, + ): + use_terminal = use_terminal if use_terminal is not None else self.use_terminal + + if use_terminal: + return await self.mt5.order_calc_profit( + action, symbol, volume, price_open, price_close + ) + + sym = self.symbols.get(symbol) + if sym is None and self.use_terminal: + sym = await self._symbol_info(symbol=symbol) + profit = ( + volume + * sym.trade_contract_size + * ( + (price_close - price_open) + if action == OrderType.BUY + else (price_open - price_close) + ) + ) + return round(profit, self._account.currency_digits) + + @error_handler_sync + def get_orders_total(self) -> int: + """ + Get the total number of pending orders. + + Returns: + int: Total number of pending orders + """ + return 0 + + @error_handler_sync + def get_orders( + self, *, symbol: str = "", group: str = "", ticket: int = None + ) -> tuple[TradeOrder, ...]: + """ + Get pending orders from the terminal history. This has to do with pending orders, which this backtester + doesn't support yet. + + Args: + symbol: Symbol name + group: Group name + ticket: Order ticket + + Returns: + tuple[TradeOrder] + """ + if symbol and group and ticket: + return tuple() + return () + + @error_handler_sync + def get_positions_total(self) -> int: + """ + Get the total number of open positions. + + Returns: + int: Total number of open positions + """ + return self.positions.positions_total() + + @error_handler_sync + def get_positions( + self, *, symbol: str = None, group: str = None, ticket: int = None + ) -> tuple[TradePosition, ...]: + """ + Get open positions from the terminal history. + + Keyword Args: + symbol: The symbol name + group: Group argument to filter by + ticket: Position ticket + + Returns: + tuple[TradePosition]: Open positions + """ + return self.positions.positions_get(ticket=ticket, symbol=symbol, group=group) + + @error_handler_sync + def get_history_orders_total( + self, *, date_from: datetime | float, date_to: datetime | float + ) -> int: + """ + Get the total number of orders in the terminal history. + + Args: + date_from: The start date of the history + + date_to: The end date of the history + + Returns: + int: Total number of orders in the history + """ + return self.orders.history_orders_total(date_from=date_from, date_to=date_to) + + @error_handler_sync + def get_history_orders( + self, + *, + date_from: datetime | float = None, + date_to: datetime | float = None, + group: str = "", + ticket: int = None, + position: int = None, + ) -> tuple[TradeOrder, ...]: + """ + Get orders from the terminal history. + + Keyword Args: + date_from: Date from which to start the history + date_to: Date to which to end the history + group: group keyword to filter by + ticket: ticket id to filter by + position: position id to filter by + + Returns: + tuple[TradeOrder]: Orders in the history + """ + return self.orders.history_orders_get( + date_from=date_from, + date_to=date_to, + group=group, + ticket=ticket, + position=position, + ) + + @error_handler_sync + def get_history_deals_total( + self, *, date_from: datetime | float, date_to: datetime | float + ) -> int: + """ + Get the total number of deals in the terminal history. + + Args: + date_from: Date from which to start the history + date_to: Date to which to end the history + + Returns: + int: Total number of deals in the history + """ + return self.deals.history_deals_total(date_from=date_from, date_to=date_to) + + @error_handler_sync + def get_history_deals( + self, + *, + date_from: datetime | float = None, + date_to: datetime | float = None, + group: str = None, + position: int = None, + ticket: int = None, + ) -> tuple[TradeDeal, ...]: + """ + Get deals from the terminal history. + + Keyword Args: + date_from: Date from which to start the history + date_to: Date to which to end the history + group: group keyword to filter by + position: position id to filter by + ticket: ticket id to filter by + + Returns: + tuple[TradeDeal, ...]: Deals in the history + """ + return self.deals.history_deals_get( + date_from=date_from, + date_to=date_to, + group=group, + position=position, + ticket=ticket, + ) diff --git a/src/aiomql/contrib/backtesting/get_data.py b/src/aiomql/core/backtesting/get_data.py similarity index 67% rename from src/aiomql/contrib/backtesting/get_data.py rename to src/aiomql/core/backtesting/get_data.py index be8a473..e2cdf8b 100644 --- a/src/aiomql/contrib/backtesting/get_data.py +++ b/src/aiomql/core/backtesting/get_data.py @@ -8,10 +8,10 @@ from typing import Sequence, NamedTuple import MetaTrader5 from numpy import ndarray -from ...core.meta_trader import MetaTrader -from ...core.config import Config -from ...core.constants import TimeFrame -from ...core.task_queue import TaskQueue, QueueItem +from ..meta_trader import MetaTrader +from ..config import Config +from ..constants import TimeFrame +from ..task_queue import TaskQueue, QueueItem from ..._utils import backoff_decorator logger = getLogger(__name__) @@ -24,9 +24,9 @@ class Cursor(NamedTuple): @dataclass class BackTestData: - name: str = '' + name: str = "" terminal: dict[str, [str | int | bool | float]] = field(default_factory=dict) - version: tuple[int, int, str] = (0, 0, '') + version: tuple[int, int, str] = (0, 0, "") account: dict = field(default_factory=dict) symbols: dict[str, dict] = field(default_factory=dict) ticks: dict[str, ndarray] = field(default_factory=dict) @@ -58,8 +58,15 @@ class BackTestData: class GetData: data: BackTestData - def __init__(self, *, start: datetime, end: datetime, symbols: Sequence[str], - timeframes: Sequence[TimeFrame], name: str = ''): + def __init__( + self, + *, + start: datetime, + end: datetime, + symbols: Sequence[str], + timeframes: Sequence[TimeFrame], + name: str = "", + ): """""" self.config = Config() self.start = start.astimezone(tz=UTC) @@ -73,13 +80,13 @@ class GetData: self.span = range(span_start, span_end) self.data = BackTestData(name=self.name, span=self.span, range=self.range) self.mt5 = MetaTrader() - self.task_queue = TaskQueue(workers=500, mode='finite', on_exit='cancel') + self.task_queue = TaskQueue(workers=500, mode="finite", on_exit="cancel") @classmethod def pickle_data(cls, *, data: BackTestData, name: str | Path): """""" try: - with open(name, 'wb') as fo: + with open(name, "wb") as fo: pickle.dump(data, fo, protocol=pickle.HIGHEST_PROTOCOL) except Exception as err: logger.error(f"Error in dump_data: {err}") @@ -88,16 +95,22 @@ class GetData: def load_data(cls, *, name: str | Path) -> BackTestData: """""" try: - with open(name, 'rb') as fo: + with open(name, "rb") as fo: data = pickle.load(fo) return data except Exception as err: logger.error(f"Error: {err}") - def save_data(self, *, name: str | Path = ''): - name = name or (self.name + '.pkl' if not self.name.endswith('.pkl') else self.name) - name = Path(self.config.backtest_dir) / name if not isinstance(name, Path) else name - with open(name, 'wb') as fo: + def save_data(self, *, name: str | Path = ""): + name = name or ( + self.name + ".pkl" if not self.name.endswith(".pkl") else self.name + ) + name = ( + Path(self.config.backtest_dir) / name + if not isinstance(name, Path) + else name + ) + with open(name, "wb") as fo: pickle.dump(self.data, fo, protocol=pickle.HIGHEST_PROTOCOL) async def get_data(self, workers: int = None): @@ -105,18 +118,26 @@ class GetData: if workers: self.task_queue.workers = workers - q_items = [QueueItem(self.get_symbols_rates), - QueueItem(self.get_symbols_ticks), - QueueItem(self.get_symbols_info), - ] + q_items = [ + QueueItem(self.get_symbols_rates), + QueueItem(self.get_symbols_ticks), + QueueItem(self.get_symbols_info), + ] - [self.task_queue.add(item=item, priority=0, must_complete=True) for item in q_items] + [ + self.task_queue.add(item=item, priority=0, must_complete=True) + for item in q_items + ] if not self.data.account: - self.task_queue.add(item=QueueItem(self.get_account_info), must_complete=True) + self.task_queue.add( + item=QueueItem(self.get_account_info), must_complete=True + ) if not self.data.terminal: - self.task_queue.add(item=QueueItem(self.get_terminal_info), must_complete=True) + self.task_queue.add( + item=QueueItem(self.get_terminal_info), must_complete=True + ) if not self.data.version: self.task_queue.add(item=QueueItem(self.get_version), must_complete=True) @@ -125,7 +146,9 @@ class GetData: if self.data.fully_loaded is False: logger.warning("Data not fully loaded") - self.data = BackTestData(name=self.name, span=self.span, range=self.range, fully_loaded=False) + self.data = BackTestData( + name=self.name, span=self.span, range=self.range, fully_loaded=False + ) async def get_terminal_info(self): """""" @@ -156,19 +179,33 @@ class GetData: async def get_symbols_info(self): """""" - [self.task_queue.add(item=QueueItem(self.get_symbol_info, symbol=symbol)) - for symbol in self.symbols if self.data.symbols.get(symbol) is None] + [ + self.task_queue.add(item=QueueItem(self.get_symbol_info, symbol=symbol)) + for symbol in self.symbols + if self.data.symbols.get(symbol) is None + ] async def get_symbols_ticks(self): """""" - [self.task_queue.add(item=QueueItem(self.get_symbol_ticks, symbol=symbol)) - for symbol in self.symbols if self.data.ticks.get(symbol) is None] + [ + self.task_queue.add(item=QueueItem(self.get_symbol_ticks, symbol=symbol)) + for symbol in self.symbols + if self.data.ticks.get(symbol) is None + ] async def get_symbols_rates(self): """""" - [self.task_queue.add(item=QueueItem(self.get_symbol_rates, symbol=symbol, timeframe=timeframe), priority=4) - for symbol in self.symbols for timeframe in self.timeframes - if self.data.rates.get(symbol, {}).get(timeframe) is None] + [ + self.task_queue.add( + item=QueueItem( + self.get_symbol_rates, symbol=symbol, timeframe=timeframe + ), + priority=4, + ) + for symbol in self.symbols + for timeframe in self.timeframes + if self.data.rates.get(symbol, {}).get(timeframe) is None + ] @backoff_decorator async def get_symbol_info(self, *, symbol: str): @@ -182,7 +219,9 @@ class GetData: @backoff_decorator async def get_symbol_ticks(self, *, symbol: str): """""" - res = await self.mt5.copy_ticks_range(symbol, self.start, self.end, MetaTrader5.COPY_TICKS_ALL) + res = await self.mt5.copy_ticks_range( + symbol, self.start, self.end, MetaTrader5.COPY_TICKS_ALL + ) if res is None: self.data.fully_loaded = False self.task_queue.stop_queue() diff --git a/src/aiomql/contrib/backtesting/trades_manager.py b/src/aiomql/core/backtesting/trades_manager.py similarity index 68% rename from src/aiomql/contrib/backtesting/trades_manager.py rename to src/aiomql/core/backtesting/trades_manager.py index 00b3a7d..27651e2 100644 --- a/src/aiomql/contrib/backtesting/trades_manager.py +++ b/src/aiomql/core/backtesting/trades_manager.py @@ -6,7 +6,7 @@ from MetaTrader5 import TradePosition, TradeOrder, TradeDeal logger = getLogger(__name__) -TradeData = TypeVar('TradeData', bound=TradePosition | TradeOrder | TradeDeal) +TradeData = TypeVar("TradeData", bound=TradePosition | TradeOrder | TradeDeal) class TradeManager(Generic[TradeData]): @@ -42,7 +42,7 @@ class TradeManager(Generic[TradeData]): klass = type(res) res = res._asdict() res.update(**kwargs) - res = klass(res.get(v) for v in klass.__match_args__) + res = klass(res.get(v) for v in klass.__match_args__) self[res.ticket] = res return res except KeyError: @@ -66,11 +66,15 @@ class PositionsManager(TradeManager): _open_positions: set[int] margins: dict[int, float] - def __init__(self, *, data: dict = None, open_positions: set = None, margins: dict = None): + def __init__( + self, *, data: dict = None, open_positions: set = None, margins: dict = None + ): super().__init__(data=data) - self._open_positions = open_positions or {trade.ticket for trade in self._data.values()} + self._open_positions = open_positions or { + trade.ticket for trade in self._data.values() + } self.margins: dict[int, float] = margins or dict() - + def __len__(self): return len(self._open_positions) @@ -80,7 +84,7 @@ class PositionsManager(TradeManager): def __getitem__(self, item): if item in self._open_positions: return super().__getitem__(item) - raise KeyError('Position not found') + raise KeyError("Position not found") def __setitem__(self, key, value: TradeData): self._open_positions.add(value.ticket) @@ -108,13 +112,23 @@ class PositionsManager(TradeManager): def set_margin(self, *, ticket: int, margin: float): self.margins[ticket] = margin - - def positions_get(self, *, ticket: int = None, symbol: str = None, group: None = None) -> tuple[TradePosition, ...]: + + def positions_get( + self, *, ticket: int = None, symbol: str = None, group: None = None + ) -> tuple[TradePosition, ...]: if ticket: - return tuple(position for position in self.open_positions if position.ticket == ticket) + return tuple( + position + for position in self.open_positions + if position.ticket == ticket + ) if symbol: - return tuple(position for position in self.open_positions if position.symbol == symbol) + return tuple( + position + for position in self.open_positions + if position.symbol == symbol + ) if group: return self.open_positions @@ -123,25 +137,40 @@ class PositionsManager(TradeManager): return self.open_positions return tuple() - + def positions_total(self) -> int: return len(self._open_positions) @property def open_positions(self) -> tuple[TradePosition, ...]: - return tuple(position for position in self.values() if position.ticket in self._open_positions) + return tuple( + position + for position in self.values() + if position.ticket in self._open_positions + ) class OrdersManager(TradeManager): _data = dict[int, TradeOrder] - def get_orders_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]: + def get_orders_range( + self, *, date_from: float, date_to: float + ) -> tuple[TradeData, ...]: start = date_from.timestamp() if isinstance(date_from, datetime) else date_from end = date_to.timestamp() if isinstance(date_to, datetime) else date_to - return tuple(order for order in self.values() if start <= order.time_setup <= end) - - def history_orders_get(self, *, date_from: float | datetime = None, date_to: float | datetime = None, - group: str = '', ticket: int = None, position: int = None) -> tuple[TradeOrder, ...]: + return tuple( + order for order in self.values() if start <= order.time_setup <= end + ) + + def history_orders_get( + self, + *, + date_from: float | datetime = None, + date_to: float | datetime = None, + group: str = "", + ticket: int = None, + position: int = None, + ) -> tuple[TradeOrder, ...]: if date_from and date_to: orders = self.get_orders_range(date_from=date_from, date_to=date_to) if group: @@ -152,23 +181,37 @@ class OrdersManager(TradeManager): return tuple(order for order in self.values() if order.ticket == ticket) if position: - return tuple(order for order in self.values() if order.position_id == position) + return tuple( + order for order in self.values() if order.position_id == position + ) return () - def history_orders_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: + def history_orders_total( + self, *, date_from: datetime | float, date_to: datetime | float + ) -> int: return len(self.get_orders_range(date_from=date_from, date_to=date_to)) + class DealsManager(TradeManager): _data = dict[int, TradeDeal] - def get_deals_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]: + def get_deals_range( + self, *, date_from: float, date_to: float + ) -> tuple[TradeData, ...]: start = date_from.timestamp() if isinstance(date_from, datetime) else date_from end = date_to.timestamp() if isinstance(date_to, datetime) else date_to return tuple(deal for deal in self.values() if start <= deal.time <= end) - def history_deals_get(self, *, date_from: float | datetime = None, date_to: float | datetime = None, - group: str = '', ticket: int = None, position: int = None) -> tuple[TradeDeal, ...]: + def history_deals_get( + self, + *, + date_from: float | datetime = None, + date_to: float | datetime = None, + group: str = "", + ticket: int = None, + position: int = None, + ) -> tuple[TradeDeal, ...]: if date_from and date_to: deals = self.get_deals_range(date_from=date_from, date_to=date_to) if group: @@ -183,5 +226,7 @@ class DealsManager(TradeManager): return () - def history_deals_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: + def history_deals_total( + self, *, date_from: datetime | float, date_to: datetime | float + ) -> int: return len(self.get_deals_range(date_from=date_from, date_to=date_to)) diff --git a/src/aiomql/core/base.py b/src/aiomql/core/base.py index bb70a65..042514e 100644 --- a/src/aiomql/core/base.py +++ b/src/aiomql/core/base.py @@ -11,8 +11,9 @@ logger = getLogger(__name__) class Base: """A base class for all data structure classes in the aiomql package. This class provides a set of common methods - and attributes for handling data. + and attributes for handling data. """ + exclude: set include: set @@ -23,16 +24,33 @@ class Base: Args: **kwargs: Set instance attributes with keyword arguments. Only if they are annotated on the class body. """ - self.exclude = {'mt5', "config", 'exclude', 'include', 'annotations', 'class_vars', 'dict', '_instance'} + self.exclude = { + "mt5", + "config", + "exclude", + "include", + "annotations", + "class_vars", + "dict", + "_instance", + } self.include = set() self.set_attributes(**kwargs) def __repr__(self): - kv = [(k, v) for k, v in self.__dict__.items() if not k.startswith('_') and - (type(v) in (int, float, str) or isinstance(v, enum.Enum))] - args = (', '.join('%s=%s' % (i, j) for i, j in kv[:3])) - args = args if len(kv) <= 3 else args + ' ... ' + ', '.join('%s=%s' % (i, j) for i, j in kv[-1:]) - return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': args} + kv = [ + (k, v) + for k, v in self.__dict__.items() + if not k.startswith("_") + and (type(v) in (int, float, str) or isinstance(v, enum.Enum)) + ] + args = ", ".join("%s=%s" % (i, j) for i, j in kv[:3]) + args = ( + args + if len(kv) <= 3 + else args + " ... " + ", ".join("%s=%s" % (i, j) for i, j in kv[-1:]) + ) + return "%(class)s(%(args)s)" % {"class": self.__class__.__name__, "args": args} def set_attributes(self, **kwargs): """Set keyword arguments as object attributes @@ -50,15 +68,21 @@ class Base: try: setattr(self, i, self.annotations[i](j)) except KeyError: - logger.debug(f"Attribute {i} does not belong to class {self.__class__.__name__}") + logger.debug( + f"Attribute {i} does not belong to class {self.__class__.__name__}" + ) continue except (ValueError, TypeError): - logger.debug(f'Cannot covert object of type {type(j)} to type {self.annotations[i]}') + logger.debug( + f"Cannot covert object of type {type(j)} to type {self.annotations[i]}" + ) setattr(self, i, j) except Exception as exe: - logger.debug(f'Did not set attribute {i} on class {self.__class__.__name__} due to {exe}') + logger.debug( + f"Did not set attribute {i} on class {self.__class__.__name__} due to {exe}" + ) continue @property @@ -71,7 +95,7 @@ class Base: """ annots = {} for base in self.__class__.__mro__[::-1]: - annots |= getattr(base, '__annotations__', {}) + annots |= getattr(base, "__annotations__", {}) return annots def get_dict(self, exclude: set = None, include: set = None) -> dict: @@ -89,7 +113,11 @@ class Base: """ exclude, include = exclude or set(), include or set() filter_ = include or set(self.dict.keys()).difference(exclude) - return {key: value for key, value in self.dict.items() if key in filter_ and value is not None} + return { + key: value + for key, value in self.dict.items() + if key in filter_ and value is not None + } @property @cache @@ -103,7 +131,9 @@ class Base: cls_dict = {} for cls in clss: cls_dict |= cls.__dict__ - return {key: value for key, value in cls_dict.items() if key in self.annotations} + return { + key: value for key, value in cls_dict.items() if key in self.annotations + } @property def dict(self) -> dict: @@ -114,8 +144,11 @@ class Base: """ try: _filter = self.exclude.difference(self.include) - return {key: value for key, value in (self.class_vars | self.__dict__).items() if - key not in _filter and value is not None} + return { + key: value + for key, value in (self.class_vars | self.__dict__).items() + if key not in _filter and value is not None + } except Exception as err: logger.warning(err) @@ -123,5 +156,5 @@ class Base: class _Base(Base): def __init__(self, **kwargs): self.config = Config() - self.mt5 = MetaTrader() if self.config.mode != 'backtest' else MetaBackTester() + self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester() super().__init__(**kwargs) diff --git a/src/aiomql/core/config.py b/src/aiomql/core/config.py index cec0e92..00c4950 100644 --- a/src/aiomql/core/config.py +++ b/src/aiomql/core/config.py @@ -11,6 +11,7 @@ logger = getLogger(__name__) Bot = TypeVar("Bot") BackTestEngine = TypeVar("BackTestEngine") + def func(): stack = inspect.stack() calling_context = next(context for context in stack if context.filename != __file__) @@ -40,8 +41,9 @@ class Config: or the load_config method. By passing reload=True to the load_config method, you can reload and search again for the config file. """ + login: int - trade_record_mode: Literal['csv', 'json'] + trade_record_mode: Literal["csv", "json"] password: str server: str path: str | Path @@ -58,14 +60,27 @@ class Config: _backtest_engine: BackTestEngine bot: Bot _instance: Self - mode: Literal['backtest', 'live'] + mode: Literal["backtest", "live"] use_terminal_for_backtesting: bool shutdown: bool force_shutdown: bool - _defaults = {"timeout": 60000, "record_trades": True, "trade_record_mode": "csv", "mode": "live", - 'filename': "aiomql.json", "records_dir_name": "trade_records", "backtest_dir_name": "backtesting", - "use_terminal_for_backtesting": True, 'path': '', 'login': 0, 'password': '', - 'server': '', 'records_dir': None, 'shutdown': False, 'force_shutdown': False} + _defaults = { + "timeout": 60000, + "record_trades": True, + "trade_record_mode": "csv", + "mode": "live", + "filename": "aiomql.json", + "records_dir_name": "trade_records", + "backtest_dir_name": "backtesting", + "use_terminal_for_backtesting": True, + "path": "", + "login": 0, + "password": "", + "server": "", + "records_dir": None, + "shutdown": False, + "force_shutdown": False, + } def __new__(cls, *args, **kwargs): if not hasattr(cls, "_instance"): @@ -78,7 +93,11 @@ class Config: return cls._instance def __init__(self, **kwargs): - self.set_attributes(**kwargs) + root = kwargs.pop("root", None) + if root is not None: + self.load_config(root=root, **kwargs) + else: + self.set_attributes(**kwargs) @property def backtest_engine(self): @@ -94,8 +113,10 @@ class Config: Keyword Args: **kwargs: Object attributes and values as keyword arguments """ - if kwargs.pop('root', None) is not None: - logger.warning('Tried setting root from set_attributes. Use load_config to change project root') + if kwargs.pop("root", None) is not None: + logger.warning( + "Tried setting root from set_attributes. Use load_config to change project root" + ) [setattr(self, key, value) for key, value in kwargs.items()] @staticmethod @@ -124,7 +145,14 @@ class Config: logger.debug(f"Error finding config file: {err}") return - def load_config(self, *, file: str | Path = None, filename: str = None, root: str | Path = None, **kwargs) -> Self: + def load_config( + self, + *, + file: str | Path = None, + filename: str = None, + root: str | Path = None, + **kwargs, + ) -> Self: """Load configuration settings from a file. Keyword Args: @@ -138,7 +166,7 @@ class Config: root.mkdir(parents=True, exist_ok=True) if not root.exists() else ... self.root = root else: - self.root = self.root if hasattr(self, 'root') else Path.cwd() + self.root = self.root if hasattr(self, "root") else Path.cwd() if file is not None: file = Path(file).resolve() if not file.exists(): @@ -162,13 +190,21 @@ class Config: self.set_attributes(**data) if self.path: - self.path = self.root / self.path if not Path(self.path).resolve().exists() else self.path + self.path = ( + self.root / self.path + if not Path(self.path).resolve().exists() + else self.path + ) - if self.record_trades and (hasattr(self, "records_dir") is False or self.records_dir is None): + if self.record_trades and ( + hasattr(self, "records_dir") is False + or self.records_dir is None + or root is not None + ): self.records_dir = self.root / self.records_dir_name self.records_dir.mkdir(parents=True, exist_ok=True) - if not hasattr(self, "backtest_dir"): + if not hasattr(self, "backtest_dir") or root is not None: self.backtest_dir = self.root / self.backtest_dir_name self.backtest_dir.mkdir(parents=True, exist_ok=True) @@ -180,4 +216,4 @@ class Config: Returns: dict: A dictionary of login details """ - return {'login': self.login, 'password': self.password, 'server': self.server} + return {"login": self.login, "password": self.password, "server": self.server} diff --git a/src/aiomql/core/constants.py b/src/aiomql/core/constants.py index 9280e86..12696e7 100644 --- a/src/aiomql/core/constants.py +++ b/src/aiomql/core/constants.py @@ -23,7 +23,7 @@ class Repr: class TradeAction(Repr, IntEnum): """TRADE_REQUEST_ACTION Enum. - + Attributes: DEAL (int): Delete the pending order placed previously Place a trade order for an immediate execution with the specified parameters (market order). @@ -31,8 +31,9 @@ class TradeAction(Repr, IntEnum): SLTP (int): Modify Stop Loss and Take Profit values of an opened position MODIFY (int): Modify the parameters of the order placed previously REMOVE (int): Delete the pending order placed previously - CLOSE_BY (int): Close a position by an opposite one + CLOSE_BY (int): Close a position by an opposite one """ + __enum_name__ = "TRADE_ACTION" DEAL = mt5.TRADE_ACTION_DEAL PENDING = mt5.TRADE_ACTION_PENDING @@ -44,7 +45,7 @@ class TradeAction(Repr, IntEnum): class OrderFilling(Repr, IntEnum): """ORDER_TYPE_FILLING Enum. - + Attributes: FOK (int): This execution policy means that an order can be executed only in the specified volume. If the necessary amount of a financial instrument is currently unavailable in the market, the order will @@ -61,6 +62,7 @@ class OrderFilling(Repr, IntEnum): During activation of the ORDER_TYPE_BUY_STOP_LIMIT and ORDER_TYPE_SELL_STOP_LIMIT orders, an appropriate limit order ORDER_TYPE_BUY_LIMIT/ORDER_TYPE_SELL_LIMIT with the ORDER_FILLING_RETURN type is created. """ + __enum_name__ = "ORDER_FILLING" FOK = mt5.ORDER_FILLING_FOK IOC = mt5.ORDER_FILLING_IOC @@ -69,14 +71,15 @@ class OrderFilling(Repr, IntEnum): class OrderTime(Repr, IntEnum): """ORDER_TIME Enum. - + Attributes: - GTC (int): Good till cancel order - DAY (int): Good till current trade day order - SPECIFIED (int): The order is active until the specified date + GTC (int): Good till cancel order + DAY (int): Good till current trade day order + SPECIFIED (int): The order is active until the specified date SPECIFIED_DAY (int): The order is active until 23:59:59 of the specified day. If this time appears to be out of a trading session, the expiration is processed at the nearest trading time. """ + __enum_name__ = "ORDER_TIME" GTC = mt5.ORDER_TIME_GTC DAY = mt5.ORDER_TIME_DAY @@ -86,21 +89,22 @@ class OrderTime(Repr, IntEnum): class OrderType(Repr, IntEnum): """ORDER_TYPE Enum. - + Attributes: - BUY (int): Market buy order - SELL (int): Market sell order - BUY_LIMIT (int): Buy Limit pending order - SELL_LIMIT (int): Sell Limit pending order - BUY_STOP (int): Buy Stop pending order - SELL_STOP (int): Sell Stop pending order - BUY_STOP_LIMIT (int): Upon reaching the order price, Buy Limit pending order is placed at StopLimit price - SELL_STOP_LIMIT (int): Upon reaching the order price, Sell Limit pending order is placed at StopLimit price - CLOSE_BY (int): Order for closing a position by an opposite one + BUY (int): Market buy order + SELL (int): Market sell order + BUY_LIMIT (int): Buy Limit pending order + SELL_LIMIT (int): Sell Limit pending order + BUY_STOP (int): Buy Stop pending order + SELL_STOP (int): Sell Stop pending order + BUY_STOP_LIMIT (int): Upon reaching the order price, Buy Limit pending order is placed at StopLimit price + SELL_STOP_LIMIT (int): Upon reaching the order price, Sell Limit pending order is placed at StopLimit price + CLOSE_BY (int): Order for closing a position by an opposite one Properties: opposite (int): Gets the opposite of an order type """ + __enum_name__ = "ORDER_TYPE" BUY = mt5.ORDER_TYPE_BUY SELL = mt5.ORDER_TYPE_SELL @@ -126,11 +130,12 @@ class BookType(Repr, IntEnum): """BOOK_TYPE Enum. Attributes: - SELL (int): Sell order (Offer) - BUY (int): Buy order (Bid) - SELL_MARKET (int): Sell order by Market - BUY_MARKET (int): Buy order by Market + SELL (int): Sell order (Offer) + BUY (int): Buy order (Bid) + SELL_MARKET (int): Sell order by Market + BUY_MARKET (int): Buy order by Market """ + __enum_name__ = "BOOK_TYPE" SELL = mt5.BOOK_TYPE_SELL BUY = mt5.BOOK_TYPE_BUY @@ -168,6 +173,7 @@ class TimeFrame(Repr, IntEnum): Methods: get: get a timeframe object from a time value in seconds """ + __enum_name__ = "TIMEFRAME" M1 = mt5.TIMEFRAME_M1 M2 = mt5.TIMEFRAME_M2 @@ -202,25 +208,82 @@ class TimeFrame(Repr, IntEnum): >>> print(t.seconds) 3600 """ - seconds = {1: 60, 2: 120, 3: 180, 4: 240, 5: 300, 6: 360, 10: 600, 15: 900, 20: 1200, 30: 1800, 16385: 3600, - 16386: 7200, 16387: 10800, 16388: 14400, 16390: 21600, 16392: 28800, 16396: 43200, 16408: 86400, - 32769: 604800, 49153: 2592000} + seconds = { + 1: 60, + 2: 120, + 3: 180, + 4: 240, + 5: 300, + 6: 360, + 10: 600, + 15: 900, + 20: 1200, + 30: 1800, + 16385: 3600, + 16386: 7200, + 16387: 10800, + 16388: 14400, + 16390: 21600, + 16392: 28800, + 16396: 43200, + 16408: 86400, + 32769: 604800, + 49153: 2592000, + } return seconds[self] @classmethod - def get_timeframe(cls, time: int) -> 'TimeFrame': + def get_timeframe(cls, time: int) -> "TimeFrame": """Get a timeframe object from a time value in seconds""" - time_frames = {60: 1, 120: 2, 180: 3, 240: 4, 300: 5, 360: 6, 600: 10, 900: 15, 1200: 20, 1800: 30, 3600: 16385, - 7200: 16386, 10800: 16387, 14400: 16388, 21600: 16390, 28800: 16392, 43200: 16396, 86400: 16408, - 604800: 32769, 2592000: 49153} + time_frames = { + 60: 1, + 120: 2, + 180: 3, + 240: 4, + 300: 5, + 360: 6, + 600: 10, + 900: 15, + 1200: 20, + 1800: 30, + 3600: 16385, + 7200: 16386, + 10800: 16387, + 14400: 16388, + 21600: 16390, + 28800: 16392, + 43200: 16396, + 86400: 16408, + 604800: 32769, + 2592000: 49153, + } return TimeFrame(time_frames[time]) @classmethod @property - def all(cls) -> tuple['TimeFrame', ...]: - return (TimeFrame.M1, TimeFrame.M2, TimeFrame.M3, TimeFrame.M4, TimeFrame.M5, TimeFrame.M6, TimeFrame.M10, - TimeFrame.M15, TimeFrame.M20, TimeFrame.M30, TimeFrame.H1, TimeFrame.H2, TimeFrame.H3, TimeFrame.H4, - TimeFrame.H6, TimeFrame.H8, TimeFrame.H12, TimeFrame.D1, TimeFrame.W1, TimeFrame.MN1) + def all(cls) -> tuple["TimeFrame", ...]: + return ( + TimeFrame.M1, + TimeFrame.M2, + TimeFrame.M3, + TimeFrame.M4, + TimeFrame.M5, + TimeFrame.M6, + TimeFrame.M10, + TimeFrame.M15, + TimeFrame.M20, + TimeFrame.M30, + TimeFrame.H1, + TimeFrame.H2, + TimeFrame.H3, + TimeFrame.H4, + TimeFrame.H6, + TimeFrame.H8, + TimeFrame.H12, + TimeFrame.D1, + TimeFrame.W1, + TimeFrame.MN1, + ) class CopyTicks(Repr, IntEnum): @@ -228,10 +291,11 @@ class CopyTicks(Repr, IntEnum): copy_ticks_range() functions. Attributes: - ALL (int): All ticks - INFO (int): Ticks containing Bid and/or Ask price changes - TRADE (int): Ticks containing Last and/or Volume price changes + ALL (int): All ticks + INFO (int): Ticks containing Bid and/or Ask price changes + TRADE (int): Ticks containing Last and/or Volume price changes """ + __enum_name__ = "COPY_TICKS" ALL = mt5.COPY_TICKS_ALL INFO = mt5.COPY_TICKS_INFO @@ -242,9 +306,10 @@ class PositionType(Repr, IntEnum): """POSITION_TYPE Enum. Direction of an open position (buy or sell) Attributes: - BUY (int): Buy + BUY (int): Buy SELL (int): Sell """ + __enum_name__ = "POSITION_TYPE" BUY = mt5.POSITION_TYPE_BUY SELL = mt5.POSITION_TYPE_SELL @@ -260,6 +325,7 @@ class PositionReason(Repr, IntEnum): EXPERT (int): The position was opened as a result of activation of an order placed from an MQL5 program, i.e. an Expert Advisor or a script """ + __enum_name__ = "POSITION_REASON" CLIENT = mt5.POSITION_REASON_CLIENT MOBILE = mt5.POSITION_REASON_MOBILE @@ -298,6 +364,7 @@ class DealType(Repr, IntEnum): DEAL_TYPE_SELL_CANCELED, and its profit/loss is zeroized. Previously obtained profit/loss is charged/withdrawn using a separated balance operation. """ + __enum_name__ = "DEAL_TYPE" BUY = mt5.DEAL_TYPE_BUY SELL = mt5.DEAL_TYPE_SELL @@ -319,7 +386,7 @@ class DealType(Repr, IntEnum): DEAL_TAX = mt5.DEAL_TAX def __str__(self): - if self.name in ('DEAL_DIVIDEND', 'DEAL_DIVIDEND_FRANKED', 'DEAL_TAX'): + if self.name in ("DEAL_DIVIDEND", "DEAL_DIVIDEND_FRANKED", "DEAL_TAX"): return self.name return super().__str__() @@ -336,6 +403,7 @@ class DealEntry(Repr, IntEnum): INOUT (int): Reverse OUT_BY (int): Close a position by an opposite one """ + __enum_name__ = "DEAL_ENTRY" IN = mt5.DEAL_ENTRY_IN OUT = mt5.DEAL_ENTRY_OUT @@ -362,6 +430,7 @@ class DealReason(Repr, IntEnum): SPLIT (int): The deal was executed after the split (price reduction) of an instrument, which had an open position during split announcement """ + __enum_name__ = "DEAL_REASON" CLIENT = mt5.DEAL_REASON_CLIENT MOBILE = mt5.DEAL_REASON_MOBILE @@ -387,6 +456,7 @@ class OrderReason(Repr, IntEnum): TP (int): The order was placed as a result of Take Profit activation SO (int): The order was placed as a result of the Stop Out event """ + __enum_name__ = "ORDER_REASON" CLIENT = mt5.ORDER_REASON_CLIENT MOBILE = mt5.ORDER_REASON_MOBILE @@ -406,6 +476,7 @@ class SymbolChartMode(Repr, IntEnum): BID (int): Bars are based on Bid prices LAST (int): Bars are based on last prices """ + __enum_name__ = "SYMBOL_CHART_MODE" BID = mt5.SYMBOL_CHART_MODE_BID LAST = mt5.SYMBOL_CHART_MODE_LAST @@ -437,6 +508,7 @@ class SymbolCalcMode(Repr, IntEnum): and liquidity ratio. The value is included into Assets, which are added to Equity. Open positions of such symbols increase the Free Margin amount and are used as additional margin (collateral) for open positions """ + __enum_name__ = "SYMBOL_CALC_MODE" FOREX = mt5.SYMBOL_CALC_MODE_FOREX FOREX_NO_LEVERAGE = mt5.SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE @@ -530,6 +602,7 @@ class SymbolSwapMode(Repr, IntEnum): Next day it is reopened by the current Bid price +/- specified number of points (parameters SYMBOL_SWAP_LONG and SYMBOL_SWAP_SHORT) """ + __enum_name__ = "SYMBOL_SWAP_MODE" DISABLED = mt5.SYMBOL_SWAP_MODE_DISABLED POINTS = mt5.SYMBOL_SWAP_MODE_POINTS @@ -554,6 +627,7 @@ class DayOfWeek(Repr, IntEnum): FRIDAY (int): Friday SATURDAY (int): Saturday """ + __enum__name__ = "DAY_OF_WEEK" SUNDAY = mt5.DAY_OF_WEEK_SUNDAY MONDAY = mt5.DAY_OF_WEEK_MONDAY @@ -579,6 +653,7 @@ class SymbolOrderGTCMode(Repr, IntEnum): DAILY_NO_STOPS (int): When a trade day changes, only pending orders are deleted, while Stop Loss and Take Profit levels are preserved """ + __enum_name__ = "SYMBOL_ORDERS" GTC = mt5.SYMBOL_ORDERS_GTC DAILY = mt5.SYMBOL_ORDERS_DAILY @@ -594,6 +669,7 @@ class SymbolOptionRight(Repr, IntEnum): CALL (int): A call option gives you the right to buy an asset at a specified price. PUT (int): A put option gives you the right to sell an asset at a specified price. """ + __enum_name__ = "SYMBOL_OPTION_RIGHT" CALL = mt5.SYMBOL_OPTION_RIGHT_CALL PUT = mt5.SYMBOL_OPTION_RIGHT_PUT @@ -608,6 +684,7 @@ class SymbolOptionMode(Repr, IntEnum): AMERICAN (int): American option may be exercised on any trading day or before expiry. The period within which a buyer can exercise the option is specified for it. """ + __enum_name__ = "SYMBOL_OPTION_MODE" EUROPEAN = mt5.SYMBOL_OPTION_MODE_EUROPEAN AMERICAN = mt5.SYMBOL_OPTION_MODE_AMERICAN @@ -623,6 +700,7 @@ class AccountTradeMode(Repr, IntEnum): CONTEST: Contest account REAL: Real Account """ + __enum_name__ = "ACCOUNT_TRADE_MODE" DEMO = mt5.ACCOUNT_TRADE_MODE_DEMO CONTEST = mt5.ACCOUNT_TRADE_MODE_CONTEST @@ -640,7 +718,8 @@ class TickFlag(Repr, IntFlag): VOLUME (int): Volume changed BUY (int): last Buy price changed SELL (int): last Sell price changed - """ + """ + __enum_name__ = "TICK_FLAG" BID = mt5.TICK_FLAG_BID ASK = mt5.TICK_FLAG_ASK @@ -718,6 +797,7 @@ class TradeRetcode(Repr, IntEnum): FIFO_CLOSE (int): The request is rejected, because "Position closing is allowed only by FIFO rule" flag is set for the trading account (ACCOUNT_FIFO_CLOSE=true) """ + __enum_name__ = "TRADE_RETCODE" REQUOTE = mt5.TRADE_RETCODE_REQUOTE REJECT = mt5.TRADE_RETCODE_REJECT @@ -789,6 +869,7 @@ class AccountMarginMode(Repr, IntEnum): (hedging, multiple positions can exist for one symbol). The margin is calculated based on the symbol type (SYMBOL_TRADE_CALC_MODE) taking into account the hedged margin (SYMBOL_MARGIN_HEDGED). """ + __enum_name__ = "ACCOUNT_MARGIN_MODE" RETAIL_NETTING = mt5.ACCOUNT_MARGIN_MODE_RETAIL_NETTING EXCHANGE = mt5.ACCOUNT_MARGIN_MODE_EXCHANGE diff --git a/src/aiomql/core/errors.py b/src/aiomql/core/errors.py index 1094ffb..56d0116 100644 --- a/src/aiomql/core/errors.py +++ b/src/aiomql/core/errors.py @@ -1,30 +1,31 @@ class Error: """Error class for handling errors from MetaTrader 5.""" + descriptions = { # common errors - 1: 'Successful', - -1: 'generic fail', - -2: 'invalid arguments/parameters', - -3: 'no memory condition', - -4: 'no history', - -5: 'invalid version', - -6: 'authorization failed', - -7: 'unsupported method', - -8: 'auto-trading disabled', + 1: "Successful", + -1: "generic fail", + -2: "invalid arguments/parameters", + -3: "no memory condition", + -4: "no history", + -5: "invalid version", + -6: "authorization failed", + -7: "unsupported method", + -8: "auto-trading disabled", # internal errors - -10000: 'internal IPC general error', - -10001: 'internal IPC send failed', - -10002: 'internal IPC recv failed', - -10003: 'internal IPC initialization fail', - -10004: 'internal IPC no ipc', - -10005: 'internal timeout', + -10000: "internal IPC general error", + -10001: "internal IPC send failed", + -10002: "internal IPC recv failed", + -10003: "internal IPC initialization fail", + -10004: "internal IPC no ipc", + -10005: "internal timeout", } conn_errors = (-10000, -10001, -10002, -10003, -10004, -10005, -6) - def __init__(self, code: int, description: str = ''): + def __init__(self, code: int, description: str = ""): self.code = code - self.description = description or self.descriptions.get(code, 'unknown error') + self.description = description or self.descriptions.get(code, "unknown error") def is_connection_error(self): return self.code in self.conn_errors diff --git a/src/aiomql/core/event_manager.py b/src/aiomql/core/event_manager.py deleted file mode 100644 index 668a112..0000000 --- a/src/aiomql/core/event_manager.py +++ /dev/null @@ -1,67 +0,0 @@ -import asyncio -from asyncio import Condition, Task -from typing import Self -from datetime import datetime -from .config import Config - - -class EventManager: - _instance: Self - task_tracker: int - config: Config - tasks: list[Task] - num_main_tasks: int # main tasks that are directly controlled by the Condition Synchronization primitives - - def __new__(cls, *args, **kwargs): - if not hasattr(cls, "_instance"): - cls._instance = super().__new__(cls) - cls._instance.config = Config() - cls._instance.condition = Condition() - cls._instance.num_main_tasks = 0 - cls._instance.task_tracker = 0 - cls._instance.tasks = [] - return cls._instance - - def __init__(self, *, num_tasks: int = 0): - self.num_main_tasks = num_tasks or self.num_main_tasks - - @property - def backtest_engine(self): - return self.config.backtest_engine - - def add_tasks(self, *tasks: Task): - self.tasks.extend(tasks) - - def sigint_handler(self, sig, frame): - for task in self.tasks: - task.cancel() - self.backtest_engine.wrap_up() - - async def acquire(self): - await self.condition.acquire() - - def notify_all(self): - self.condition.notify_all() - - async def event_monitor(self): - self.backtest_engine.next() - while True: - async with self.condition: - if self.task_tracker == self.num_main_tasks: # all main tasks have been completed in the current cycle - self.task_tracker = 0 - await self.backtest_engine.tracker() - self.backtest_engine.next() - self.condition.notify_all() - if self.backtest_engine.cursor.time % 3600 == 0: - print(datetime.strftime(self.backtest_engine.cursor.time, "%Y-%m-%d %H:%M:%S")) - if self.backtest_engine.stop_testing: - break - await asyncio.sleep(0) - self.backtest_engine.wrap_up() - - async def wait(self): - self.task_tracker += 1 - await self.condition.wait() - - def release(self): - self.condition.release() diff --git a/src/aiomql/core/exceptions.py b/src/aiomql/core/exceptions.py index cffbd2a..fcba963 100644 --- a/src/aiomql/core/exceptions.py +++ b/src/aiomql/core/exceptions.py @@ -1,19 +1,33 @@ """Exceptions for the aiomql package.""" -__all__ = ['LoginError', 'VolumeError', 'SymbolError', 'OrderError'] +__all__ = ["LoginError", "VolumeError", "SymbolError", "OrderError"] + class LoginError(Exception): """Raised when an error occurs when logging in.""" - pass + + ... + class VolumeError(Exception): """Raised when a volume is not valid or out of range for a symbol.""" - pass + + ... class SymbolError(Exception): """Raised when a symbol is not provided where required or not available in the Market Watch.""" + ... + class OrderError(Exception): """Raised when an error occurs when working with the order class.""" + + ... + + +class StopTrading(Exception): + """Raised when the user wants to stop trading.""" + + ... diff --git a/src/aiomql/core/meta_backtester.py b/src/aiomql/core/meta_backtester.py index 8a11d84..9ac2b34 100644 --- a/src/aiomql/core/meta_backtester.py +++ b/src/aiomql/core/meta_backtester.py @@ -3,8 +3,17 @@ from logging import getLogger from typing import Literal, TypeVar from numpy import ndarray -from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TerminalInfo, TradeOrder, TradePosition, TradeDeal, - OrderCheckResult, OrderSendResult) +from MetaTrader5 import ( + Tick, + SymbolInfo, + AccountInfo, + TerminalInfo, + TradeOrder, + TradePosition, + TradeDeal, + OrderCheckResult, + OrderSendResult, +) from .meta_trader import MetaTrader from .constants import TimeFrame, CopyTicks, OrderType @@ -12,10 +21,12 @@ from .._utils import error_handler logger = getLogger(__name__) -BackTestEngine = TypeVar('BackTestEngine') +BackTestEngine = TypeVar("BackTestEngine") + class MetaBackTester(MetaTrader): """A class for testing trading strategies in the MetaTrader 5 terminal. A subclass of MetaTrader.""" + backtest_engine: BackTestEngine def __init__(self, *, backtest_engine: BackTestEngine = None): @@ -32,111 +43,173 @@ class MetaBackTester(MetaTrader): self.config.backtest_engine = value async def last_error(self) -> tuple[int, str]: - return -1, '' - - async def initialize(self, *, path: str = "", login: int = 0, password: str = "", server: str = "", - timeout: int | None = None, portable=False) -> bool: + return -1, "" + + async def initialize( + self, + *, + path: str = "", + login: int = 0, + password: str = "", + server: str = "", + timeout: int | None = None, + portable=False, + ) -> bool: if self.config.use_terminal_for_backtesting: - return await super().initialize(path=path, login=login, password=password, server=server, timeout=timeout) + return await super().initialize( + path=path, + login=login, + password=password, + server=server, + timeout=timeout, + ) return True - - async def login(self, *, login: int = 0, password: str = '', server: str = '', timeout: int = 60000) -> bool: + + async def login( + self, + *, + login: int = 0, + password: str = "", + server: str = "", + timeout: int = 60000, + ) -> bool: if self.config.use_terminal_for_backtesting: - return await super().login(login=login, password=password, server=server, timeout=timeout) + return await super().login( + login=login, password=password, server=server, timeout=timeout + ) return True async def shutdown(self) -> None: await super().shutdown() if self.config.use_terminal_for_backtesting else ... - @error_handler(msg='test data not available', exe=AttributeError) + @error_handler(msg="test data not available", exe=AttributeError) async def terminal_info(self) -> TerminalInfo: res = await self.backtest_engine.get_terminal_info() return res - @error_handler(msg='test data not available', exe=AttributeError) + @error_handler(msg="test data not available", exe=AttributeError) async def account_info(self) -> AccountInfo: """""" return self.backtest_engine.get_account_info() - @error_handler(msg='test data not available', exe=AttributeError) + @error_handler(msg="test data not available", exe=AttributeError) async def symbols_total(self) -> int: tot = await self.backtest_engine.get_symbols_total() return tot - @error_handler(msg='test data not available', exe=AttributeError) + @error_handler(msg="test data not available", exe=AttributeError) async def symbols_get(self, group: str = "") -> tuple[SymbolInfo, ...] | None: """""" syms = await self.backtest_engine.get_symbols(group=group) return syms - @error_handler(msg='test data not available', exe=AttributeError) + @error_handler(msg="test data not available") async def symbol_info(self, symbol: str) -> SymbolInfo | None: sym = await self.backtest_engine.get_symbol_info(symbol=symbol) return sym - @error_handler(msg='test data not available', exe=AttributeError) + @error_handler(msg="test data not available") async def symbol_info_tick(self, symbol: str) -> Tick | None: tick = await self.backtest_engine.get_symbol_info_tick(symbol=symbol) return tick - @error_handler(msg='test data not available', exe=AttributeError) - async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, - count: int) -> ndarray | None: - rates = await self.backtest_engine.get_rates_from(symbol=symbol, timeframe=timeframe, date_from=date_from, - count=count) + @error_handler(msg="test data not available", exe=AttributeError) + async def copy_rates_from( + self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int + ) -> ndarray | None: + rates = await self.backtest_engine.get_rates_from( + symbol=symbol, timeframe=timeframe, date_from=date_from, count=count + ) return rates - @error_handler(msg='test data not available', exe=AttributeError) - async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, - count: int) -> ndarray | None: - rates = await self.backtest_engine.get_rates_from_pos(symbol=symbol, timeframe=timeframe, start_pos=start_pos, - count=count) + @error_handler(msg="test data not available", exe=AttributeError) + async def copy_rates_from_pos( + self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int + ) -> ndarray | None: + rates = await self.backtest_engine.get_rates_from_pos( + symbol=symbol, timeframe=timeframe, start_pos=start_pos, count=count + ) return rates - @error_handler(msg='test data not available', exe=AttributeError) - async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, - date_to: datetime | float) -> ndarray | None: - rates = await self.backtest_engine.get_rates_range(symbol=symbol, timeframe=timeframe, date_from=date_from, - date_to=date_to) + @error_handler(msg="test data not available", exe=AttributeError) + async def copy_rates_range( + self, + symbol: str, + timeframe: TimeFrame, + date_from: datetime | float, + date_to: datetime | float, + ) -> ndarray | None: + rates = await self.backtest_engine.get_rates_range( + symbol=symbol, timeframe=timeframe, date_from=date_from, date_to=date_to + ) return rates - @error_handler(msg='test data not available', exe=AttributeError) - async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, - flags: CopyTicks) -> ndarray | None: - ticks = await self.backtest_engine.get_ticks_from(symbol=symbol, date_from=date_from, count=count, flags=flags) + @error_handler(msg="test data not available", exe=AttributeError) + async def copy_ticks_from( + self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks + ) -> ndarray | None: + ticks = await self.backtest_engine.get_ticks_from( + symbol=symbol, date_from=date_from, count=count, flags=flags + ) return ticks - @error_handler(msg='test data not available', exe=AttributeError) - async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, - flags: CopyTicks) -> ndarray | None: - ticks = await self.backtest_engine.get_ticks_range(symbol=symbol, date_from=date_from, date_to=date_to, - flags=flags) + @error_handler(msg="test data not available", exe=AttributeError) + async def copy_ticks_range( + self, + symbol: str, + date_from: datetime | float, + date_to: datetime | float, + flags: CopyTicks, + ) -> ndarray | None: + ticks = await self.backtest_engine.get_ticks_range( + symbol=symbol, date_from=date_from, date_to=date_to, flags=flags + ) return ticks - @error_handler(msg='test data not available', exe=AttributeError) + @error_handler(msg="test data not available", exe=AttributeError) async def orders_total(self) -> int: return self.backtest_engine.get_orders_total() - @error_handler(msg='test data not available', exe=AttributeError) - async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder, ...] | None: - kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value} + @error_handler(msg="test data not available", exe=AttributeError) + async def orders_get( + self, group: str = "", ticket: int = 0, symbol: str = "" + ) -> tuple[TradeOrder, ...] | None: + kwargs = { + key: value + for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) + if value + } return self.backtest_engine.get_orders(**kwargs) - @error_handler(msg='test data not available', exe=AttributeError) - async def order_calc_margin(self, action: OrderType, symbol: str, volume: float, price: float) -> float | None: - res = await self.backtest_engine.order_calc_margin(action=action, symbol=symbol, volume=volume, price=price) + @error_handler(msg="test data not available", exe=AttributeError) + async def order_calc_margin( + self, action: OrderType, symbol: str, volume: float, price: float + ) -> float | None: + res = await self.backtest_engine.order_calc_margin( + action=action, symbol=symbol, volume=volume, price=price + ) return res - @error_handler(msg='test data not available', exe=AttributeError) - async def order_calc_profit(self, action: Literal[0, 1], symbol: str, volume: float, price_open: float, - price_close: float) -> float | None: - - profit = await self.backtest_engine.order_calc_profit(action=action, symbol=symbol, volume=volume, - price_open=price_open, price_close=price_close) + @error_handler(msg="test data not available", exe=AttributeError) + async def order_calc_profit( + self, + action: Literal[0, 1], + symbol: str, + volume: float, + price_open: float, + price_close: float, + ) -> float | None: + profit = await self.backtest_engine.order_calc_profit( + action=action, + symbol=symbol, + volume=volume, + price_open=price_open, + price_close=price_close, + ) return profit - @error_handler(msg='test data not available', exe=AttributeError) + @error_handler(msg="test data not available", exe=AttributeError) async def order_check(self, request: dict) -> OrderCheckResult: ocr = await self.backtest_engine.order_check(request=request) return ocr @@ -145,38 +218,71 @@ class MetaBackTester(MetaTrader): osr = await self.backtest_engine.order_send(request=request) return osr - @error_handler(msg='test data not available', exe=AttributeError) + @error_handler(msg="test data not available", exe=AttributeError) async def positions_total(self) -> int: return self.backtest_engine.get_positions_total() - @error_handler(msg='test data not available', exe=AttributeError) - async def positions_get(self, group: str = "", ticket: int = None, - symbol: str = "") -> tuple[TradePosition, ...] | None: - kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value} + @error_handler(msg="test data not available", exe=AttributeError) + async def positions_get( + self, group: str = "", ticket: int = None, symbol: str = "" + ) -> tuple[TradePosition, ...] | None: + kwargs = { + key: value + for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) + if value + } return self.backtest_engine.get_positions(**kwargs) - @error_handler(msg='test data not available', exe=AttributeError) - async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int | None: - return self.backtest_engine.get_history_orders_total(date_from=date_from, date_to=date_to) + @error_handler(msg="test data not available", exe=AttributeError) + async def history_orders_total( + self, date_from: datetime | float, date_to: datetime | float + ) -> int | None: + return self.backtest_engine.get_history_orders_total( + date_from=date_from, date_to=date_to + ) - @error_handler(msg='test data not available', exe=AttributeError) - async def history_orders_get(self, date_from: datetime | float = None, date_to: datetime | float = None, - group: str = '', ticket: int = None, - position: int = None) -> tuple[TradeOrder, ...] | None: - args = (('date_from', date_from), ('date_to', date_to), ('group', group), ('ticket', ticket), - ('position', position)) + @error_handler(msg="test data not available", exe=AttributeError) + async def history_orders_get( + self, + date_from: datetime | float = None, + date_to: datetime | float = None, + group: str = "", + ticket: int = None, + position: int = None, + ) -> tuple[TradeOrder, ...] | None: + args = ( + ("date_from", date_from), + ("date_to", date_to), + ("group", group), + ("ticket", ticket), + ("position", position), + ) kwargs = {key: value for key, value in args if value} return self.backtest_engine.get_history_orders(**kwargs) - @error_handler(msg='test data not available', exe=AttributeError) - async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int | None: - return self.backtest_engine.get_history_deals_total(date_from=date_from, date_to=date_to) + @error_handler(msg="test data not available", exe=AttributeError) + async def history_deals_total( + self, date_from: datetime | float, date_to: datetime | float + ) -> int | None: + return self.backtest_engine.get_history_deals_total( + date_from=date_from, date_to=date_to + ) - @error_handler(msg='test data not available', exe=AttributeError) - async def history_deals_get(self, date_from: datetime | float = None, date_to: datetime | float = None, - group: str = '', ticket: int = None, - position: int = None) -> tuple[TradeDeal, ...] | None: - args = (('date_from', date_from), ('date_to', date_to), ('group', group), ('ticket', ticket), - ('position', position)) + @error_handler(msg="test data not available", exe=AttributeError) + async def history_deals_get( + self, + date_from: datetime | float = None, + date_to: datetime | float = None, + group: str = "", + ticket: int = None, + position: int = None, + ) -> tuple[TradeDeal, ...] | None: + args = ( + ("date_from", date_from), + ("date_to", date_to), + ("group", group), + ("ticket", ticket), + ("position", position), + ) kwargs = {key: value for key, value in args if value} return self.backtest_engine.get_history_deals(**kwargs) diff --git a/src/aiomql/core/meta_trader.py b/src/aiomql/core/meta_trader.py index 1972727..40f8210 100644 --- a/src/aiomql/core/meta_trader.py +++ b/src/aiomql/core/meta_trader.py @@ -5,8 +5,18 @@ from typing import Literal from pathlib import Path import numpy as np -from MetaTrader5 import (BookInfo, SymbolInfo, AccountInfo, Tick, TerminalInfo, TradeOrder, TradeDeal, - TradePosition, OrderSendResult, OrderCheckResult) +from MetaTrader5 import ( + BookInfo, + SymbolInfo, + AccountInfo, + Tick, + TerminalInfo, + TradeOrder, + TradeDeal, + TradePosition, + OrderSendResult, + OrderCheckResult, +) from .constants import OrderType, CopyTicks @@ -18,12 +28,11 @@ logger = getLogger() class MetaTrader(MetaCore): - def __init__(self): self.config = Config() self.error: Error = Error(1) - async def __aenter__(self) -> 'MetaTrader': + async def __aenter__(self) -> "MetaTrader": """ Async context manager entry point. Initializes the connection to the MetaTrader terminal. @@ -42,10 +51,10 @@ class MetaTrader(MetaCore): await self.shutdown() async def _handler(self, api: dict): - func = api['func'] - args = api.get('args', ()) - kwargs = api.get('kwargs', {}) - error_msg = api.get('error_msg', 'An error occurred') + func = api["func"] + args = api.get("args", ()) + kwargs = api.get("kwargs", {}) + error_msg = api.get("error_msg", "An error occurred") res = await asyncio.to_thread(func, *args, **kwargs) if res is None: @@ -60,12 +69,19 @@ class MetaTrader(MetaCore): if res is None: err = await self.last_error() self.error = Error(*err) - logger.warning(f'{error_msg}:{self.error.description}') + logger.warning(f"{error_msg}:{self.error.description}") else: - logger.warning(f'{error_msg}:{self.error.description}') + logger.warning(f"{error_msg}:{self.error.description}") return res - async def login(self, *, login: int = 0, password: str = '', server: str = '', timeout: int = 60000) -> bool: + async def login( + self, + *, + login: int = 0, + password: str = "", + server: str = "", + timeout: int = 60000, + ) -> bool: """ Connects to the MetaTrader terminal using the specified login, password and server. @@ -79,13 +95,22 @@ class MetaTrader(MetaCore): bool: True if successful, False otherwise. """ acc_details = self.config.account_info() - login = login or acc_details.get('login', 0) - password = password or acc_details.get('password', '') - server = server or acc_details.get('server', '') - return await asyncio.to_thread(self._login, login, password=password, server=server, timeout=timeout) + login = login or acc_details.get("login", 0) + password = password or acc_details.get("password", "") + server = server or acc_details.get("server", "") + return await asyncio.to_thread( + self._login, login, password=password, server=server, timeout=timeout + ) - async def initialize(self, path: str = None, login: int = 0, password: str = "", server: str = "", - timeout: int | None = None, portable=False) -> bool: + async def initialize( + self, + path: str = None, + login: int = 0, + password: str = "", + server: str = "", + timeout: int | None = None, + portable=False, + ) -> bool: """ Initializes the connection to the MetaTrader terminal. All parameters are optional. @@ -105,11 +130,17 @@ class MetaTrader(MetaCore): path = "" if Path(path).exists() is False else path args = (str(path),) if path else () acc = self.config.account_info() - kwargs = {key: value for key, value in (('login', login or acc.get('login')), - ('password', password or acc.get('password')), - ('server', server or acc.get('server')), - ('timeout', timeout or 60000), - ('portable', portable)) if key is not None} + kwargs = { + key: value + for key, value in ( + ("login", login or acc.get("login")), + ("password", password or acc.get("password")), + ("server", server or acc.get("server")), + ("timeout", timeout or 60000), + ("portable", portable), + ) + if key is not None + } res = await asyncio.to_thread(self._initialize, *args, **kwargs) if res is False: await self.shutdown() @@ -120,8 +151,7 @@ class MetaTrader(MetaCore): return res async def shutdown(self) -> None: - """Closes the connection to the MetaTrader terminal. - """ + """Closes the connection to the MetaTrader terminal.""" self._shutdown() async def last_error(self) -> tuple[int, str]: @@ -129,173 +159,336 @@ class MetaTrader(MetaCore): res = await asyncio.to_thread(self._last_error) return res except Exception as err: - logger.warning(f'Error in obtaining last error.') + logger.warning(f"Error in obtaining last error.") return -1, str(err) async def version(self) -> tuple[int, int, str] | None: """""" - api = {'func': self._version, 'error_msg': 'Error in obtaining version.'} + api = {"func": self._version, "error_msg": "Error in obtaining version."} res = await self._handler(api) return res async def account_info(self) -> AccountInfo | None: """""" - api = {'func': self._account_info, 'error_msg': 'Error in obtaining account information'} + api = { + "func": self._account_info, + "error_msg": "Error in obtaining account information", + } res = await self._handler(api) return res async def terminal_info(self) -> TerminalInfo | None: - api = {'func': self._terminal_info, 'error_msg': 'Error in obtaining terminal information'} + api = { + "func": self._terminal_info, + "error_msg": "Error in obtaining terminal information", + } res = await self._handler(api) return res async def symbols_total(self) -> int: - api = {'func': self._symbols_total, 'error_msg': 'Error in obtaining total symbols.'} + api = { + "func": self._symbols_total, + "error_msg": "Error in obtaining total symbols.", + } res = await self._handler(api) return res async def symbols_get(self, group: str = "") -> tuple[SymbolInfo] | None: - kwargs = {'group': group} if group else {} - api = {'func': self._symbols_get, 'kwargs': kwargs, 'error_msg': 'Error in obtaining symbols.'} + kwargs = {"group": group} if group else {} + api = { + "func": self._symbols_get, + "kwargs": kwargs, + "error_msg": "Error in obtaining symbols.", + } res = await self._handler(api) return res async def symbol_info(self, symbol: str) -> SymbolInfo | None: - api = {'func': self._symbol_info, 'args': (symbol,), 'error_msg': f'Error in obtaining information for {symbol}'} + api = { + "func": self._symbol_info, + "args": (symbol,), + "error_msg": f"Error in obtaining information for {symbol}", + } res = await self._handler(api) return res async def symbol_info_tick(self, symbol: str) -> Tick | None: - api = {'func': self._symbol_info_tick, 'args': (symbol,), 'error_msg': f'Error in obtaining tick for {symbol}'} + api = { + "func": self._symbol_info_tick, + "args": (symbol,), + "error_msg": f"Error in obtaining tick for {symbol}", + } res = await self._handler(api) return res async def symbol_select(self, symbol: str, enable: bool) -> bool: - api = {'func': self._symbol_select, 'args': (symbol, enable), 'error_msg': f'Error in selecting {symbol}'} + api = { + "func": self._symbol_select, + "args": (symbol, enable), + "error_msg": f"Error in selecting {symbol}", + } res = await self._handler(api) return res async def market_book_add(self, symbol: str) -> bool: - api = {'func': self._market_book_add, 'args': (symbol,), 'error_msg': f'Error in adding {symbol} to market book'} + api = { + "func": self._market_book_add, + "args": (symbol,), + "error_msg": f"Error in adding {symbol} to market book", + } res = await self._handler(api) return res async def market_book_get(self, symbol: str) -> tuple[BookInfo] | None: - api = {'func': self._market_book_get, 'args': (symbol,), 'error_msg': f'Error in obtaining market depth for {symbol}'} + api = { + "func": self._market_book_get, + "args": (symbol,), + "error_msg": f"Error in obtaining market depth for {symbol}", + } res = await self._handler(api) return res async def market_book_release(self, symbol: str) -> bool: - api = {'func': self._market_book_release, 'args': (symbol,), 'error_msg': f'Error in releasing market depth for {symbol}'} + api = { + "func": self._market_book_release, + "args": (symbol,), + "error_msg": f"Error in releasing market depth for {symbol}", + } res = await self._handler(api) return res - async def copy_rates_from(self, symbol: str, timeframe: int, date_from: datetime | float, count: int) -> np.ndarray | None: - api = {'func': self._copy_rates_from, 'args': (symbol, timeframe, date_from, count), - 'error_msg': f'Error in obtaining rates for {symbol}'} + async def copy_rates_from( + self, symbol: str, timeframe: int, date_from: datetime | float, count: int + ) -> np.ndarray | None: + api = { + "func": self._copy_rates_from, + "args": (symbol, timeframe, date_from, count), + "error_msg": f"Error in obtaining rates for {symbol}", + } res = await self._handler(api) return res - async def copy_rates_from_pos(self, symbol: str, timeframe: int, start_pos: int, count: int) -> np.ndarray | None: - api = {'func': self._copy_rates_from_pos, 'args': (symbol, timeframe, start_pos, count), - 'error_msg': f'Error in obtaining rates for {symbol}'} + async def copy_rates_from_pos( + self, symbol: str, timeframe: int, start_pos: int, count: int + ) -> np.ndarray | None: + api = { + "func": self._copy_rates_from_pos, + "args": (symbol, timeframe, start_pos, count), + "error_msg": f"Error in obtaining rates for {symbol}", + } res = await self._handler(api) return res - async def copy_rates_range(self, symbol: str, timeframe: int, date_from: datetime | float, - date_to: datetime | float) -> np.ndarray | None: - api = {'func': self._copy_rates_range, 'args': (symbol, timeframe, date_from, date_to), - 'error_msg': f'Error in obtaining rates for {symbol}'} + async def copy_rates_range( + self, + symbol: str, + timeframe: int, + date_from: datetime | float, + date_to: datetime | float, + ) -> np.ndarray | None: + api = { + "func": self._copy_rates_range, + "args": (symbol, timeframe, date_from, date_to), + "error_msg": f"Error in obtaining rates for {symbol}", + } res = await self._handler(api) return res - async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, - flags: CopyTicks) -> np.ndarray | None: - api = {'func': self._copy_ticks_from, 'args': (symbol, date_from, count, flags), - 'error_msg': f'Error in obtaining ticks for {symbol}'} + async def copy_ticks_from( + self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks + ) -> np.ndarray | None: + api = { + "func": self._copy_ticks_from, + "args": (symbol, date_from, count, flags), + "error_msg": f"Error in obtaining ticks for {symbol}", + } res = await self._handler(api) return res - async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, - flags: CopyTicks) -> np.ndarray | None: - api = {'func': self._copy_ticks_range, 'args': (symbol, date_from, date_to, flags), - 'error_msg': f'Error in obtaining ticks for {symbol}'} + async def copy_ticks_range( + self, + symbol: str, + date_from: datetime | float, + date_to: datetime | float, + flags: CopyTicks, + ) -> np.ndarray | None: + api = { + "func": self._copy_ticks_range, + "args": (symbol, date_from, date_to, flags), + "error_msg": f"Error in obtaining ticks for {symbol}", + } res = await self._handler(api) return res async def orders_total(self) -> int: - api = {'func': self._orders_total, 'error_msg': 'Error in obtaining total orders.'} + api = { + "func": self._orders_total, + "error_msg": "Error in obtaining total orders.", + } res = await self._handler(api) return res - async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder] | None: - kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value} - api = {'func': self._orders_get, 'kwargs': kwargs, 'error_msg': 'Error in obtaining orders.'} + async def orders_get( + self, group: str = "", ticket: int = 0, symbol: str = "" + ) -> tuple[TradeOrder] | None: + kwargs = { + key: value + for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) + if value + } + api = { + "func": self._orders_get, + "kwargs": kwargs, + "error_msg": "Error in obtaining orders.", + } res = await self._handler(api) return res - async def order_calc_margin(self, action: Literal[OrderType.BUY, OrderType.SELL], - symbol: str, volume: float, price: float) -> float | None: - api = {'func': self._order_calc_margin, 'args': (action, symbol, volume, price), - 'error_msg': 'Error in calculating margin.'} + async def order_calc_margin( + self, + action: Literal[OrderType.BUY, OrderType.SELL], + symbol: str, + volume: float, + price: float, + ) -> float | None: + api = { + "func": self._order_calc_margin, + "args": (action, symbol, volume, price), + "error_msg": "Error in calculating margin.", + } res = await self._handler(api) return res - async def order_calc_profit(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, - volume: float, price_open: float, price_close: float) -> float | None: - api = {'func': self._order_calc_profit, 'args': (action, symbol, volume, price_open, price_close), - 'error_msg': 'Error in calculating profit.'} + async def order_calc_profit( + self, + action: Literal[OrderType.BUY, OrderType.SELL], + symbol: str, + volume: float, + price_open: float, + price_close: float, + ) -> float | None: + api = { + "func": self._order_calc_profit, + "args": (action, symbol, volume, price_open, price_close), + "error_msg": "Error in calculating profit.", + } res = await self._handler(api) return res async def order_check(self, request: dict) -> OrderCheckResult: - api = {'func': self._order_check, 'args': (request,), 'error_msg': 'Error in checking order.'} + api = { + "func": self._order_check, + "args": (request,), + "error_msg": "Error in checking order.", + } res = await self._handler(api) return res async def order_send(self, request: dict) -> OrderSendResult: - api = {'func': self._order_send, 'args': (request,), 'error_msg': 'Error in sending order.'} + api = { + "func": self._order_send, + "args": (request,), + "error_msg": "Error in sending order.", + } res = await self._handler(api) return res async def positions_total(self) -> int: - api = {'func': self._positions_total, 'error_msg': 'Error in obtaining total positions.'} + api = { + "func": self._positions_total, + "error_msg": "Error in obtaining total positions.", + } res = await self._handler(api) return res - async def positions_get(self, group: str = "", ticket: int = None, symbol: str = "") -> tuple[TradePosition] | None: - kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value} - api = {'func': self._positions_get, 'kwargs': kwargs, - 'error_msg': 'Error in obtaining open positions.'} + async def positions_get( + self, group: str = "", ticket: int = None, symbol: str = "" + ) -> tuple[TradePosition] | None: + kwargs = { + key: value + for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) + if value + } + api = { + "func": self._positions_get, + "kwargs": kwargs, + "error_msg": "Error in obtaining open positions.", + } res = await self._handler(api) return res - async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int: - api = {'func': self._history_orders_total, 'args': (date_from, date_to), - 'error_msg': 'Error in obtaining total history orders.'} + async def history_orders_total( + self, date_from: datetime | float, date_to: datetime | float + ) -> int: + api = { + "func": self._history_orders_total, + "args": (date_from, date_to), + "error_msg": "Error in obtaining total history orders.", + } res = await self._handler(api) return res - async def history_orders_get(self, date_from: datetime | float = None, date_to: datetime | float = None, - group: str = '', ticket: int = None, position: int = None) -> tuple[TradeOrder] | None: - kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value} + async def history_orders_get( + self, + date_from: datetime | float = None, + date_to: datetime | float = None, + group: str = "", + ticket: int = None, + position: int = None, + ) -> tuple[TradeOrder] | None: + kwargs = { + key: value + for key, value in ( + ("group", group), + ("ticket", ticket), + ("position", position), + ) + if value + } args = tuple(arg for arg in (date_from, date_to) if arg) - api = {'func': self._history_orders_get, 'args': args, 'kwargs': kwargs, - 'error_msg': 'Error in obtaining history orders'} + api = { + "func": self._history_orders_get, + "args": args, + "kwargs": kwargs, + "error_msg": "Error in obtaining history orders", + } res = await self._handler(api) return res - async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int: - api = {'func': self._history_deals_total, 'args': (date_from, date_to), - 'error_msg': 'Error in obtaining total history deals'} + async def history_deals_total( + self, date_from: datetime | float, date_to: datetime | float + ) -> int: + api = { + "func": self._history_deals_total, + "args": (date_from, date_to), + "error_msg": "Error in obtaining total history deals", + } res = await self._handler(api) return res - async def history_deals_get(self, date_from: datetime | float = None, date_to: datetime | float = None, - group: str = '', ticket: int = None, position: int = None) -> tuple[TradeDeal] | None: - kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value} + async def history_deals_get( + self, + date_from: datetime | float = None, + date_to: datetime | float = None, + group: str = "", + ticket: int = None, + position: int = None, + ) -> tuple[TradeDeal] | None: + kwargs = { + key: value + for key, value in ( + ("group", group), + ("ticket", ticket), + ("position", position), + ) + if value + } args = tuple(arg for arg in (date_from, date_to) if arg) - api = {'func': self._history_deals_get, 'args': args, 'kwargs': kwargs, - 'error_msg': 'Error in obtaining history deals'} + api = { + "func": self._history_deals_get, + "args": args, + "kwargs": kwargs, + "error_msg": "Error in obtaining history deals", + } res = await self._handler(api) return res diff --git a/src/aiomql/core/models.py b/src/aiomql/core/models.py index c51936e..7ba1a76 100644 --- a/src/aiomql/core/models.py +++ b/src/aiomql/core/models.py @@ -1,10 +1,29 @@ import MetaTrader5 as mt5 -from .constants import BookType, TradeAction, OrderType, OrderTime, OrderFilling, PositionReason, DealType, DealEntry, \ - DealReason, SymbolChartMode, SymbolTradeMode, SymbolCalcMode, SymbolOptionMode, SymbolOrderGTCMode, \ - SymbolOptionRight, \ - SymbolTradeExecution, SymbolSwapMode, DayOfWeek, AccountTradeMode, AccountStopOutMode, AccountMarginMode, \ - OrderReason +from .constants import ( + BookType, + TradeAction, + OrderType, + OrderTime, + OrderFilling, + PositionReason, + DealType, + DealEntry, + DealReason, + SymbolChartMode, + SymbolTradeMode, + SymbolCalcMode, + SymbolOptionMode, + SymbolOrderGTCMode, + SymbolOptionRight, + SymbolTradeExecution, + SymbolSwapMode, + DayOfWeek, + AccountTradeMode, + AccountStopOutMode, + AccountMarginMode, + OrderReason, +) from .base import Base @@ -49,8 +68,9 @@ class AccountInfo(Base): name: str company: str """ + login: int = 0 - server: str = '' + server: str = "" trade_mode: AccountTradeMode balance: float leverage: float @@ -108,6 +128,7 @@ class TerminalInfo(Base): data_path: str commondata_path: str """ + community_account: bool community_connection: bool connected: bool @@ -234,6 +255,7 @@ class SymbolInfo(Base): page: str path: str """ + custom: bool chart_mode: SymbolChartMode select: bool @@ -329,10 +351,13 @@ class SymbolInfo(Base): isin: str page: str path: str - name: str = '' + name: str = "" def __repr__(self): - return '%(class)s(name=%(name)s)' % {'class': self.__class__.__name__, 'name': self.name} + return "%(class)s(name=%(name)s)" % { + "class": self.__class__.__name__, + "name": self.name, + } def __str__(self): return self.name @@ -354,6 +379,7 @@ class BookInfo(Base): volume: float volume_dbl: float """ + type: BookType price: float volume: float @@ -389,6 +415,7 @@ class TradeOrder(Base): comment: str external_id: str """ + ticket: int time_setup: int time_setup_msc: int @@ -439,6 +466,7 @@ class TradeRequest(Base): deviation: int comment: str """ + action: TradeAction type: OrderType order: int @@ -474,6 +502,7 @@ class OrderCheckResult(Base): comment: str request: TradeRequest """ + retcode: int balance: float equity: float @@ -503,6 +532,7 @@ class OrderSendResult(Base): request_id: int retcode_external: int """ + retcode: int deal: int order: int @@ -517,6 +547,7 @@ class OrderSendResult(Base): profit: float = None loss: float = None + class TradePosition(Base): """Trade Position @@ -541,6 +572,7 @@ class TradePosition(Base): comment: str external_id: str """ + ticket: int time: int time_msc: int @@ -587,6 +619,7 @@ class TradeDeal(Base): comment: str external_id: str """ + ticket: int order: int time: int diff --git a/src/aiomql/core/task_queue.py b/src/aiomql/core/task_queue.py index b24bc4c..52c707b 100644 --- a/src/aiomql/core/task_queue.py +++ b/src/aiomql/core/task_queue.py @@ -8,7 +8,7 @@ logger = getLogger(__name__) class QueueItem: must_complete: bool - + def __init__(self, task_item: Callable | Coroutine, *args, **kwargs): self.task_item = task_item self.args = args @@ -31,13 +31,22 @@ class QueueItem: self.task_item(*self.args, **self.kwargs) except Exception as err: - logger.error(f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs {self.kwargs}") + logger.error( + f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs {self.kwargs}" + ) class TaskQueue: - def __init__(self, size: int = 0, workers: int = 10, timeout: int = None, queue: asyncio.Queue = None, - on_exit: Literal['cancel', 'complete_priority'] = 'complete_priority', - mode: Literal['finite', 'infinite'] = 'infinite', worker_timeout: int = 60): + def __init__( + self, + size: int = 0, + workers: int = 10, + timeout: int = None, + queue: asyncio.Queue = None, + on_exit: Literal["cancel", "complete_priority"] = "complete_priority", + mode: Literal["finite", "infinite"] = "infinite", + worker_timeout: int = 60, + ): self.queue = queue or asyncio.PriorityQueue(maxsize=size) self.workers = workers self.tasks = [] @@ -75,7 +84,9 @@ class TaskQueue: self.queue.task_done() self.priority_tasks.discard(item) - if self.stop and (self.on_exit == 'cancel' or len(self.priority_tasks) == 0): + if self.stop and ( + self.on_exit == "cancel" or len(self.priority_tasks) == 0 + ): self.cancel() break @@ -83,7 +94,7 @@ class TaskQueue: if self.stop: break - if self.mode == 'finite': + if self.mode == "finite": break sleep = QueueItem(asyncio.sleep, 1) @@ -92,44 +103,55 @@ class TaskQueue: except Exception as err: logger.error("%s: Error occurred in worker", err) - + async def run(self, timeout: int = 0): start = time.perf_counter() try: - self.tasks.extend(asyncio.create_task(self.worker()) for _ in range(self.workers)) + self.tasks.extend( + asyncio.create_task(self.worker()) for _ in range(self.workers) + ) timeout = timeout or self.timeout queue_task = asyncio.create_task(self.queue.join()) if timeout: - main_task = asyncio.create_task(asyncio.wait_for(queue_task, timeout=timeout)) + main_task = asyncio.create_task( + asyncio.wait_for(queue_task, timeout=timeout) + ) else: main_task = queue_task self.tasks.append(main_task) await main_task except TimeoutError: - logger.warning("Timed out after %d seconds, %d tasks remaining", - time.perf_counter() - start, self.queue.qsize()) + logger.warning( + "Timed out after %d seconds, %d tasks remaining", + time.perf_counter() - start, + self.queue.qsize(), + ) self.stop = True except asyncio.CancelledError as _: logger.warning("Main task cancelled") except Exception as err: - logger.warning("%s: An error occurred in %s.run", err, self.__class__.__name__) + logger.warning( + "%s: An error occurred in %s.run", err, self.__class__.__name__ + ) finally: await self.clean_up() def stop_queue(self): self.stop = True - self.on_exit = 'cancel' + self.on_exit = "cancel" self.cancel() async def clean_up(self): try: - if self.on_exit == 'complete_priority' and len(self.priority_tasks) > 0: - logger.warning(f'Completing {len(self.priority_tasks)} priority tasks...') + if self.on_exit == "complete_priority" and len(self.priority_tasks) > 0: + logger.warning( + f"Completing {len(self.priority_tasks)} priority tasks..." + ) queue_task = asyncio.create_task(self.queue.join()) self.tasks.append(queue_task) await queue_task @@ -139,7 +161,9 @@ class TaskQueue: ... except Exception as err: - logger.error(f"%s: Error occurred in %s.clean_up", err, self.__class__.__name__) + logger.error( + f"%s: Error occurred in %s.clean_up", err, self.__class__.__name__ + ) finally: self.cancel() diff --git a/src/aiomql/lib/__init__.py b/src/aiomql/lib/__init__.py index cbf24f7..369cf17 100644 --- a/src/aiomql/lib/__init__.py +++ b/src/aiomql/lib/__init__.py @@ -1,5 +1,4 @@ from .account import Account -from .backtest_runner import BackTestRunner from .bot import Bot from .candle import Candle, Candles from .executor import Executor diff --git a/src/aiomql/lib/account.py b/src/aiomql/lib/account.py index bd619df..a5da45d 100644 --- a/src/aiomql/lib/account.py +++ b/src/aiomql/lib/account.py @@ -18,11 +18,12 @@ class Account(_Base, AccountInfo): Notes: Other Account properties are defined in the AccountInfo class. """ + _instance: Self connected: bool - + def __new__(cls, *args, **kwargs): - if not hasattr(cls, '_instance'): + if not hasattr(cls, "_instance"): cls._instance = super().__new__(cls) cls._instance.connected = False return cls._instance @@ -40,7 +41,7 @@ class Account(_Base, AccountInfo): await self.mt5.initialize() self.connected = await self.mt5.login() if not self: - raise LoginError('Login failed') + raise LoginError("Login failed") return self async def __aexit__(self, exc_type, exc_val, exc_tb): diff --git a/src/aiomql/lib/backtest_runner.py b/src/aiomql/lib/backtest_runner.py deleted file mode 100644 index 538f4c8..0000000 --- a/src/aiomql/lib/backtest_runner.py +++ /dev/null @@ -1,35 +0,0 @@ -import asyncio -import signal -from logging import getLogger - -from ..core.event_manager import EventManager -from ..core.task_queue import TaskQueue -from ..contrib.backtesting.backtest_engine import BackTestEngine -from .strategy import Strategy -from ..core.meta_backtester import MetaBackTester - -logger = getLogger(__name__) - - -class BackTestRunner: - def __init__(self, *, strategies: list[Strategy], backtest_engine: BackTestEngine): - self.backtest_engine = backtest_engine - self.strategies = strategies or [] - self.event_manager = EventManager() - self.mt5 = MetaBackTester() - signal.signal(signal.SIGINT, self.event_manager.sigint_handler) - self.mt5.config.task_queue.worker_timeout = 5 - - async def run(self): - try: - await self.mt5.initialize() - await self.mt5.login() - strategies = [strategy for strategy in self.strategies if await strategy.symbol.initialize()] - self.event_manager.num_main_tasks = len(strategies) - tasks = [*[asyncio.create_task(strategy.run_strategy()) for strategy in strategies], - asyncio.create_task(self.event_manager.event_monitor())] - self.event_manager.add_tasks(*tasks) - tasks.append(asyncio.create_task(self.mt5.config.task_queue.run())) - await asyncio.gather(*tasks, return_exceptions=True) if len(strategies) else ... - except Exception as err: - logger.error(f"Error {err} occurred in StrategyTester") diff --git a/src/aiomql/lib/backtester.py b/src/aiomql/lib/backtester.py new file mode 100644 index 0000000..c893618 --- /dev/null +++ b/src/aiomql/lib/backtester.py @@ -0,0 +1,156 @@ +import asyncio +from typing import Type, Iterable, Callable, Coroutine +import logging + +from .executor import Executor +from ..core.config import Config +from ..core.backtesting.backtest_controller import BackTestController +from ..core.meta_backtester import MetaBackTester +from ..core.backtesting.backtest_engine import BackTestEngine +from .symbol import Symbol as Symbol +from .strategy import Strategy as Strategy + +logger = logging.getLogger(__name__) + + +class BackTester: + """The bot class. Create a bot instance to run your strategies. + + Attributes: + executor: The default thread executor. + config (Config): Config instance + mt (MetaTrader): MetaTrader instance + + """ + + config: Config + executor: Executor + mt: MetaBackTester + backtest_engine: BackTestEngine + backtest_controller: BackTestController + strategies: list[Strategy] + + def __init__(self, *, backtest_engine: BackTestEngine): + self.config = Config() + self.executor = Executor() + self.mt = MetaBackTester() + self.backtest_engine = backtest_engine + self.backtest_controller = BackTestController() + self.strategies = [] + + async def initialize(self): + """Prepares the bot by signing in to the trading account and initializing the symbols for the trading session. + Starts the global task queue. + + Raises: + SystemExit if sign in was not successful + """ + try: + await self.mt.initialize() + login = await self.mt.login() + if not login: + logger.critical(f"Unable to sign in to MetaTrder 5 Terminal") + raise Exception("Unable to sign in to MetaTrader 5 Terminal") + logger.info("Login Successful") + await self.init_strategies() + self.config.task_queue.worker_timeout = 5 + self.add_coroutine( + coroutine=self.config.task_queue.run, on_separate_thread=True + ) + self.add_coroutine(coroutine=self.executor.exit) + self.add_coroutine( + coroutine=self.backtest_controller.control, on_separate_thread=True + ) + if (strategies := len(self.executor.strategy_runners)) == 0: + logger.warning( + "No strategies were added to the backtester. Exiting ..." + ) + raise Exception("No strategies added to the backtester") + parties = strategies + 1 + self.backtest_controller.set_parties(parties=parties) + except Exception as err: + logger.error(f"{err}. Backtester initialization failed") + raise SystemExit + + def add_coroutine( + self, + *, + coroutine: Callable[..., ...] | Coroutine, + on_separate_thread=False, + **kwargs, + ): + """Add a coroutine to the executor. + + Args: + coroutine (Coroutine): A coroutine to be executed + on_separate_thread (bool): Run the coroutine + **kwargs (dict): keyword arguments for the coroutine + + Returns: + + """ + self.executor.add_coroutine( + coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread + ) + + def execute(self): + """Execute the bot.""" + asyncio.run(self.start()) + + async def start(self): + """Initialize the bot and execute it. Similar to calling `execute` method but is a coroutine.""" + await self.initialize() + await self.executor.execute() + + def add_strategy(self, *, strategy: Strategy): + """Add a strategy to the list of strategies. + An added strategy will only run if it's symbol was successfully initialized and it is added to the executor. + + Args: + strategy (Strategy): A Strategy instance to run on bot + + Notes: + Make sure the symbol has been added to the market + """ + self.strategies.append(strategy) + + def add_strategies(self, *, strategies: Iterable[Strategy]): + """Add multiple strategies at the same time + + Args: + strategies: A list of strategies + """ + [self.add_strategy(strategy=strategy) for strategy in strategies] + + def add_strategy_all( + self, + *, + strategy: Type[Strategy], + params: dict | None = None, + symbols: list[Symbol] = None, + **kwargs, + ): + """Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments. + + Keyword Args: + strategy (Strategy): Strategy class + params (dict): A dictionary of parameters for the strategy + symbols (list): A list of symbols to run the strategy on + **kwargs: Additional keyword arguments for the strategy + """ + [ + self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs)) + for symbol in symbols + ] + + async def init_strategy(self, *, strategy: Strategy) -> bool: + """Initialize a single strategy. This method is called internally by the bot.""" + res = await strategy.symbol.initialize() + if res: + self.executor.add_strategy(strategy=strategy) + return res + + async def init_strategies(self): + """Initialize the symbols for the current trading session. This method is called internally by the bot.""" + tasks = [self.init_strategy(strategy=strategy) for strategy in self.strategies] + await asyncio.gather(*tasks) diff --git a/src/aiomql/lib/bot.py b/src/aiomql/lib/bot.py index a7da8c1..d101bc7 100644 --- a/src/aiomql/lib/bot.py +++ b/src/aiomql/lib/bot.py @@ -21,17 +21,20 @@ class Bot: mt (MetaTrader): MetaTrader instance """ + config: Config executor: Executor mt: MetaTrader + strategies: list[Strategy] def __init__(self): self.config = Config(bot=self) self.executor = Executor() self.mt = MetaTrader() + self.strategies = [] @classmethod - def run_bots(cls, funcs: dict[Callable: dict] = None, num_workers: int = None): + def run_bots(cls, funcs: dict[Callable:dict] = None, num_workers: int = None): """Run multiple scripts or bots in parallel with different accounts. Args: @@ -54,14 +57,19 @@ class Bot: await self.mt.initialize() login = await self.mt.login() if not login: - logger.critical(f"Unable to sign in to MetaTrder 5 Terminal") - raise SystemExit + logger.critical("Unable to sign in to MetaTrder 5 Terminal") + raise Exception("Unable to sign in to MetaTrader 5 Terminal") logger.info("Login Successful") await self.init_strategies() - self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True) + self.add_coroutine( + coroutine=self.config.task_queue.run, on_separate_thread=True + ) self.add_coroutine(coroutine=self.executor.exit) + + if len(self.executor.strategy_runners) == 0: + logger.warning("No strategies were added to the bot") except Exception as err: - logger.error(f"{err}. Bot initialization failed") + logger.error("%s: Bot initialization failed", err) raise SystemExit def add_function(self, *, function: Callable[..., ...], **kwargs: dict): @@ -73,7 +81,13 @@ class Bot: """ self.executor.add_function(function=function, kwargs=kwargs) - def add_coroutine(self, *, coroutine: Callable[..., ...] | Coroutine, on_separate_thread=False, **kwargs): + def add_coroutine( + self, + *, + coroutine: Callable[..., ...] | Coroutine, + on_separate_thread=False, + **kwargs, + ): """Add a coroutine to the executor. Args: @@ -84,7 +98,9 @@ class Bot: Returns: """ - self.executor.add_coroutine(coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread) + self.executor.add_coroutine( + coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread + ) def execute(self): """Execute the bot.""" @@ -96,7 +112,8 @@ class Bot: await self.executor.execute() def add_strategy(self, *, strategy: Strategy): - """Add a strategy to the executor. An added strategy will only run if it's symbol was successfully initialized. + """Add a strategy to the list of strategies. + An added strategy will only run if it's symbol was successfully initialized and it is added to the executor. Args: strategy (Strategy): A Strategy instance to run on bot @@ -104,7 +121,7 @@ class Bot: Notes: Make sure the symbol has been added to the market """ - self.executor.add_strategy(strategy=strategy) + self.strategies.append(strategy) def add_strategies(self, *, strategies: Iterable[Strategy]): """Add multiple strategies at the same time @@ -114,8 +131,14 @@ class Bot: """ [self.add_strategy(strategy=strategy) for strategy in strategies] - def add_strategy_all(self, *, strategy: Type[Strategy], params: dict | None = None, - symbols: list[Symbol] = None, **kwargs): + def add_strategy_all( + self, + *, + strategy: Type[Strategy], + params: dict | None = None, + symbols: list[Symbol] = None, + **kwargs, + ): """Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments. Keyword Args: @@ -129,17 +152,14 @@ class Bot: for symbol in symbols ] - @staticmethod - async def init_strategy(*, strategy: Strategy) -> tuple[bool, Strategy]: + async def init_strategy(self, *, strategy: Strategy) -> bool: """Initialize a single strategy. This method is called internally by the bot.""" res = await strategy.symbol.initialize() - return res, strategy + if res: + self.executor.add_strategy(strategy=strategy) + return res async def init_strategies(self): """Initialize the symbols for the current trading session. This method is called internally by the bot.""" - tasks = [self.init_strategy(strategy=strategy) for strategy in self.executor.strategy_runners] - for task in asyncio.as_completed(tasks): - res = await task - if not res[0]: - logger.warning(f"Failed to initialize symbol {res[1].symbol}") - self.executor.strategy_runners.remove(res[1]) + tasks = [self.init_strategy(strategy=strategy) for strategy in self.strategies] + await asyncio.gather(*tasks) diff --git a/src/aiomql/lib/candle.py b/src/aiomql/lib/candle.py index 26fc8ff..f88bd2a 100644 --- a/src/aiomql/lib/candle.py +++ b/src/aiomql/lib/candle.py @@ -28,6 +28,7 @@ class Candle: spread (float): Spread Index (int): Custom attribute representing the position of the candle in a sequence. """ + time: float open: float high: float @@ -45,19 +46,30 @@ class Candle: Keyword Args: **kwargs: Candle attributes and values as keyword arguments. """ - if not all(i in kwargs for i in ['open', 'high', 'low', 'close']): - raise ValueError("Candle must be instantiated with open, high, low and close prices") - self.time = kwargs.pop('time', time.monotonic_ns()) - self.Index = kwargs.pop('Index', 0) - self.real_volume = kwargs.pop('real_volume', 0) - self.spread = kwargs.pop('spread', 0) - self.tick_volume = kwargs.pop('tick_volume', 0) + if not all(i in kwargs for i in ["open", "high", "low", "close"]): + raise ValueError( + "Candle must be instantiated with open, high, low and close prices" + ) + self.time = kwargs.pop("time", time.monotonic_ns()) + self.Index = kwargs.pop("Index", 0) + self.real_volume = kwargs.pop("real_volume", 0) + self.spread = kwargs.pop("spread", 0) + self.tick_volume = kwargs.pop("tick_volume", 0) self.set_attributes(**kwargs) def __repr__(self): - return ("%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)" - % {"class": self.__class__.__name__, "open": self.open, "high": self.high, - "low": self.low, "close": self.close, "time": self.time, 'Index': self.Index}) + return ( + "%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)" + % { + "class": self.__class__.__name__, + "open": self.open, + "high": self.high, + "low": self.low, + "close": self.close, + "time": self.time, + "Index": self.Index, + } + ) def __eq__(self, other: Self): return self.time == other.time @@ -146,6 +158,7 @@ class Candles: The candle class can be customized by subclassing the Candle class and passing the subclass as the candle keyword argument, or defining it on the class body as a class attribute. """ + Index: Series time: Series open: Series @@ -159,7 +172,13 @@ class Candles: timeframe: TimeFrame _data: DataFrame - def __init__(self, *, data: DataFrame | Self | Iterable, flip=False, candle_class: Candle = None): + def __init__( + self, + *, + data: DataFrame | Self | Iterable, + flip=False, + candle_class: Candle = None, + ): """A container class of Candle objects in chronological order. Args: @@ -198,7 +217,7 @@ class Candles: return cls(data=data) elif isinstance(index, str): - if index == 'Index': + if index == "Index": return Series(self._data.index) return self._data[index] @@ -217,9 +236,11 @@ class Candles: if item in self._data.columns: return self._data[item] - if item == 'Index': + if item == "Index": return Series(self._data.index) - raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}") + raise AttributeError( + f"Attribute {item} not defined on class {self.__class__.__name__}" + ) def __iter__(self): return (self.Candle(**row._asdict()) for row in self._data.itertuples()) @@ -280,11 +301,21 @@ class Candles: """ columns = columns or [] data = self._data[-count:] - data.index = pd.to_datetime(data['time'], unit='s') + data.index = pd.to_datetime(data["time"], unit="s") return mplt.make_addplot(data[columns], **kwargs) - def visualize(self, *, count: int = 50, _type='candle', savefig: str | dict = None, addplot: dict = None, - style: str = 'charles', ylabel: str = 'Price', title: str = 'Chart', **kwargs): + def visualize( + self, + *, + count: int = 50, + _type="candle", + savefig: str | dict = None, + addplot: dict = None, + style: str = "charles", + ylabel: str = "Price", + title: str = "Chart", + **kwargs, + ): """Visualize the candles using the mplfinance library. Args: count (int): The number of candles to visualize, counting from behind, i.e the most recent candles. @@ -298,8 +329,18 @@ class Candles: title (str): The title of the chart. Defaults to 'Chart'. kwargs: valid kwargs for the plot function. """ - kwargs |= {key: arg for key, arg in (('savefig', savefig), ('addplot', addplot), ('style', style), - ('ylabel', ylabel), ('title', title), ('type', _type)) if arg} + kwargs |= { + key: arg + for key, arg in ( + ("savefig", savefig), + ("addplot", addplot), + ("style", style), + ("ylabel", ylabel), + ("title", title), + ("type", _type), + ) + if arg + } data = self._data[-count:] - data.index = pd.to_datetime(data['time'], unit='s') + data.index = pd.to_datetime(data["time"], unit="s") mplt.plot(data, **kwargs) diff --git a/src/aiomql/lib/executor.py b/src/aiomql/lib/executor.py index c6de372..9d6e6f6 100644 --- a/src/aiomql/lib/executor.py +++ b/src/aiomql/lib/executor.py @@ -19,6 +19,7 @@ class Executor: coroutines (list[Coroutine]): A list of coroutines to run in the executor functions (dict[Callable, dict]): A dictionary of functions to run in the executor """ + executor: ThreadPoolExecutor tasks: list[asyncio.Task] config: Config @@ -27,20 +28,27 @@ class Executor: self.strategy_runners: list[Strategy] = [] self.coroutines: list[Coroutine] = [] self.coroutine_threads: list[Coroutine] = [] - self.functions: dict[Callable: dict] = {} + self.functions: dict[Callable:dict] = {} self.tasks = [] - self.no_of_running_strategies = 0 self.config = Config() - self.timeout = None # Timeout for the executor. For testing purposes only + self.timeout = None # Timeout for the executor. For testing purposes only def add_function(self, *, function: Callable, kwargs: dict = None): kwargs = kwargs or {} self.functions[function] = kwargs - def add_coroutine(self, *, coroutine: Callable | Coroutine, kwargs: dict = None, on_separate_thread=False): + def add_coroutine( + self, + *, + coroutine: Callable | Coroutine, + kwargs: dict = None, + on_separate_thread=False, + ): kwargs = kwargs or {} coroutine = coroutine(**kwargs) - self.coroutines.append(coroutine) if on_separate_thread is False else self.coroutine_threads.append(coroutine) + self.coroutines.append( + coroutine + ) if on_separate_thread is False else self.coroutine_threads.append(coroutine) def add_strategies(self, *, strategies: tuple[Strategy]): """Add multiple strategies at once @@ -61,7 +69,6 @@ class Executor: async def create_strategy_task(self, strategy: Strategy): task = asyncio.create_task(strategy.run_strategy()) self.tasks.append(task) - self.no_of_running_strategies += 1 await task def run_strategy(self, strategy: Strategy): @@ -79,7 +86,9 @@ class Executor: async def create_coroutines_task(self): """""" - task = asyncio.create_task(asyncio.gather(*self.coroutines, return_exceptions=True)) + task = asyncio.create_task( + asyncio.gather(*self.coroutines, return_exceptions=True) + ) self.tasks.append(task) await task @@ -101,17 +110,18 @@ class Executor: try: function(**kwargs) except Exception as err: - logger.error(f'Error: {err}. Unable to run function: {function.__name__}') + logger.error(f"Error: {err}. Unable to run function: {function.__name__}") async def exit(self): """Shutdown the executor""" + start = asyncio.get_event_loop().time() try: while self.config.shutdown is False and self.config.force_shutdown is False: - if self.timeout is not None and self.no_of_running_strategies == len(self.strategy_runners): - self.timeout -= 1 - if self.timeout == 0: - self.config.shutdown = True - continue + if self.timeout is not None and self.timeout < ( + asyncio.get_event_loop().time() - start + ): + self.config.shutdown = True + for strategy in self.strategy_runners: strategy.running = False for task in self.tasks: @@ -131,11 +141,25 @@ class Executor: Notes: No matter the number specified, the executor will always use a minimum of 5 workers. """ - workers_ = sum([len(self.strategy_runners), len(self.functions), len(self.coroutines)]) + workers_ = ( + len(self.strategy_runners) + + len(self.functions) + + len(self.coroutine_threads) + + 2 + ) workers = max(workers, workers_) with ThreadPoolExecutor(max_workers=workers) as executor: 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() + ] + [ + self.executor.submit(self.run_coroutine_task, coroutine) + for coroutine in self.coroutine_threads + ] self.executor.submit(self.run_coroutine_tasks) - [self.executor.submit(self.run_coroutine_task, coroutine) for coroutine in self.coroutine_threads] - [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()] diff --git a/src/aiomql/lib/history.py b/src/aiomql/lib/history.py index f73fbfe..3bd7cb3 100644 --- a/src/aiomql/lib/history.py +++ b/src/aiomql/lib/history.py @@ -27,10 +27,18 @@ class History: mt5 (MetaTrader): MetaTrader instance config (Config): Config instance """ + mt5: MetaTrader | MetaBackTester config: Config - def __init__(self, *, date_from: datetime | int, date_to: datetime | int, group: str = '', use_utc: bool = True): + def __init__( + self, + *, + date_from: datetime | int, + date_to: datetime | int, + group: str = "", + use_utc: bool = True, + ): """ Args: date_from (datetime, int): Date the orders are requested from. Set by the 'datetime' object or as a @@ -42,9 +50,17 @@ class History: group (str): Filter for selecting history by symbols. Defaults to an empty string """ self.config = Config() - self.mt5 = MetaTrader() if self.config.mode != 'backtest' else MetaBackTester() - date_from = date_from if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from) - date_to = date_to if isinstance(date_to, datetime) else datetime.fromtimestamp(date_to) + self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester() + date_from = ( + date_from + if isinstance(date_from, datetime) + else datetime.fromtimestamp(date_from) + ) + date_to = ( + date_to + if isinstance(date_to, datetime) + else datetime.fromtimestamp(date_to) + ) self.date_from = date_from.astimezone(pytz.UTC) if use_utc else date_from self.date_to = date_to.astimezone(pytz.UTC) if use_utc else date_to self.group = group @@ -55,7 +71,9 @@ class History: async def initialize(self): """Get history deals and orders""" - deals, orders = await asyncio.gather(self.get_deals(), self.get_orders(), return_exceptions=True) + deals, orders = await asyncio.gather( + self.get_deals(), self.get_orders(), return_exceptions=True + ) self.deals = deals if isinstance(deals, tuple) else () self.orders = orders if isinstance(orders, tuple) else () self.total_deals = len(self.deals) @@ -67,10 +85,12 @@ class History: Returns: tuple[TradeDeal]: A list of trade deals """ - deals = await self.mt5.history_deals_get(date_from=self.date_from, date_to=self.date_to, group=self.group) + deals = await self.mt5.history_deals_get( + date_from=self.date_from, date_to=self.date_to, group=self.group + ) if deals is not None: return tuple(TradeDeal(**deal._asdict()) for deal in deals) - logger.warning(f'Failed to get deals') + logger.warning(f"Failed to get deals") return tuple() def get_deals_by_ticket(self, *, ticket: int) -> tuple[TradeDeal, ...]: @@ -83,7 +103,12 @@ class History: Returns: tuple[TradeDeal]: A tuple of all deals with the order ticket """ - return tuple(sorted((deal for deal in self.deals if deal.order == ticket), key=lambda x: x.time_msc)) + return tuple( + sorted( + (deal for deal in self.deals if deal.order == ticket), + key=lambda x: x.time_msc, + ) + ) def get_deals_by_position(self, *, position: int = None) -> tuple[TradeDeal, ...]: """ @@ -94,7 +119,12 @@ class History: Returns: tuple[TradeDeal]: A tuple of all deals with the position ticket """ - return tuple(sorted((deal for deal in self.deals if deal.position_id == position), key=lambda x: x.time_msc)) + return tuple( + sorted( + (deal for deal in self.deals if deal.position_id == position), + key=lambda x: x.time_msc, + ) + ) @backoff_decorator async def get_orders(self) -> tuple[TradeOrder, ...]: @@ -103,25 +133,37 @@ class History: Returns: list[TradeOrder]: A list of trade orders """ - orders = await self.mt5.history_orders_get(date_from=self.date_from, date_to=self.date_to, group=self.group) + orders = await self.mt5.history_orders_get( + date_from=self.date_from, date_to=self.date_to, group=self.group + ) if orders is not None: return tuple(TradeOrder(**order._asdict()) for order in orders) - logger.warning(f'Failed to get orders') + logger.warning(f"Failed to get orders") return tuple() def get_orders_by_ticket(self, *, ticket: int) -> tuple[TradeOrder, ...]: """filter orders by ticket""" - return tuple(sorted((order for order in self.orders if order.ticket == ticket), key=lambda x: x.time_done_msc)) - + return tuple( + sorted( + (order for order in self.orders if order.ticket == ticket), + key=lambda x: x.time_done_msc, + ) + ) def get_orders_by_position(self, *, position: int) -> tuple[TradeOrder, ...]: - """ filter orders by position""" - return tuple(sorted((order for order in self.orders if order.position_id == position), - key=lambda x: x.time_done_msc)) + """filter orders by position""" + return tuple( + sorted( + (order for order in self.orders if order.position_id == position), + key=lambda x: x.time_done_msc, + ) + ) - async def track_order(self, *, position: int = None, end_time: datetime = None) -> DataFrame: + async def track_order( + self, *, position: int = None, end_time: datetime = None + ) -> DataFrame: """ Track an order from the time it was opened to the time it was closed or any given time. The tracking is done by getting the ticks @@ -142,16 +184,28 @@ class History: open_order = orders[0] open_deal = deals[0] close_deal = deals[-1] - time_done = datetime.timestamp(end_time) if end_time is not None else close_deal.time + time_done = ( + datetime.timestamp(end_time) if end_time is not None else close_deal.time + ) time_done_msc = int(time_done * 1000) - open_order.set_attributes(time_done_msc=time_done_msc, time_done=time_done, price_open=open_deal.price) - ticks = await self.mt5.copy_ticks_range(open_order.symbol, open_order.time_setup, open_order.time_done, - CopyTicks.ALL) + open_order.set_attributes( + time_done_msc=time_done_msc, time_done=time_done, price_open=open_deal.price + ) + ticks = await self.mt5.copy_ticks_range( + open_order.symbol, + open_order.time_setup, + open_order.time_done, + CopyTicks.ALL, + ) data = pd.DataFrame(ticks) - profit = lambda x: self.mt5._order_calc_profit(open_order.type, open_order.symbol, open_order.volume_initial, - open_order.price_open, - x.ask if open_order.type == OrderType.BUY else x.bid) - data['profits'] = data.apply(profit, axis=1) - data['time'] = pd.to_datetime(data['time'], unit='s') - data.set_index('time', inplace=True) + profit = lambda x: self.mt5._order_calc_profit( + open_order.type, + open_order.symbol, + open_order.volume_initial, + open_order.price_open, + x.ask if open_order.type == OrderType.BUY else x.bid, + ) + data["profits"] = data.apply(profit, axis=1) + data["time"] = pd.to_datetime(data["time"], unit="s") + data.set_index("time", inplace=True) return data diff --git a/src/aiomql/lib/order.py b/src/aiomql/lib/order.py index 53c22ff..eef5191 100644 --- a/src/aiomql/lib/order.py +++ b/src/aiomql/lib/order.py @@ -25,7 +25,12 @@ class Order(_Base, TradeRequest): type_time (OrderTime.DAY): Order time type_filling (OrderFilling.FOK): Order filling """ - kwargs = {'action': TradeAction.DEAL, 'type_time': OrderTime.DAY, 'type_filling': OrderFilling.FOK, **kwargs} + kwargs = { + "action": TradeAction.DEAL, + "type_time": OrderTime.DAY, + "type_filling": OrderFilling.FOK, + **kwargs, + } super().__init__(**kwargs) async def orders_total(self): @@ -52,7 +57,9 @@ class Order(_Base, TradeRequest): return TradeOrder(**order_._asdict()) return order - async def get_orders(self, *, ticket: int = 0, symbol: str = '', group: str = '') -> tuple[TradeOrder, ...]: + async def get_orders( + self, *, ticket: int = 0, symbol: str = "", group: str = "" + ) -> tuple[TradeOrder, ...]: """Get the list of active pending orders for the current symbol. Keyword Args: @@ -80,7 +87,7 @@ class Order(_Base, TradeRequest): req = self.request | kwargs res = await self.mt5.order_check(req) if res is None: - raise OrderError(f'Order check failed for {self.symbol}') + raise OrderError(f"Order check failed for {self.symbol}") return OrderCheckResult(**res._asdict()) @backoff_decorator @@ -95,7 +102,7 @@ class Order(_Base, TradeRequest): """ res = await self.mt5.order_send(self.request) if res is None: - raise OrderError(f'Failed to send order {self.symbol}') + raise OrderError(f"Failed to send order {self.symbol}") return OrderSendResult(**res._asdict()) @error_handler(log_error_msg=False) @@ -105,7 +112,9 @@ class Order(_Base, TradeRequest): Returns: float: Returns float value if successful """ - res = await self.mt5.order_calc_margin(self.type, self.symbol, self.volume, self.price) + res = await self.mt5.order_calc_margin( + self.type, self.symbol, self.volume, self.price + ) return res @error_handler(response=0, log_error_msg=False) @@ -116,8 +125,16 @@ class Order(_Base, TradeRequest): float: Returns float value if successful None: If not successful """ - action, symbol, volume, price_open, price_close = self.type, self.symbol, self.volume, self.price, self.tp - res = await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close) + action, symbol, volume, price_open, price_close = ( + self.type, + self.symbol, + self.volume, + self.price, + self.tp, + ) + res = await self.mt5.order_calc_profit( + action, symbol, volume, price_open, price_close + ) return res @error_handler(response=0, log_error_msg=False) @@ -128,11 +145,23 @@ class Order(_Base, TradeRequest): float: Returns float value if successful None: If not successful """ - action, symbol, volume, price_open, price_close = self.type, self.symbol, self.volume, self.price, self.sl - res = await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close) + action, symbol, volume, price_open, price_close = ( + self.type, + self.symbol, + self.volume, + self.price, + self.sl, + ) + res = await self.mt5.order_calc_profit( + action, symbol, volume, price_open, price_close + ) return res @property def request(self) -> dict: """Return the order request as a dictionary.""" - return {key: value for key, value in self.dict.items() if key in self.mt5.TradeRequest.__match_args__} + return { + key: value + for key, value in self.dict.items() + if key in self.mt5.TradeRequest.__match_args__ + } diff --git a/src/aiomql/lib/positions.py b/src/aiomql/lib/positions.py index 9e42450..9ee5de9 100644 --- a/src/aiomql/lib/positions.py +++ b/src/aiomql/lib/positions.py @@ -4,7 +4,7 @@ from logging import getLogger from ..core.meta_trader import MetaTrader from ..core.models import TradePosition, OrderSendResult -from ..core.constants import OrderType, TradeAction +from ..core.constants import OrderType, TradeAction from ..core.config import Config from .._utils import backoff_decorator from ..core.meta_backtester import MetaBackTester @@ -20,13 +20,14 @@ class Positions: Attributes: mt5 (MetaTrader): MetaTrader instance. """ + mt5: MetaTrader | MetaBackTester positions: tuple[TradePosition, ...] def __init__(self): """Get Open Positions""" self.config = Config() - self.mt5 = MetaTrader() if self.config.mode != 'backtest' else MetaBackTester() + self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester() self.positions = () @backoff_decorator @@ -40,7 +41,7 @@ class Positions: if positions is not None: self.positions = tuple(TradePosition(**pos._asdict()) for pos in positions) return self.positions - logger.warning('Failed to get open positions') + logger.warning("Failed to get open positions") return () async def get_position_by_ticket(self, *, ticket: int) -> TradePosition | None: @@ -69,7 +70,9 @@ class Positions: return tuple(TradePosition(**pos._asdict()) for pos in (positions or ())) @staticmethod - async def close(*, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType) -> OrderSendResult: + async def close( + *, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType + ) -> OrderSendResult: """Close an open position for the trading account using the ticket and other parameters. Args: @@ -79,8 +82,14 @@ class Positions: volume (float): Volume to close. order_type (OrderType): Order type. """ - order = Order(action=TradeAction.DEAL, price=price, position=ticket, symbol=symbol, volume=volume, - type=order_type.opposite) + order = Order( + action=TradeAction.DEAL, + price=price, + position=ticket, + symbol=symbol, + volume=volume, + type=order_type.opposite, + ) return await order.send() async def close_position_by_ticket(self, *, ticket: int) -> OrderSendResult | None: @@ -88,15 +97,27 @@ class Positions: position = await self.get_position_by_ticket(ticket=ticket) if position is None: return None - order = Order(position=position.ticket, symbol=position.symbol, volume=position.volume, - type=position.type.opposite, price=position.price_current, action=TradeAction.DEAL) + order = Order( + position=position.ticket, + symbol=position.symbol, + volume=position.volume, + type=position.type.opposite, + price=position.price_current, + action=TradeAction.DEAL, + ) return await order.send() @staticmethod async def close_position(*, position: TradePosition): """Close an open position for the trading account. Using a position object.""" - order = Order(position=position.ticket, symbol=position.symbol, volume=position.volume, - type=position.type.opposite, price=position.price_current, action=TradeAction.DEAL) + order = Order( + position=position.ticket, + symbol=position.symbol, + volume=position.volume, + type=position.type.opposite, + price=position.price_current, + action=TradeAction.DEAL, + ) return await order.send() async def close_all(self) -> int: @@ -106,6 +127,14 @@ class Positions: int: Return number of positions closed. """ positions = self.positions or await self.get_positions() - results = await asyncio.gather(*(self.close_position(position=position) for position in positions), - return_exceptions=True) - return len([res for res in results if (isinstance(res, OrderSendResult) and res.retcode == 10009)]) + results = await asyncio.gather( + *(self.close_position(position=position) for position in positions), + return_exceptions=True, + ) + return len( + [ + res + for res in results + if (isinstance(res, OrderSendResult) and res.retcode == 10009) + ] + ) diff --git a/src/aiomql/lib/ram.py b/src/aiomql/lib/ram.py index fe9eaf7..9ea22a6 100644 --- a/src/aiomql/lib/ram.py +++ b/src/aiomql/lib/ram.py @@ -27,13 +27,13 @@ class RAM: """ self.account = Account() self.positions = Positions() - self.risk_to_reward = kwargs.get('risk_to_reward', 2) - self.risk = kwargs.get('risk', 1) - self.min_amount = kwargs.get('min_amount', 0) - self.max_amount = kwargs.get('max_amount', 0) - self.loss_limit = kwargs.get('loss_limit', 3) - self.open_limit = kwargs.get('open_limit', 3) - self.fixed_amount = kwargs.get('fixed_amount', None) + self.risk_to_reward = kwargs.get("risk_to_reward", 2) + self.risk = kwargs.get("risk", 1) + self.min_amount = kwargs.get("min_amount", 0) + self.max_amount = kwargs.get("max_amount", 0) + self.loss_limit = kwargs.get("loss_limit", 3) + self.open_limit = kwargs.get("open_limit", 3) + self.fixed_amount = kwargs.get("fixed_amount", None) async def get_amount(self) -> float: """Calculate the amount to risk per trade as a percentage of margin_free. @@ -44,7 +44,7 @@ class RAM: if self.fixed_amount: return self.fixed_amount await self.account.refresh() - amount = self.account.margin_free * (self.risk/100) + amount = self.account.margin_free * (self.risk / 100) if self.min_amount and self.max_amount: return max(self.min_amount, min(self.max_amount, amount)) return amount diff --git a/src/aiomql/lib/result.py b/src/aiomql/lib/result.py index 6c4a154..2882885 100644 --- a/src/aiomql/lib/result.py +++ b/src/aiomql/lib/result.py @@ -2,7 +2,7 @@ import csv import json from logging import getLogger from typing import Iterable, Literal -from asyncio import Lock +from threading import Lock from ..core.config import Config from ..core.models import OrderSendResult @@ -18,9 +18,13 @@ class Result: config (Config): The configuration object name: Any desired name for the result file object """ - config: Config - def __init__(self, *, result: OrderSendResult, parameters: dict = None, name: str = ''): + config: Config + lock = Lock() + + def __init__( + self, *, result: OrderSendResult, parameters: dict = None, name: str = "" + ): """ Prepare result data Args: @@ -31,35 +35,37 @@ class Result: self.config = Config() self.parameters = parameters or {} self.result = result - self.name = name or self.parameters.get('name', 'Trades') - self.lock = Lock() + self.name = name or self.parameters.get("name", "Trades") def get_data(self) -> dict: - res = self.result.get_dict(exclude={'retcode', 'comment', 'retcode_external', 'request_id', 'request'}) - return self.parameters | res | {'actual_profit': 0, 'closed': False, 'win': False} + res = self.result.get_dict( + exclude={"retcode", "comment", "retcode_external", "request_id", "request"} + ) + return ( + self.parameters | res | {"actual_profit": 0, "closed": False, "win": False} + ) - async def save(self, *, trade_record_mode: Literal['csv', 'json'] = None): + async def save(self, *, trade_record_mode: Literal["csv", "json"] = None): """Record trade results as a csv or json file Args: trade_record_mode (Literal['csv'|'json']): Mode of saving trade records """ - trade_record_mode = trade_record_mode or self.config.trade_record_mode - if trade_record_mode == 'csv': - await self.to_csv() - elif trade_record_mode == 'json': - await self.to_json() - else: - logger.error(f"Invalid trade record mode: {trade_record_mode}") + with self.lock: + trade_record_mode = trade_record_mode or self.config.trade_record_mode + if trade_record_mode == "csv": + await self.to_csv() + elif trade_record_mode == "json": + await self.to_json() + else: + logger.error(f"Invalid trade record mode: {trade_record_mode}") async def to_csv(self): - """Record trade results and associated parameters as a csv file - """ - await self.lock.acquire() + """Record trade results and associated parameters as a csv file""" try: data = self.get_data() file = self.config.records_dir / f"{self.name}.csv" file.touch(exist_ok=True) if not file.exists() else ... - read_file = file.open('r', newline='') + read_file = file.open("r", newline="") reader: Iterable[dict] = csv.DictReader(read_file) rows: list[dict] = [] headers = set() @@ -67,46 +73,39 @@ class Result: rows.append(data) headers.update(data.keys()) read_file.close() - with file.open('w', newline='') as write_file: - writer = csv.DictWriter(write_file, fieldnames=headers, restval=None, extrasaction='ignore') + with file.open("w", newline="") as write_file: + writer = csv.DictWriter( + write_file, fieldnames=headers, restval=None, extrasaction="ignore" + ) writer.writeheader() writer.writerows(rows) except Exception as err: - logger.error(f'Unable to save to csv: {err}') - - finally: - self.lock.release() + logger.error(f"Unable to save to csv: {err}") @staticmethod def serialize(value) -> str: - """Serialize the trade records and strategy parameters - """ + """Serialize the trade records and strategy parameters""" try: return str(value) except (ValueError, TypeError) as _: return "" async def to_json(self): - """Save trades and strategy parameters in a json file - """ - await self.lock.acquire() + """Save trades and strategy parameters in a json file""" try: file = self.config.records_dir / f"{self.name}.json" data = self.get_data() if not file.exists(): file.touch() - with file.open('w') as fh: + with file.open("w") as fh: json.dump([], fh, indent=2) - with file.open('r') as fh: + with file.open("r") as fh: rows = json.load(fh) rows.append(data) - with file.open('w') as fh: + with file.open("w") as fh: json.dump(rows, fh, indent=2, skipkeys=True, default=self.serialize) except Exception as err: logger.error(f"Unable to save as json file: {err}") - - finally: - self.lock.release() diff --git a/src/aiomql/lib/sessions.py b/src/aiomql/lib/sessions.py index eb9e013..3f396d0 100644 --- a/src/aiomql/lib/sessions.py +++ b/src/aiomql/lib/sessions.py @@ -5,7 +5,7 @@ from logging import getLogger from ..core.models import OrderSendResult, TradePosition from ..core.config import Config -from ..core.event_manager import EventManager +from ..core.backtesting.backtest_controller import BackTestController from .positions import Positions logger = getLogger(__name__) @@ -23,17 +23,21 @@ def delta(obj: time) -> timedelta: Args: obj (datetime.time): A datetime.time object. """ - return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond) + return timedelta( + hours=obj.hour, + minutes=obj.minute, + seconds=obj.second, + microseconds=obj.microsecond, + ) async def backtest_sleep(secs): """An async sleep function for use during backtesting.""" - em = EventManager() + btc = BackTestController() config = Config() sleep = config.backtest_engine.cursor.time + secs - async with em.condition: - while sleep > config.backtest_engine.cursor.time: - await em.wait() + while sleep > config.backtest_engine.cursor.time: + btc.wait() class Session: @@ -48,10 +52,20 @@ class Session: custom_end (Callable): A custom function to call when the session ends. Default is None. name (str): A name for the session. Default is a combination of start and end. """ - def __init__(self, *, start: int | time, end: int | time, - on_start: Literal['close_all', 'close_win', 'close_loss', 'custom_start'] = None, - on_end: Literal['close_all', 'close_win', 'close_loss', 'custom_end'] = None, - custom_start: Callable = None, custom_end: Callable = None, name: str = ''): + + def __init__( + self, + *, + start: int | time, + end: int | time, + on_start: Literal[ + "close_all", "close_win", "close_loss", "custom_start" + ] = None, + on_end: Literal["close_all", "close_win", "close_loss", "custom_end"] = None, + custom_start: Callable = None, + custom_end: Callable = None, + name: str = "", + ): """Create a session. Keyword Args: @@ -65,13 +79,17 @@ class Session: custom_end (Callable): A custom function to call when the session ends. Default is None. name (str): A name for the session. Default is a combination of start and end. """ - self.start = start.replace(tzinfo=UTC) if isinstance(start, time) else time(hour=start, tzinfo=UTC) + self.start = ( + start.replace(tzinfo=UTC) + if isinstance(start, time) + else time(hour=start, tzinfo=UTC) + ) self.end = end if isinstance(end, time) else time(hour=end, tzinfo=UTC) self.on_start = on_start self.on_end = on_end self.custom_start = custom_start self.custom_end = custom_end - self.name = name or f'{self.start}<-->{self.end}' + self.name = name or f"{self.start}<-->{self.end}" self.positions_manager = Positions() self.config = Config() @@ -81,20 +99,25 @@ class Session: return item_span <= span def __str__(self): - return f'{self.start}<-->{self.end}' + return f"{self.start}<-->{self.end}" def __repr__(self): - return f'{self.start}<-->{self.end}' + return f"{self.start}<-->{self.end}" def __len__(self): return int((delta(self.end) - delta(self.start)).seconds) - + def in_session(self) -> bool: """Check if the current time is within the session.""" - now = datetime.now(tz=UTC).time() if self.config.mode == 'live'\ - else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time() + now = ( + datetime.now(tz=UTC).time() + if self.config.mode == "live" + else datetime.fromtimestamp( + self.config.backtest_engine.cursor.time, tz=UTC + ).time() + ) return now in self - + async def begin(self): """Call the action specified in on_start or custom_start.""" await self.action(action=self.on_start) @@ -110,16 +133,20 @@ class Session: return Duration(hours=hours, minutes=minutes, seconds=seconds) async def close_positions(self, *, positions: tuple[TradePosition, ...]): - - results = asyncio.gather(*(self.positions_manager.close_position(position=position) for position in positions), - return_exceptions=True) + results = asyncio.gather( + *( + self.positions_manager.close_position(position=position) + for position in positions + ), + return_exceptions=True, + ) closed = pending = 0 for result in results: if isinstance(result, OrderSendResult) and result.retcode == 10009: closed += 1 continue pending += 1 - logger.info(f'Closed {closed} positions') + logger.info(f"Closed {closed} positions") logger.warning(f"{pending} positions still pending") if pending else ... async def close_all(self): @@ -128,12 +155,16 @@ class Session: async def close_win(self): open_positions = await self.positions_manager.get_positions() - positions = tuple(position for position in open_positions if position.profit >= 0) + positions = tuple( + position for position in open_positions if position.profit >= 0 + ) await self.close_positions(positions=positions) async def close_loss(self): open_positions = await self.positions_manager.get_positions() - positions = tuple(position for position in open_positions if position.profit < 0) + positions = tuple( + position for position in open_positions if position.profit < 0 + ) await self.close_positions(positions=positions) async def action(self, *, action): @@ -144,30 +175,32 @@ class Session: """ try: match action: - case 'close_all': + case "close_all": await self.close_all() - case 'close_win': + case "close_win": await self.close_win() - case 'close_loss': + case "close_loss": await self.close_loss() - case 'custom_end': + case "custom_end": await self.custom_end() - case 'custom_start': + case "custom_start": await self.custom_start() case _: pass except Exception as exe: - logger.warning(f'Failed to call action {action} due to {exe}') + logger.warning(f"Failed to call action {action} due to {exe}") def until(self): """Get the seconds until the session starts from the current time in seconds.""" - if self.config.mode == 'backtest': - now = datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time() + if self.config.mode == "backtest": + now = datetime.fromtimestamp( + self.config.backtest_engine.cursor.time, tz=UTC + ).time() secs = (delta(self.start) - delta(now)).seconds else: secs = (delta(self.start) - delta(datetime.now(tz=UTC).time())).seconds @@ -187,6 +220,7 @@ class Sessions: find_next: Find the next session that contains a datetime.time object. check: Check if the current session has started and if not, wait until it starts. """ + sessions: list[Session] current_session: Session | None @@ -205,8 +239,15 @@ class Sessions: Returns: Session | None: A Session object or None if not found. """ - moment = moment or datetime.now(tz=UTC).time() if self.config.mode == 'live' else ( - datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time()) + moment = ( + moment or datetime.now(tz=UTC).time() + if self.config.mode == "live" + else ( + datetime.fromtimestamp( + self.config.backtest_engine.cursor.time, tz=UTC + ).time() + ) + ) for session in self.sessions: if moment in session: return session @@ -221,8 +262,15 @@ class Sessions: Returns: Session: A Session object. """ - moment = moment or datetime.now(tz=UTC).time() if self.config.mode == 'live' else ( - datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time()) + moment = ( + moment or datetime.now(tz=UTC).time() + if self.config.mode == "live" + else ( + datetime.fromtimestamp( + self.config.backtest_engine.cursor.time, tz=UTC + ).time() + ) + ) for session in self.sessions: if delta(moment) < delta(session.start): return session @@ -242,9 +290,11 @@ class Sessions: """Check if the current session has started and if not, wait until it starts.""" if self.current_session is not None and self.current_session.in_session(): return - - if self.config.mode == 'backtest': - now = datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time() + + if self.config.mode == "backtest": + now = datetime.fromtimestamp( + self.config.backtest_engine.cursor.time, tz=UTC + ).time() else: now = datetime.now(tz=UTC).time() @@ -266,8 +316,8 @@ class Sessions: next_session = self.find_next(moment=now) secs = next_session.until() + 10 - logger.info(f'sleeping for {secs} seconds until next {next_session} session') - sleep_func = asyncio.sleep if self.config.mode == 'live' else backtest_sleep + logger.info(f"sleeping for {secs} seconds until next {next_session} session") + sleep_func = asyncio.sleep if self.config.mode == "live" else backtest_sleep await sleep_func(secs) self.current_session = next_session await self.current_session.begin() diff --git a/src/aiomql/lib/strategy.py b/src/aiomql/lib/strategy.py index 8d079dd..b6f2ec0 100644 --- a/src/aiomql/lib/strategy.py +++ b/src/aiomql/lib/strategy.py @@ -8,8 +8,9 @@ from logging import getLogger from ..core.meta_trader import MetaTrader from ..core import Config +from ..core.exceptions import StopTrading from ..core.meta_backtester import MetaBackTester -from ..core.event_manager import EventManager +from ..core.backtesting.backtest_controller import BackTestController from .sessions import Sessions, Session from .symbol import Symbol as _Symbol @@ -30,6 +31,7 @@ class Strategy(ABC): Notes: Define the name of a strategy as a class attribute. If not provided, the class name will be used as the name. """ + name: str symbol: Symbol sessions: Sessions @@ -37,10 +39,12 @@ class Strategy(ABC): config: Config running: bool parameters = {} - event_manager = EventManager + backtest_controller = BackTestController current_session = Session - def __init__(self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None, name=''): + def __init__( + self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None, name="" + ): """Initiate the parameters dict and add name and symbol fields. Use class name as strategy name if name is not provided @@ -54,10 +58,12 @@ class Strategy(ABC): self.parameters["symbol"] = symbol.name self.parameters["name"] = self.name self.running = True - self.sessions = sessions or Sessions(sessions=[Session(start=0, end=dtime(hour=23, minute=59, second=59))]) + self.sessions = sessions or Sessions( + sessions=[Session(start=0, end=dtime(hour=23, minute=59, second=59))] + ) self.config = Config() - self.mt5 = MetaTrader() if self.config.mode != 'backtest' else MetaBackTester() - self.event_manager = EventManager() + self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester() + self.backtest_controller = BackTestController() def __repr__(self): return f"{self.name}({self.symbol!r})" @@ -65,7 +71,7 @@ class Strategy(ABC): def __getattr__(self, item): if item in self.parameters: return self.parameters[item] - raise AttributeError(f'{item} not an attribute of {self.name}') + raise AttributeError(f"{item} not an attribute of {self.name}") def __setattr__(self, key, value): if key in self.parameters: @@ -105,66 +111,74 @@ class Strategy(ABC): Args: secs (float): The time in seconds. Usually the timeframe you are trading on. """ - if self.config.mode == 'backtest': + if self.config.mode == "backtest": await self.backtest_sleep(secs=secs) else: await self.live_sleep(secs=secs) async def backtest_sleep(self, *, secs: float): - # print(f"Sleeping for {secs} seconds") try: _time = self.config.backtest_engine.cursor.time mod = _time % secs - # print('mod', mod) secs = secs - mod if mod != 0 else mod - # print('secs', secs) - if self.event_manager.num_main_tasks == 1: - self.config.backtest_engine.fast_forward(steps=int(secs)) - await self.event_manager.wait() + if self.backtest_controller.parties == 2: + steps = int(secs) // self.config.backtest_engine.speed + self.config.backtest_engine.fast_forward(steps=steps) + self.backtest_controller.wait() - elif self.event_manager.num_main_tasks > 1: + elif self.backtest_controller.parties > 2: _time = self.config.backtest_engine.cursor.time + secs - # print(_time, self.config.backtest_engine.cursor.time) while _time > self.config.backtest_engine.cursor.time: - print(f"Time in sleep {self.symbol}: {self.config.backtest_engine.cursor.time}") - await self.event_manager.wait() - - # await self.event_manager.wait() + self.backtest_controller.wait() else: - await self.event_manager.wait() + self.backtest_controller.wait() except Exception as err: - logger.error(f"Error: {err} in backtest_sleep") + logger.error("Error: %s in backtest_sleep", err) async def run_strategy(self): """Run the strategy.""" - if self.config.mode == 'live': + if self.config.mode == "live": await self.live_strategy() - elif self.config.mode == 'backtest': + elif self.config.mode == "backtest": await self.backtest_strategy() async def live_strategy(self): """Run the strategy.""" async with self as _: + logger.info("Running %s strategy on %s", self.name, self.symbol.name) while self.running: - await self.sessions.check() - await self.trade() + try: + await self.sessions.check() + await self.trade() + except StopTrading: + self.running = False + break + except Exception as err: + logger.error("Error: %s in live_strategy", err) + return async def backtest_strategy(self): """Backtest the strategy.""" - try: - async with self as _: - while self.running: - async with self.event_manager.condition: - await self.sessions.check() - await self.event_manager.wait() - await self.test() - except Exception as err: - logger.error(f"Error: {err} in backtest_strategy") + async with self as _: + logger.info( + "Testing %s strategy on %s with Backtester", self.name, self.symbol.name + ) + while self.running: + try: + await self.sessions.check() + self.backtest_controller.wait() + await self.test() + except StopTrading: + self.running = False + break + except Exception as err: + logger.error(f"Error: {err} in backtest_strategy") + return @abstractmethod async def trade(self): """Place trades using this method. This is the main method of the strategy. - It will be called by the strategy runner. + It will be called by the strategy runner. """ raise NotImplementedError("Implement this method in your subclass") diff --git a/src/aiomql/lib/symbol.py b/src/aiomql/lib/symbol.py index 2530ac5..f7dbbaf 100644 --- a/src/aiomql/lib/symbol.py +++ b/src/aiomql/lib/symbol.py @@ -27,6 +27,7 @@ class Symbol(_Base, SymbolInfo): Full properties are on the SymbolInfo Object. Make sure Symbol is always initialized with a name argument """ + tick: Tick account: Account @@ -36,11 +37,11 @@ class Symbol(_Base, SymbolInfo): Args: name (str): Name of the financial instrument """ - assert 'name' in kwargs, "Symbol Object Must be initialized with a name" + assert "name" in kwargs, "Symbol Object Must be initialized with a name" super().__init__(**kwargs) self.account = Account() - @backoff_decorator + # @backoff_decorator async def info_tick(self, *, name: str = "") -> Tick | None: """Get the current price tick of a financial instrument. @@ -51,12 +52,16 @@ class Symbol(_Base, SymbolInfo): Tick: Return a Tick Object None: If request was unsuccessful """ - tick = await self.mt5.symbol_info_tick(name or self.name) - if tick is not None: - tick = Tick(**tick._asdict()) - setattr(self, 'tick', tick) if not name else ... - return tick - return None + try: + tick = await self.mt5.symbol_info_tick(name or self.name) + if tick is not None: + tick = Tick(**tick._asdict()) + setattr(self, "tick", tick) if not name else ... + return tick + return None + except Exception as err: + logger.warning(f"{err}: Unable to get tick for {self.name}") + return None async def symbol_select(self, *, enable: bool = True) -> bool: """Select a symbol in the MarketWatch window or remove a symbol from the window. @@ -72,7 +77,7 @@ class Symbol(_Base, SymbolInfo): self.select = await self.mt5.symbol_select(self.name, enable) return self.select - @backoff_decorator + # @backoff_decorator async def info(self) -> SymbolInfo | None: """Get data on the specified financial instrument and update the symbol object properties @@ -83,7 +88,7 @@ class Symbol(_Base, SymbolInfo): info = await self.mt5.symbol_info(self.name) if info is not None: info = info._asdict() - info['swap_rollover3days'] = info.get('swap_rollover3days', 0) % 7 + info["swap_rollover3days"] = info.get("swap_rollover3days", 0) % 7 self.set_attributes(**info) return SymbolInfo(**info) return None @@ -95,14 +100,16 @@ class Symbol(_Base, SymbolInfo): bool: Returns True if symbol info was successful initialized """ try: - res = await asyncio.gather(self.symbol_select(), self.info(), self.info_tick(), self.book_add(), - return_exceptions=True) - if any(res): + await self.symbol_select() + info = await self.info() + info_tick = await self.info_tick() + await self.book_add() + if all((info is not None, info_tick is not None)): return True - logger.warning('Unable to initialize %s', self.name) + logger.warning("Unable to initialize %s", self.name) return False except Exception as err: - logger.warning('%s: Unable to initialize %s', err, self.name) + logger.warning("%s: Unable to initialize %s", err, self.name) return False async def book_add(self) -> bool: @@ -117,7 +124,6 @@ class Symbol(_Base, SymbolInfo): logger.debug("Could not add %s to market book", self.name) return res - @backoff_decorator async def book_get(self) -> tuple[BookInfo, ...]: """Returns a tuple of BookInfo featuring Market Depth entries for the specified symbol. @@ -133,7 +139,7 @@ class Symbol(_Base, SymbolInfo): book_infos = (BookInfo(**info._asdict()) for info in infos) return tuple(book_infos) - raise ValueError(f'Could not get book info for {self.name}') + raise ValueError(f"Could not get book info for {self.name}") async def book_release(self) -> bool: """Cancels subscription of the MetaTrader 5 terminal to the Market Depth change events for a specified symbol. @@ -156,7 +162,10 @@ class Symbol(_Base, SymbolInfo): if check := self.volume_min <= volume <= self.volume_max: return check, volume else: - return check, self.volume_min if volume <= self.volume_min else self.volume_max + return ( + check, + self.volume_min if volume <= self.volume_min else self.volume_max, + ) def round_off_volume(self, *, volume: float, round_down: bool = False) -> float: """Round off the volume to the nearest volume step. @@ -173,8 +182,11 @@ class Symbol(_Base, SymbolInfo): async def amount_in_quote_currency(self, *, amount: float) -> float: """Convert the amount to the quote currency of the symbol.""" if self.currency_profit != self.account.currency: - amount = await self.convert_currency(amount=amount, from_currency=self.account.currency, - to_currency=self.currency_profit) + amount = await self.convert_currency( + amount=amount, + from_currency=self.account.currency, + to_currency=self.currency_profit, + ) return amount async def compute_volume(self) -> float: @@ -187,7 +199,9 @@ class Symbol(_Base, SymbolInfo): """ return self.volume_min - async def convert_currency(self, *, amount: float, from_currency: str, to_currency: str) -> float: + async def convert_currency( + self, *, amount: float, from_currency: str, to_currency: str + ) -> float: """Convert a given amount from one currency to the other. Args: amount: Amount to convert @@ -196,21 +210,24 @@ class Symbol(_Base, SymbolInfo): """ base, quote = to_currency, from_currency try: - pair = f'{quote}{base}' + pair = f"{quote}{base}" tick = await self.info_tick(name=pair) if tick is not None: return round(amount * tick.bid, 2) - pair = f'{base}{quote}' + pair = f"{base}{quote}" tick = await self.info_tick(name=pair) if tick is not None: return round(amount / tick.ask, 2) except Exception as err: - logger.warning(f'{err}: Currency conversion failed: Unable to convert {amount} in {quote} to {base}') + logger.warning( + f"{err}: Currency conversion failed: Unable to convert {amount} in {quote} to {base}" + ) @backoff_decorator - async def copy_rates_from(self, *, timeframe: TimeFrame, - date_from: datetime | int, count: int = 500) -> Candles: + async def copy_rates_from( + self, *, timeframe: TimeFrame, date_from: datetime | int, count: int = 500 + ) -> Candles: """ Get bars from the MetaTrader 5 terminal starting from the specified date. @@ -231,11 +248,12 @@ class Symbol(_Base, SymbolInfo): rates = await self.mt5.copy_rates_from(self.name, timeframe, date_from, count) if rates is not None: return Candles(data=rates) - raise ValueError(f'Could not get rates for {self.name}.') + raise ValueError(f"Could not get rates for {self.name}.") @backoff_decorator - async def copy_rates_from_pos(self, *, timeframe: TimeFrame, count: int = 500, - start_position: int = 0) -> Candles: + async def copy_rates_from_pos( + self, *, timeframe: TimeFrame, count: int = 500, start_position: int = 0 + ) -> Candles: """Get bars from the MetaTrader 5 terminal starting from the specified index. Args: @@ -252,14 +270,21 @@ class Symbol(_Base, SymbolInfo): Raises: ValueError: If request was unsuccessful and None was returned """ - rates = await self.mt5.copy_rates_from_pos(self.name, timeframe, start_position, count) + rates = await self.mt5.copy_rates_from_pos( + self.name, timeframe, start_position, count + ) if rates is not None: return Candles(data=rates) - raise ValueError(f'Could not get rates for {self.name}.') + raise ValueError(f"Could not get rates for {self.name}.") @backoff_decorator - async def copy_rates_range(self, *, timeframe: TimeFrame, date_from: datetime | int, - date_to: datetime | int) -> Candles: + async def copy_rates_range( + self, + *, + timeframe: TimeFrame, + date_from: datetime | int, + date_to: datetime | int, + ) -> Candles: """Get bars in the specified date range from the MetaTrader 5 terminal. Args: @@ -279,15 +304,21 @@ class Symbol(_Base, SymbolInfo): Raises: ValueError: If request was unsuccessful and None was returned """ - rates = await self.mt5.copy_rates_range(symbol=self.name, timeframe=timeframe, date_from=date_from, - date_to=date_to) + rates = await self.mt5.copy_rates_range( + symbol=self.name, timeframe=timeframe, date_from=date_from, date_to=date_to + ) if rates is not None: return Candles(data=rates) - raise ValueError(f'Could not get rates for {self.name}.') + raise ValueError(f"Could not get rates for {self.name}.") @backoff_decorator - async def copy_ticks_from(self, *, date_from: datetime | int, count: int = 100, - flags: CopyTicks = CopyTicks.ALL) -> Ticks: + async def copy_ticks_from( + self, + *, + date_from: datetime | int, + count: int = 100, + flags: CopyTicks = CopyTicks.ALL, + ) -> Ticks: """ Get ticks from the MetaTrader 5 terminal starting from the specified date. @@ -307,11 +338,16 @@ class Symbol(_Base, SymbolInfo): ticks = await self.mt5.copy_ticks_from(self.name, date_from, count, flags) if ticks is not None: return Ticks(data=ticks) - raise ValueError(f'Could not get ticks for {self.name}.') + raise ValueError(f"Could not get ticks for {self.name}.") @backoff_decorator - async def copy_ticks_range(self, *, date_from: datetime | int, date_to: datetime | int, - flags: CopyTicks = CopyTicks.ALL) -> Ticks: + async def copy_ticks_range( + self, + *, + date_from: datetime | int, + date_to: datetime | int, + flags: CopyTicks = CopyTicks.ALL, + ) -> Ticks: """Get ticks for the specified date range from the MetaTrader 5 terminal. Args: @@ -333,4 +369,4 @@ class Symbol(_Base, SymbolInfo): ticks = await self.mt5.copy_ticks_range(self.name, date_from, date_to, flags) if ticks is not None: return Ticks(data=ticks) - raise ValueError(f'Could not get ticks for {self.name}.') + raise ValueError(f"Could not get ticks for {self.name}.") diff --git a/src/aiomql/lib/terminal.py b/src/aiomql/lib/terminal.py index 3497673..abf43d7 100644 --- a/src/aiomql/lib/terminal.py +++ b/src/aiomql/lib/terminal.py @@ -7,7 +7,9 @@ from ..core.base import _Base logger = getLogger(__name__) -Version = NamedTuple("Version", (('version', str), ('build', int), ('release_date', str))) +Version = NamedTuple( + "Version", (("version", str), ("build", int), ("release_date", str)) +) class Terminal(_Base, TerminalInfo): @@ -17,6 +19,7 @@ class Terminal(_Base, TerminalInfo): Notes: Other attributes are defined in the TerminalInfo Class """ + version: Version | None = None async def initialize(self) -> bool: @@ -31,7 +34,7 @@ class Terminal(_Base, TerminalInfo): self.connected = await self.mt5.initialize() if not self.connected: err = await self.mt5.last_error() - logger.warning(f'Failed to initialize Terminal. Error Code: {err}') + logger.warning(f"Failed to initialize Terminal. Error Code: {err}") info = await self.info() await self.get_version() return bool(self.connected and info and self.version) @@ -45,7 +48,7 @@ class Terminal(_Base, TerminalInfo): """ res = await self.mt5.version() if res is None: - logger.error('Failed to get terminal version') + logger.error("Failed to get terminal version") return None self.version = Version(*res) return self.version diff --git a/src/aiomql/lib/ticks.py b/src/aiomql/lib/ticks.py index 3a37b8f..5cca9fe 100644 --- a/src/aiomql/lib/ticks.py +++ b/src/aiomql/lib/ticks.py @@ -25,6 +25,7 @@ class Tick: volume_real (float): Volume for the current Last price Index (int): Custom attribute representing the position of the tick in a sequence. """ + time: float bid: float ask: float @@ -38,17 +39,28 @@ class Tick: def __init__(self, **kwargs): """Initialize the Tick class. Set attributes from keyword arguments. bid, ask, last and volume must be present""" - if not all(key in kwargs for key in ['bid', 'ask', 'last', 'volume']): - raise ValueError("bid, ask, last and volume, time must be present in the keyword arguments") - self.Index = kwargs.pop('Index', 0) - self.time = kwargs.pop('time', time.monotonic()) + if not all(key in kwargs for key in ["bid", "ask", "last", "volume"]): + raise ValueError( + "bid, ask, last and volume, time must be present in the keyword arguments" + ) + self.Index = kwargs.pop("Index", 0) + self.time = kwargs.pop("time", time.monotonic()) self.time_msc = int(self.time * 1000) self.set_attributes(**kwargs) def __repr__(self): - return ("%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s)" - % {"class": self.__class__.__name__, "time": self.time, "bid": self.bid, - "ask": self.ask, "last": self.last, "volume": self.volume, 'Index': self.Index}) + return ( + "%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s)" + % { + "class": self.__class__.__name__, + "time": self.time, + "bid": self.bid, + "ask": self.ask, + "last": self.last, + "volume": self.volume, + "Index": self.Index, + } + ) def __eq__(self, other: Self): return self.time == other.time @@ -97,6 +109,7 @@ class Tick: class Ticks: """Container class for price ticks. Arrange in chronological order. Supports iteration, slicing and assignment""" + time: Series bid: Series ask: Series @@ -122,7 +135,7 @@ class Ticks: elif isinstance(data, Iterable): data = DataFrame(data) else: - raise ValueError(f'Cannot create DataFrame from object of {type(data)}') + raise ValueError(f"Cannot create DataFrame from object of {type(data)}") self._data = data.iloc[::-1] if flip else data def __repr__(self): @@ -137,7 +150,9 @@ class Ticks: def __getattr__(self, item): if item in list(self._data.columns.values): return self._data[item] - raise AttributeError(f'Attribute {item} not defined on class {self.__class__.__name__}') + raise AttributeError( + f"Attribute {item} not defined on class {self.__class__.__name__}" + ) def __getitem__(self, index) -> Tick | Self: if isinstance(index, slice): @@ -156,7 +171,7 @@ class Ticks: if isinstance(value, Series): self._data[index] = value return - raise TypeError(f'Expected Series got {type(value)}') + raise TypeError(f"Expected Series got {type(value)}") def __iter__(self): return (Tick(**row._asdict()) for row in self._data.itertuples()) @@ -176,7 +191,7 @@ class Ticks: Returns: ta: The ta library - """ + """ return ta @property @@ -209,11 +224,21 @@ class Ticks: """ columns = columns or [] data = self._data[-count:] - data.index = pd.to_datetime(data['time'], unit='s') + data.index = pd.to_datetime(data["time"], unit="s") return mplt.make_addplot(data[columns], **kwargs) - def visualize(self, *, count: int = 50, _type='candle', savefig: str | dict = None, addplot: dict = None, - style: str = 'charles', ylabel: str = 'Price', title: str = 'Chart', **kwargs): + def visualize( + self, + *, + count: int = 50, + _type="candle", + savefig: str | dict = None, + addplot: dict = None, + style: str = "charles", + ylabel: str = "Price", + title: str = "Chart", + **kwargs, + ): """Visualize the candles using the mplfinance library. Args: count (int): The number of candles to visualize, counting from behind, i.e the most recent candles. @@ -227,8 +252,18 @@ class Ticks: title (str): The title of the chart. Defaults to 'Chart'. kwargs: valid kwargs for the plot function. """ - kwargs |= {key: arg for key, arg in (('savefig', savefig), ('addplot', addplot), ('style', style), - ('ylabel', ylabel), ('title', title), ('type', _type)) if arg} + kwargs |= { + key: arg + for key, arg in ( + ("savefig", savefig), + ("addplot", addplot), + ("style", style), + ("ylabel", ylabel), + ("title", title), + ("type", _type), + ) + if arg + } data = self._data[-count:] - data.index = pd.to_datetime(data['time'], unit='s') + data.index = pd.to_datetime(data["time"], unit="s") mplt.plot(data, **kwargs) diff --git a/src/aiomql/lib/trade_records.py b/src/aiomql/lib/trade_records.py index 30f26c6..650a0b0 100644 --- a/src/aiomql/lib/trade_records.py +++ b/src/aiomql/lib/trade_records.py @@ -24,11 +24,12 @@ class TradeRecords: records_dir(Path): Absolute path to directory containing record of placed trades, If not given takes the default from the config """ + config: Config mt5: MetaTrader | MetaBackTester positions: list[TradePosition] | None = None - def __init__(self, *, records_dir: Path | str = ''): + def __init__(self, *, records_dir: Path | str = ""): """Initialize the Records class. The main method of this class is update_records which you should call to update all the records specified in the records_dir. @@ -36,7 +37,7 @@ class TradeRecords: records_dir (Path): Absolute path to directory containing record of placed trades. """ self.config = Config() - self.mt5 = MetaTrader() if self.config.mode != 'backtest' else MetaBackTester() + self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester() self.records_dir = records_dir or self.config.records_dir def get_csv_records(self): @@ -46,7 +47,7 @@ class TradeRecords: files: Trade record files """ for file in self.records_dir.iterdir(): - if file.is_file() and file.name.endswith('.csv'): + if file.is_file() and file.name.endswith(".csv"): yield file def get_json_records(self): @@ -56,7 +57,7 @@ class TradeRecords: files (Path): Trade record files """ for file in self.records_dir.iterdir(): - if file.is_file() and file.name.endswith('.json'): + if file.is_file() and file.name.endswith(".json"): yield file async def read_update_csv(self, *, file: Path): @@ -66,17 +67,22 @@ class TradeRecords: file: Trade record file in csv format """ try: - with open(file, mode='r', newline='') as fr: + with open(file, mode="r", newline="") as fr: reader: Iterable[dict] | csv.DictReader = csv.DictReader(fr) rows = [row for row in reader] rows = await self.update_rows(rows=rows) - with open(file, mode='w', newline='') as fw: - writer = csv.DictWriter(fw, fieldnames=reader.fieldnames, extrasaction='ignore', restval=None) + with open(file, mode="w", newline="") as fw: + writer = csv.DictWriter( + fw, + fieldnames=reader.fieldnames, + extrasaction="ignore", + restval=None, + ) writer.writeheader() writer.writerows(rows) except Exception as err: - logger.error(f'Error: {err}. Unable to read and update csv trade records') + logger.error(f"Error: {err}. Unable to read and update csv trade records") async def read_update_json(self, *, file: Path): """Read and update json trade records @@ -84,15 +90,15 @@ class TradeRecords: file: Trade record file in csv format """ try: - with open(file, mode='r') as fh: + with open(file, mode="r") as fh: data = json.load(fh) rows = [row for row in data] rows = await self.update_rows(rows=rows) - with open(file, mode='w') as fh: + with open(file, mode="w") as fh: json.dump(rows, fh, indent=2) except Exception as err: - logger.error(f'Error: {err}. Unable to read and update json trade records') + logger.error(f"Error: {err}. Unable to read and update json trade records") async def update_row(self, *, row: dict) -> dict: """Update a single row of entered trade in the csv or json file with the actual profit. @@ -104,20 +110,28 @@ class TradeRecords: dict: A dictionary with the actual profit and win status. """ try: - order = int(row['order']) + order = int(row["order"]) positions = self.positions or await self.mt5.positions_get() position_ids = [position.ticket for position in positions] deals = await self.mt5.history_deals_get(position=order) if not deals or len(deals) <= 1: return row - deals = [deal for deal in deals if (deal.order != deal.position_id and deal.position_id == order - and deal.entry == 1 and deal.position_id not in position_ids)] + deals = [ + deal + for deal in deals + if ( + deal.order != deal.position_id + and deal.position_id == order + and deal.entry == 1 + and deal.position_id not in position_ids + ) + ] deals.sort(key=lambda deal: deal.time_msc) deal = deals[-1] row.update(actual_profit=deal.profit, win=deal.profit > 0, closed=True) return row except Exception as err: - logging.error(f'Error: {err}. Unable to update trade record') + logging.error(f"Error: {err}. Unable to update trade record") return row async def update_rows(self, *, rows: list[dict]) -> list[dict]: @@ -132,8 +146,8 @@ class TradeRecords: self.positions = await self.mt5.positions_get() closed, unclosed = [], [] for row in rows: - closed_ = row.get('closed', False) - closed_ = closed_.title() == 'True' if isinstance(closed_, str) else closed_ + closed_ = row.get("closed", False) + closed_ = closed_.title() == "True" if isinstance(closed_, str) else closed_ if closed_: closed.append(row) else: @@ -143,12 +157,16 @@ class TradeRecords: async def update_csv_records(self): """Update csv trade records in the records_dir folder.""" - records = [self.read_update_csv(file=record) for record in self.get_csv_records()] + records = [ + self.read_update_csv(file=record) for record in self.get_csv_records() + ] await asyncio.gather(*records) async def update_json_records(self): """Update json trade records in the records_dir folder.""" - records = [self.read_update_json(file=record) for record in self.get_json_records()] + records = [ + self.read_update_json(file=record) for record in self.get_json_records() + ] await asyncio.gather(*records) async def update_csv_record(self, *, file: Path | str): diff --git a/src/aiomql/lib/trader.py b/src/aiomql/lib/trader.py index 982e47f..928df8d 100644 --- a/src/aiomql/lib/trader.py +++ b/src/aiomql/lib/trader.py @@ -29,6 +29,7 @@ class Trader(ABC): parameters (dict): Parameters of the trading strategy used to place the trade config (Config): The Config instance. """ + config: Config ram: RAM parameters: dict @@ -58,13 +59,17 @@ class Trader(ABC): sl, tp = pips, pips * self.ram.risk_to_reward price = self.order.price if self.order.type == OrderType.BUY: - self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round(price + tp, - self.symbol.digits) + self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round( + price + tp, self.symbol.digits + ) elif self.order.type == OrderType.SELL: - self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(price - tp, - self.symbol.digits) + self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round( + price - tp, self.symbol.digits + ) - def set_trade_stop_levels_points(self, *, points: float, risk_to_reward: float = None): + def set_trade_stop_levels_points( + self, *, points: float, risk_to_reward: float = None + ): """Set the stop loss and take profit levels of the order based on the points and the risk to reward ratio. It is assumed that order_type and price are already set before calling this method. @@ -77,13 +82,23 @@ class Trader(ABC): price, digits = self.order.price, self.symbol.digits if self.order.type == OrderType.BUY: - self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round(price + tp, digits) + self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round( + price + tp, digits + ) elif self.order.type == OrderType.SELL: - self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(price - tp, digits) + self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round( + price - tp, digits + ) - async def create_order_with_stops(self, *, order_type: OrderType, sl: float, tp: float, - amount_to_risk: float = None): + async def create_order_with_stops( + self, + *, + order_type: OrderType, + sl: float, + tp: float, + amount_to_risk: float = None, + ): """Create an order with stop loss and take profit levels. Use the amount to risk per trade to calculate the volume. @@ -99,10 +114,18 @@ class Trader(ABC): tick = await self.symbol.info_tick() price = tick.ask if order_type == OrderType.BUY else tick.bid 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_sl(self, *, order_type: OrderType, sl: float, amount_to_risk: float = None, - risk_to_reward: float = None): + async def create_order_with_sl( + self, + *, + order_type: OrderType, + sl: float, + amount_to_risk: float = None, + risk_to_reward: float = None, + ): """ Create an order with a given stop_loss level. Use the amount to risk per trade to calculate the volume. @@ -122,10 +145,18 @@ class Trader(ABC): dtp = dsl * (risk_to_reward or self.ram.risk_to_reward) tp = price + dtp if order_type == OrderType.BUY else price - dtp 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(self, *, order_type: OrderType, points: float, - amount_to_risk: float = None, risk_to_reward: float = None): + async def create_order_with_points( + self, + *, + order_type: OrderType, + points: float, + amount_to_risk: float = None, + risk_to_reward: float = None, + ): """Create an order with specific points to risk. Use the amount to risk per trade to calculate the volume. Args: @@ -145,7 +176,9 @@ class Trader(ABC): self.order.volume = volume self.set_trade_stop_levels_points(points=points, risk_to_reward=risk_to_reward) - async def create_order_no_stops(self, *, order_type: OrderType, volume: float = None): + async def create_order_no_stops( + self, *, order_type: OrderType, volume: float = None + ): """Create an order without setting stop loss and take profit. Using minimum lot size. Args: @@ -171,9 +204,6 @@ class Trader(ABC): if check.retcode != 0: logger.warning(f"Invalid order for due to {check.comment}") - else: - logger.info(f"Order check passed for {self.symbol}") - return check async def send_order(self) -> OrderSendResult | None: @@ -186,11 +216,12 @@ class Trader(ABC): if result.retcode != 10009: logger.warning(f"Unable to place order for due to {result.comment}") return result - logger.info("Order placed successfully") return result @error_handler - async def record_trade(self, *, result: OrderSendResult, parameters: dict = None, name: str = ''): + async def record_trade( + self, *, result: OrderSendResult, parameters: dict = None, name: str = "" + ): """Record the trade in csv or json. Args: result (OrderSendResult): Result of the order send @@ -202,11 +233,14 @@ class Trader(ABC): params = {**parameters} or {} profit = await self.order.calc_profit() params["expected_profit"] = profit - date = datetime.now(tz=UTC) if self.config.mode == 'live' else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC) + date = ( + datetime.now(tz=UTC) + if self.config.mode == "live" + else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC) + ) params["date"] = date.strftime("%Y-%m-%d %H:%M:%S.%f") - if self.config.record_trades: - res = Result(result=result, parameters=params, name=name) - self.config.task_queue.add(item=QueueItem(res.save), must_complete=True) + res = Result(result=result, parameters=params, name=name) + self.config.task_queue.add(item=QueueItem(res.save), must_complete=True) @abstractmethod async def place_trade(self, *args, **kwargs): diff --git a/tests/actions/__init__.py b/tests/actions/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/backtest/conftest.py b/tests/backtest/conftest.py index 7310a71..b4952e2 100644 --- a/tests/backtest/conftest.py +++ b/tests/backtest/conftest.py @@ -8,7 +8,7 @@ from pathlib import Path import pytest from aiomql.core import Config from aiomql.core.meta_backtester import MetaBackTester -from aiomql.contrib import BackTestEngine +from aiomql.core.backtesting.backtest_engine import BackTestEngine from aiomql.lib import Positions, History, Order logger = getLogger(__name__) @@ -16,10 +16,10 @@ logger = getLogger(__name__) async def cleanup(): try: - shutil.rmtree(Path('tests/backtest/configs'), ignore_errors=True) - Path.unlink(Path('tests/backtest/test.json'), missing_ok=True) - shutil.rmtree(Path('tests/backtest/trade_records'), ignore_errors=True) - shutil.rmtree(Path('tests/backtest/backtesting'), ignore_errors=True) + shutil.rmtree(Path("tests/backtest/configs"), ignore_errors=True) + Path.unlink(Path("tests/backtest/test.json"), missing_ok=True) + shutil.rmtree(Path("tests/backtest/trade_records"), ignore_errors=True) + shutil.rmtree(Path("tests/backtest/backtesting"), ignore_errors=True) await close_all_positions() await MetaBackTester().shutdown() except Exception as err: @@ -32,29 +32,41 @@ async def close_all_positions(): positions = await mt.positions_get() tasks = [] for position in positions: - order_type = mt.ORDER_TYPE_BUY if position.type == mt.ORDER_TYPE_SELL else mt.ORDER_TYPE_SELL - req = {'action': mt.TRADE_ACTION_DEAL, 'symbol': position.symbol, 'volume': position.volume, - 'type': order_type, 'position': position.ticket, 'price': position.price_current} + order_type = ( + mt.ORDER_TYPE_BUY + if position.type == mt.ORDER_TYPE_SELL + else mt.ORDER_TYPE_SELL + ) + req = { + "action": mt.TRADE_ACTION_DEAL, + "symbol": position.symbol, + "volume": position.volume, + "type": order_type, + "position": position.ticket, + "price": position.price_current, + } tasks.append(mt.order_send(req)) await asyncio.gather(*tasks) except Exception as err: logger.error(f"Failed to close all positions: {err}") -@pytest.fixture(scope='package', autouse=True) +@pytest.fixture(scope="package", autouse=True) async def config(request): - Path('tests/backtest/configs').mkdir(exist_ok=True) - with open('aiomql.json', 'r') as fh, open('tests/backtest/configs/test2.json', 'w') as fh1, open('tests/backtest/test.json', 'w') as fh2: + Path("tests/backtest/configs").mkdir(exist_ok=True) + with open("aiomql.json", "r") as fh, open( + "tests/backtest/configs/test2.json", "w" + ) as fh1, open("tests/backtest/test.json", "w") as fh2: data = json.load(fh) - data['mode'] = 'backtest' + data["mode"] = "backtest" json.dump(data, fh1, indent=2) json.dump(data, fh2, indent=2) - config = Config(filename='test.json', root='tests/backtest') + config = Config(filename="test.json", root="tests/backtest") yield config await cleanup() -@pytest.fixture(scope='package', autouse=True) +@pytest.fixture(scope="package", autouse=True) async def mt(): mt = MetaBackTester() await mt.initialize() @@ -62,35 +74,41 @@ async def mt(): yield mt await mt.shutdown() -@pytest.fixture(scope='package') + +@pytest.fixture(scope="package") async def period(): - return {'start': datetime(2024, 2, 1, hour=8, tzinfo=UTC), - 'end': datetime(2024, 2, 7, hour=16, tzinfo=UTC)} + return { + "start": datetime(2024, 2, 1, hour=8, tzinfo=UTC), + "end": datetime(2024, 2, 7, hour=16, tzinfo=UTC), + } -@pytest.fixture(scope='package') + +@pytest.fixture(scope="package") async def backtest_engine(period): - start = period['start'] - end = period['end'] - return BackTestEngine(start=start, end=end, name='backtest_data') + start = period["start"] + end = period["end"] + return BackTestEngine( + start=start, end=end, name="backtest_data", assign_to_config=True, preload=False + ) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def order_sell(sell_order): return Order(**sell_order) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def order_buy(buy_order): return Order(**buy_order) -@pytest.fixture(scope='package') +@pytest.fixture(scope="package") def positions(): return Positions() -@pytest.fixture(scope='package') +@pytest.fixture(scope="package") def history(period): - start = period['start'] - end = period['end'] + start = period["start"] + end = period["end"] return History(date_from=start, date_to=end) diff --git a/tests/backtest/integration/test_backtesting.py b/tests/backtest/integration/test_backtesting.py index 7f0479e..865a468 100644 --- a/tests/backtest/integration/test_backtesting.py +++ b/tests/backtest/integration/test_backtesting.py @@ -1,27 +1,36 @@ -from aiomql.contrib import BackTestEngine, ForexSymbol, GetData -from aiomql.core import MetaBackTester +from aiomql.contrib import ForexSymbol +from aiomql.core import MetaBackTester, BackTestEngine, GetData from aiomql.lib import Order async def make_buy_sell_orders(): - sym = ForexSymbol(name='BTCUSD') + sym = ForexSymbol(name="BTCUSD") sym_info = await sym.mt5.symbol_info(sym.name) dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point sl = sym_info.ask - dsl tp = sym_info.ask + dsl - buy_req = {'action': sym.mt5.TRADE_ACTION_DEAL, 'symbol': sym.name, 'volume': sym_info.volume_min, - 'type': sym.mt5.ORDER_TYPE_BUY, 'price': sym_info.ask, 'sl': sl, 'tp': tp} + buy_req = { + "action": sym.mt5.TRADE_ACTION_DEAL, + "symbol": sym.name, + "volume": sym_info.volume_min, + "type": sym.mt5.ORDER_TYPE_BUY, + "price": sym_info.ask, + "sl": sl, + "tp": tp, + } sell_req = buy_req.copy() - sell_req['type'] = sym.mt5.ORDER_TYPE_SELL - sell_req['price'] = sym_info.bid - del sell_req['tp'] - del sell_req['sl'] - return {'buy': Order(**buy_req), 'sell': Order(**sell_req)} + sell_req["type"] = sym.mt5.ORDER_TYPE_SELL + sell_req["price"] = sym_info.bid + del sell_req["tp"] + del sell_req["sl"] + return {"buy": Order(**buy_req), "sell": Order(**sell_req)} -def test_trade_mode(config, backtest_engine, history, positions, order_sell, order_buy, btc_usd): - assert config.mode == 'backtest' +def test_trade_mode( + config, backtest_engine, history, positions, order_sell, order_buy, btc_usd +): + assert config.mode == "backtest" assert isinstance(backtest_engine, BackTestEngine) assert isinstance(history.mt5, MetaBackTester) assert isinstance(positions.mt5, MetaBackTester) @@ -69,15 +78,27 @@ async def test_history(backtest_engine, history, order_sell, order_buy, position async def test_margin(backtest_engine, order_sell, order_buy): await backtest_engine.setup_account(balance=100) - so_margin = await backtest_engine.order_calc_margin(action=order_sell.action, volume=order_sell.volume, - symbol=order_sell.symbol, price=order_sell.price) - bo_margin = await backtest_engine.order_calc_margin(action=order_buy.action, volume=order_buy.volume, - symbol=order_buy.symbol, price=order_buy.price) + so_margin = await backtest_engine.order_calc_margin( + action=order_sell.action, + volume=order_sell.volume, + symbol=order_sell.symbol, + price=order_sell.price, + ) + bo_margin = await backtest_engine.order_calc_margin( + action=order_buy.action, + volume=order_buy.volume, + symbol=order_buy.symbol, + price=order_buy.price, + ) total_margin = so_margin + bo_margin await backtest_engine.order_send(request=order_sell.request) await backtest_engine.order_send(request=order_buy.request) # noinspection PyTestUnpassedFixture - assert backtest_engine.positions.margin == total_margin == backtest_engine._account.margin + assert ( + backtest_engine.positions.margin + == total_margin + == backtest_engine._account.margin + ) backtest_engine.reset(clear_data=True) @@ -87,8 +108,8 @@ async def test_account(backtest_engine, positions): balance = backtest_engine._account.balance equity = backtest_engine._account.equity orders = await make_buy_sell_orders() - buy_order = orders['buy'] - sell_order = orders['sell'] + buy_order = orders["buy"] + sell_order = orders["sell"] so = await backtest_engine.order_send(request=sell_order.request) bo = await backtest_engine.order_send(request=buy_order.request) backtest_engine.fast_forward(steps=22000) @@ -103,7 +124,11 @@ async def test_account(backtest_engine, positions): deal = backtest_engine.deals.history_deals_get(position=bo.order) bo_profit = deal[-1].profit assert len(all_pos) == 1 - assert backtest_engine.positions.margin == backtest_engine._account.margin == backtest_engine.positions.margins[so.order] + assert ( + backtest_engine.positions.margin + == backtest_engine._account.margin + == backtest_engine.positions.margins[so.order] + ) profit = sum([pos.profit for pos in all_pos]) n_balance = backtest_engine._account.balance n_equity = backtest_engine._account.equity @@ -128,9 +153,11 @@ async def test_wrapup(positions, buy_order, sell_order, backtest_engine, config) last_balance = backtest_engine._account.balance last_equity = backtest_engine._account.equity last_profit = backtest_engine._account.profit - backtest_engine.wrap_up() - tdata = GetData.load_data(name=config.backtest_dir / f'{backtest_engine.name}.pkl') - new_bte = BackTestEngine(data=tdata, restart=False) + await backtest_engine.wrap_up() + tdata = GetData.load_data(name=config.backtest_dir / f"{backtest_engine.name}.pkl") + new_bte = BackTestEngine( + data=tdata, restart=False, assign_to_config=False, preload=False + ) assert new_bte._account.balance == last_balance assert new_bte._account.equity == last_equity assert new_bte._account.profit == last_profit diff --git a/tests/backtest/unit/test_deals_manager.py b/tests/backtest/unit/test_deals_manager.py index c828e54..4f3d13a 100644 --- a/tests/backtest/unit/test_deals_manager.py +++ b/tests/backtest/unit/test_deals_manager.py @@ -5,8 +5,8 @@ async def test_deals_manager(backtest_engine, sell_order, buy_order, period, pos backtest_engine.fast_forward(steps=100) await backtest_engine.order_send(request=sell_order) bo = await backtest_engine.order_send(request=buy_order) - start = period['start'] - end = period['end'] + start = period["start"] + end = period["end"] all_deals = backtest_engine.deals.get_deals_range(date_from=start, date_to=end) assert len(all_deals) == 2 backtest_engine.fast_forward(steps=10_000) @@ -21,4 +21,8 @@ async def test_deals_manager(backtest_engine, sell_order, buy_order, period, pos deals = backtest_engine.deals.history_deals_get(position=bo.order) assert len(deals) <= 2 orders = backtest_engine.deals.get_deals_range(date_from=start, date_to=end) - assert len(orders) == backtest_engine.deals.history_deals_total(date_from=start, date_to=end) == len(backtest_engine.deals._data.keys()) + assert ( + len(orders) + == backtest_engine.deals.history_deals_total(date_from=start, date_to=end) + == len(backtest_engine.deals._data.keys()) + ) diff --git a/tests/backtest/unit/test_order_manager.py b/tests/backtest/unit/test_order_manager.py index cd5c408..0be0bf3 100644 --- a/tests/backtest/unit/test_order_manager.py +++ b/tests/backtest/unit/test_order_manager.py @@ -1,12 +1,14 @@ # noinspection PyTestUnpassedFixture -async def test_orders_manager(backtest_engine, sell_order, buy_order, period, positions): +async def test_orders_manager( + backtest_engine, sell_order, buy_order, period, positions +): backtest_engine.reset(clear_data=True) await backtest_engine.setup_account(balance=100) backtest_engine.fast_forward(steps=100) await backtest_engine.order_send(request=sell_order) bo = await backtest_engine.order_send(request=buy_order) - start = period['start'] - end = period['end'] + start = period["start"] + end = period["end"] all_orders = backtest_engine.orders.get_orders_range(date_from=start, date_to=end) assert len(all_orders) == 2 backtest_engine.fast_forward(steps=10_000) @@ -21,4 +23,8 @@ async def test_orders_manager(backtest_engine, sell_order, buy_order, period, po orders = backtest_engine.orders.history_orders_get(position=bo.order) assert len(orders) <= 2 orders = backtest_engine.orders.get_orders_range(date_from=start, date_to=end) - assert len(orders) == backtest_engine.orders.history_orders_total(date_from=start, date_to=end) == len(backtest_engine.orders._data.keys()) + assert ( + len(orders) + == backtest_engine.orders.history_orders_total(date_from=start, date_to=end) + == len(backtest_engine.orders._data.keys()) + ) diff --git a/tests/backtest/unit/test_positions_manager.py b/tests/backtest/unit/test_positions_manager.py index 4891e38..68e16aa 100644 --- a/tests/backtest/unit/test_positions_manager.py +++ b/tests/backtest/unit/test_positions_manager.py @@ -10,7 +10,7 @@ async def test_positions_manager(backtest_engine, sell_order, buy_order): so_positions = backtest_engine.positions.positions_get(ticket=so.order) so_position = so_positions[0] assert so_position.ticket == so.order - btc_positions = backtest_engine.positions.positions_get(symbol='BTCUSD') + btc_positions = backtest_engine.positions.positions_get(symbol="BTCUSD") assert len(btc_positions) == 2 assert backtest_engine.positions.positions_total() == 2 backtest_engine.positions.close(ticket=bo.order) diff --git a/tests/conftest.py b/tests/conftest.py index 5a6026c..51cb628 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,42 +2,61 @@ from aiomql.lib.symbol import Symbol import pytest -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def btc_usd(): - return Symbol(name='BTCUSD') + return Symbol(name="BTCUSD") -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") async def buy_order(btc_usd): sym = btc_usd sym_info = await sym.mt5.symbol_info(sym.name) dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point sl = sym_info.ask - dsl tp = sym_info.ask + dsl - return {'action': sym.mt5.TRADE_ACTION_DEAL, 'symbol': sym.name, 'volume': sym_info.volume_min, - 'type': sym.mt5.ORDER_TYPE_BUY, 'price': sym_info.ask, 'sl': sl, 'tp': tp} + return { + "action": sym.mt5.TRADE_ACTION_DEAL, + "symbol": sym.name, + "volume": sym_info.volume_min, + "type": sym.mt5.ORDER_TYPE_BUY, + "price": sym_info.ask, + "sl": sl, + "tp": tp, + } -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") async def sell_order(btc_usd): sym = btc_usd sym_info = await sym.mt5.symbol_info(sym.name) - return {'action': sym.mt5.TRADE_ACTION_DEAL, 'symbol': sym.name, 'volume': sym_info.volume_min, - 'type': sym.mt5.ORDER_TYPE_SELL, 'price': sym_info.bid} + return { + "action": sym.mt5.TRADE_ACTION_DEAL, + "symbol": sym.name, + "volume": sym_info.volume_min, + "type": sym.mt5.ORDER_TYPE_SELL, + "price": sym_info.bid, + } -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") async def make_buy_sell_orders(): - sym = Symbol(name='BTCUSD') + sym = Symbol(name="BTCUSD") sym_info = await sym.mt5.symbol_info(sym.name) dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point sl = sym_info.ask - dsl tp = sym_info.ask + dsl - req = {'action': sym.mt5.TRADE_ACTION_DEAL, 'symbol': sym.name, 'volume': sym_info.volume_min, - 'type': sym.mt5.ORDER_TYPE_BUY, 'price': sym_info.ask, 'sl': sl, 'tp': tp} + req = { + "action": sym.mt5.TRADE_ACTION_DEAL, + "symbol": sym.name, + "volume": sym_info.volume_min, + "type": sym.mt5.ORDER_TYPE_BUY, + "price": sym_info.ask, + "sl": sl, + "tp": tp, + } await sym.mt5.order_send(req) - req['type'] = sym.mt5.ORDER_TYPE_SELL - req['price'] = sym_info.bid - req['sl'] = sym_info.bid + dsl - req['tp'] = sym_info.bid - dsl + req["type"] = sym.mt5.ORDER_TYPE_SELL + req["price"] = sym_info.bid + req["sl"] = sym_info.bid + dsl + req["tp"] = sym_info.bid - dsl await sym.mt5.order_send(req) diff --git a/tests/live/conftest.py b/tests/live/conftest.py index 5fbc35e..5f48369 100644 --- a/tests/live/conftest.py +++ b/tests/live/conftest.py @@ -13,10 +13,10 @@ logger = getLogger(__name__) async def cleanup(): try: - shutil.rmtree(Path('tests/live/configs'), ignore_errors=True) - Path.unlink(Path('tests/live/test.json'), missing_ok=True) - shutil.rmtree(Path('tests/live/trade_records'), ignore_errors=True) - shutil.rmtree(Path('tests/live/backtesting'), ignore_errors=True) + shutil.rmtree(Path("tests/live/configs"), ignore_errors=True) + Path.unlink(Path("tests/live/test.json"), missing_ok=True) + shutil.rmtree(Path("tests/live/trade_records"), ignore_errors=True) + shutil.rmtree(Path("tests/live/backtesting"), ignore_errors=True) await close_all_positions() await MetaTrader().shutdown() except Exception as err: @@ -29,28 +29,40 @@ async def close_all_positions(): positions = await mt.positions_get() tasks = [] for position in positions: - order_type = mt.ORDER_TYPE_BUY if position.type == mt.ORDER_TYPE_SELL else mt.ORDER_TYPE_SELL - req = {'action': mt.TRADE_ACTION_DEAL, 'symbol': position.symbol, 'volume': position.volume, - 'type': order_type, 'position': position.ticket, 'price': position.price_current} + order_type = ( + mt.ORDER_TYPE_BUY + if position.type == mt.ORDER_TYPE_SELL + else mt.ORDER_TYPE_SELL + ) + req = { + "action": mt.TRADE_ACTION_DEAL, + "symbol": position.symbol, + "volume": position.volume, + "type": order_type, + "position": position.ticket, + "price": position.price_current, + } tasks.append(mt.order_send(req)) await asyncio.gather(*tasks) except Exception as err: logger.error(f"Failed to close all positions: {err}") -@pytest.fixture(scope='package', autouse=True) +@pytest.fixture(scope="package", autouse=True) async def config(request): - Path('tests/live/configs').mkdir(exist_ok=True) - with open('aiomql.json', 'r') as fh, open('tests/live/configs/test2.json', 'w') as fh1, open('tests/live/test.json', 'w') as fh2: + Path("tests/live/configs").mkdir(exist_ok=True) + with open("aiomql.json", "r") as fh, open( + "tests/live/configs/test2.json", "w" + ) as fh1, open("tests/live/test.json", "w") as fh2: data = json.load(fh) json.dump(data, fh1, indent=2) json.dump(data, fh2, indent=2) - config = Config(filename='test.json', root='tests/live') + config = Config(root="tests/live", filename="test.json") yield config await cleanup() -@pytest.fixture(scope='package', autouse=True) +@pytest.fixture(scope="package", autouse=True) async def mt(): mt = MetaTrader() await mt.initialize() diff --git a/tests/live/integration/test_bot.py b/tests/live/integration/test_bot.py index 1eef137..1e72d08 100644 --- a/tests/live/integration/test_bot.py +++ b/tests/live/integration/test_bot.py @@ -6,17 +6,19 @@ from aiomql.contrib.symbols import ForexSymbol async def test_bot(): - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - syms = ['BTCUSD', 'SOLUSD', 'ETHUSD'] + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + syms = ["BTCUSD", "SOLUSD", "ETHUSD"] symbols = [ForexSymbol(name=sym) for sym in syms] - stgs = [Chaos(symbol=symbol, name='test_chaos') for symbol in symbols] + strategies = [Chaos(symbol=symbol, name="test_chaos") for symbol in symbols] bot = Bot() assert bot.config.shutdown is False - bot.executor.timeout = 30 - bot.add_strategies(strategies=stgs) + bot.executor.timeout = 10 + bot.add_strategies(strategies=strategies) await bot.initialize() await bot.executor.execute() - assert bot.executor.no_of_running_strategies == 3 assert len(bot.executor.coroutines) == 1 assert len(bot.executor.coroutine_threads) == 1 assert bot.config.shutdown is True diff --git a/tests/live/integration/test_results_records.py b/tests/live/integration/test_results_records.py index 2546e26..9278bcb 100644 --- a/tests/live/integration/test_results_records.py +++ b/tests/live/integration/test_results_records.py @@ -15,35 +15,59 @@ class TestRecordsAndResults: def setup_class(cls): cls.trade_records = TradeRecords() - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") async def buy(self, mt): - sym = 'BTCUSD' + sym = "BTCUSD" sym_info = await mt.symbol_info(sym) dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point sl = sym_info.ask - dsl tp = sym_info.ask + dsl - return {'action': mt.TRADE_ACTION_DEAL, 'symbol': sym, 'volume': sym_info.volume_min, - 'type': mt.ORDER_TYPE_BUY, 'price': sym_info.ask, 'sl': sl, 'tp': tp} + return { + "action": mt.TRADE_ACTION_DEAL, + "symbol": sym, + "volume": sym_info.volume_min, + "type": mt.ORDER_TYPE_BUY, + "price": sym_info.ask, + "sl": sl, + "tp": tp, + } - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") async def sell(self, mt): - sym = 'BTCUSD' + sym = "BTCUSD" sym_info = await mt.symbol_info(sym) - return {'action': mt.TRADE_ACTION_DEAL, 'symbol': sym, 'volume': sym_info.volume_min, - 'type': mt.ORDER_TYPE_SELL, 'price': sym_info.bid} + return { + "action": mt.TRADE_ACTION_DEAL, + "symbol": sym, + "volume": sym_info.volume_min, + "type": mt.ORDER_TYPE_SELL, + "price": sym_info.bid, + } - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) async def setup(self, sell, buy, mt): buy_res = await mt.order_send(buy) buy_res_2 = await mt.order_send(buy) sell_res = await mt.order_send(sell) sell_res_2 = await mt.order_send(sell) - buy_res = Result(result=OrderSendResult(**buy_res._asdict()), name='test_result') - sell_res = Result(result=OrderSendResult(**sell_res._asdict()), name='test_result') - sell_res_2 = Result(result=OrderSendResult(**sell_res_2._asdict()), name='test_result') - buy_res_2 = Result(result=OrderSendResult(**buy_res_2._asdict()), name='test_result') - await asyncio.gather(buy_res.save(), sell_res.save(), buy_res_2.save(trade_record_mode='json'), - sell_res_2.save(trade_record_mode='json')) + buy_res = Result( + result=OrderSendResult(**buy_res._asdict()), name="test_result" + ) + sell_res = Result( + result=OrderSendResult(**sell_res._asdict()), name="test_result" + ) + sell_res_2 = Result( + result=OrderSendResult(**sell_res_2._asdict()), name="test_result" + ) + buy_res_2 = Result( + result=OrderSendResult(**buy_res_2._asdict()), name="test_result" + ) + await asyncio.gather( + buy_res.save(), + sell_res.save(), + buy_res_2.save(trade_record_mode="json"), + sell_res_2.save(trade_record_mode="json"), + ) await Positions().close_all() def test_records_dir(self): @@ -66,29 +90,33 @@ class TestRecordsAndResults: async def test_json_records(self): json_records = self.trade_records.get_json_records() - matched_recs = [record for record in json_records if record.match('test_result.json')] + matched_recs = [ + record for record in json_records if record.match("test_result.json") + ] assert len(matched_recs) == 1 record = matched_recs[0] record_data = json.load(record.open()) assert isinstance(record_data, list) assert len(record_data) == 2 - is_open = [data['closed'] is False for data in record_data] + is_open = [data["closed"] is False for data in record_data] assert all(is_open) await self.trade_records.update_json_records() - is_close = [data['closed'] is True for data in record_data] + is_close = [data["closed"] is True for data in record_data] assert len(is_close) == 2 async def test_csv_records(self): csv_records = self.trade_records.get_csv_records() - matched_recs = [record for record in csv_records if record.match('test_result.csv')] + matched_recs = [ + record for record in csv_records if record.match("test_result.csv") + ] assert len(matched_recs) == 1 record = matched_recs[0] record_data = DictReader(record.open()) record_data = [row for row in record_data] assert isinstance(record_data, list) assert len(record_data) == 2 - is_open = [data['closed'].title() == 'False' for data in record_data] + is_open = [data["closed"].title() == "False" for data in record_data] assert all(is_open) await self.trade_records.update_json_records() - is_close = [data['closed'].title() == 'True' for data in record_data] + is_close = [data["closed"].title() == "True" for data in record_data] assert len(is_close) == 2 diff --git a/tests/live/unit/test_account.py b/tests/live/unit/test_account.py index 6f27daf..a58a43f 100644 --- a/tests/live/unit/test_account.py +++ b/tests/live/unit/test_account.py @@ -1,13 +1,13 @@ import pytest from aiomql.lib.account import Account -class TestAccount: +class TestAccount: @classmethod def setup_class(cls): cls.account = Account() - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) async def refresh(self): await self.account.refresh() diff --git a/tests/live/unit/test_backtest_engine.py b/tests/live/unit/test_backtest_engine.py index 7d42d2c..7c9bc2f 100644 --- a/tests/live/unit/test_backtest_engine.py +++ b/tests/live/unit/test_backtest_engine.py @@ -1,9 +1,9 @@ from datetime import datetime, UTC from aiomql import TimeFrame -from aiomql.contrib.backtesting import BackTestEngine -from aiomql.contrib.backtesting.get_data import GetData -from aiomql._utils import round_down, round_up +from aiomql.core.backtesting import BackTestEngine +from aiomql.core.backtesting.get_data import GetData +from aiomql._utils import round_down from aiomql.core.constants import OrderType, TradeAction import pytest @@ -14,49 +14,79 @@ class TestBackTestEngine: def setup_class(cls): cls.start = datetime(2024, 2, 1) cls.end = datetime(2024, 2, 7) - cls.g_data = GetData(start=cls.start, end=cls.end, symbols=['BTCUSD', 'SOLUSD'], - timeframes=[TimeFrame.H1, TimeFrame.H2], name='test_engine') - cls.bte = BackTestEngine(start=cls.start, end=cls.end) + cls.g_data = GetData( + start=cls.start, + end=cls.end, + symbols=["BTCUSD", "SOLUSD"], + timeframes=[TimeFrame.H1, TimeFrame.H2], + name="test_engine", + ) + cls.bte = BackTestEngine( + start=cls.start, end=cls.end, assign_to_config=True, preload=False + ) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") async def bte2(self): await self.g_data.get_data() - bte2 = BackTestEngine(start=self.start, end=self.end, data=self.g_data.data, use_terminal=False) + bte2 = BackTestEngine( + start=self.start, + end=self.end, + data=self.g_data.data, + use_terminal=False, + preload=False, + ) await bte2.setup_account(balance=100) return bte2 - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") async def sell_order(self): - sym = await self.bte.get_symbol_info(symbol='BTCUSD') - request = {'type': OrderType.SELL, 'symbol': 'BTCUSD', 'volume': sym.volume_min, - 'price': sym.bid, 'action': TradeAction.DEAL} + sym = await self.bte.get_symbol_info(symbol="BTCUSD") + request = { + "type": OrderType.SELL, + "symbol": "BTCUSD", + "volume": sym.volume_min, + "price": sym.bid, + "action": TradeAction.DEAL, + } return request - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") async def buy_order(self): - sym = await self.bte.get_symbol_info(symbol='BTCUSD') + sym = await self.bte.get_symbol_info(symbol="BTCUSD") dsl = (sym.trade_stops_level + sym.spread) * 2 * sym.point sl = sym.ask - dsl tp = sym.ask + dsl - request = {'type': OrderType.BUY, 'symbol': 'BTCUSD', 'volume': sym.volume_min, - 'price': sym.ask, 'action': TradeAction.DEAL, 'sl': sl, 'tp': tp} + request = { + "type": OrderType.BUY, + "symbol": "BTCUSD", + "volume": sym.volume_min, + "price": sym.ask, + "action": TradeAction.DEAL, + "sl": sl, + "tp": tp, + } return request def modify_stops(self, order): ... def test_span_and_range(self): - assert self.bte.range == range(0, int((self.end - self.start).total_seconds()), self.bte.speed) - assert self.bte.span == range(int(self.start.timestamp()), int(self.end.timestamp()), self.bte.speed) + assert self.bte.range == range( + 0, int((self.end - self.start).total_seconds()), self.bte.speed + ) + assert self.bte.span == range( + int(self.start.timestamp()), int(self.end.timestamp()), self.bte.speed + ) assert len(self.bte.span) == len(self.bte.range) def test_cursor(self): self.bte.next() r, t = self.bte.cursor self.bte.fast_forward(steps=100) - assert self.bte.cursor.time == t + 100 - assert self.bte.cursor.index == r + 100 - go_to = datetime(2024, 2, 3, tzinfo=UTC) + assert self.bte.cursor.time == t + 100 * self.bte.speed + assert self.bte.cursor.index == r + 100 * self.bte.speed + print(datetime.fromtimestamp(self.bte.cursor.time, tz=UTC), "test_cursor") + go_to = datetime(2024, 2, 6, tzinfo=UTC) self.bte.go_to(time=go_to) assert self.bte.cursor.time == int(datetime.timestamp(go_to)) self.bte.reset() @@ -72,7 +102,7 @@ class TestBackTestEngine: assert self.bte.cursor.index == index + 3600 assert self.bte.cursor.time == int(now.timestamp()) + 3600 self.bte.setup_test_range(start=self.start, end=self.end) - assert self.bte.speed == 1 + assert self.bte.speed == 60 async def test_account(self): await self.bte.setup_account(balance=100) @@ -122,12 +152,16 @@ class TestBackTestEngine: async def test_get_rates_from(self): start = datetime(2024, 2, 3, 12, 43, tzinfo=UTC) - rates = await self.bte.get_rates_from(symbol='BTCUSD', timeframe=TimeFrame.H1, date_from=start, count=24) + rates = await self.bte.get_rates_from( + symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24 + ) assert len(rates) == 24 async def test_get_rates_from_2(self, bte2): start = datetime(2024, 2, 3, 12, 12, tzinfo=UTC) - rates = await bte2.get_rates_from(symbol='BTCUSD', timeframe=TimeFrame.H1, date_from=start, count=24) + rates = await bte2.get_rates_from( + symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24 + ) assert len(rates) == 24 async def test_get_rates_from_pos(self): @@ -135,69 +169,91 @@ class TestBackTestEngine: self.bte.go_to(time=now) tf = TimeFrame.H2 start_pos = 2 - rates = await self.bte.get_rates_from_pos(symbol='BTCUSD', timeframe=tf, start_pos=start_pos, count=24) + rates = await self.bte.get_rates_from_pos( + symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24 + ) assert len(rates) == 24 - assert int(rates[-1][0]) == round_down(int(now.replace(hour = now.hour - start_pos).timestamp()), tf.seconds) + assert int(rates[-1][0]) == round_down( + int(now.replace(hour=now.hour - start_pos).timestamp()), tf.seconds + ) async def test_get_rates_from_pos2(self, bte2): now = datetime(2024, 2, 4, 12, 15, tzinfo=UTC) bte2.go_to(time=now) tf = TimeFrame.H1 start_pos = 2 - rates = await bte2.get_rates_from_pos(symbol='BTCUSD', timeframe=tf, start_pos=start_pos, count=24) - assert int(rates[-1][0]) == round_down(int(now.replace(hour=10).timestamp()), tf.seconds) + rates = await bte2.get_rates_from_pos( + symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24 + ) + assert int(rates[-1][0]) == round_down( + int(now.replace(hour=10).timestamp()), tf.seconds + ) # assert int(rates[-1][0]) == round_up(int(now.timestamp()), tf.seconds) - start_pos * tf.seconds assert len(rates) == 24 async def test_get_rates_range(self): start = datetime(2024, 2, 3, 12, tzinfo=UTC) end = datetime(2024, 2, 4, 18, tzinfo=UTC) - rates = await self.bte.get_rates_range(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end) + rates = await self.bte.get_rates_range( + symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end + ) assert len(rates) == 31 assert int(rates[-1][0]) == int(end.timestamp()) async def test_get_rates_range2(self, bte2): start = datetime(2024, 2, 3, 12, tzinfo=UTC) end = datetime(2024, 2, 4, 18, tzinfo=UTC) - rates = await bte2.get_rates_range(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end) + rates = await bte2.get_rates_range( + symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end + ) assert len(rates) == 31 assert int(rates[-1][0]) == int(end.timestamp()) async def test_get_ticks_from(self): start = datetime(2024, 2, 3, 12, tzinfo=UTC) - ticks = await self.bte.get_ticks_from(symbol='BTCUSD', date_from=start, count=24) + ticks = await self.bte.get_ticks_from( + symbol="BTCUSD", date_from=start, count=24 + ) assert len(ticks) == 24 async def test_get_ticks_from2(self, bte2): start = datetime(2024, 2, 3, 12, tzinfo=UTC) - ticks = await bte2.get_ticks_from(symbol='BTCUSD', date_from=start, count=24) + ticks = await bte2.get_ticks_from(symbol="BTCUSD", date_from=start, count=24) assert len(ticks) == 24 async def test_get_ticks_range(self): start = datetime(2024, 2, 3, 12, tzinfo=UTC) end = datetime(2024, 2, 3, 15, tzinfo=UTC) - ticks = await self.bte.get_ticks_range(symbol="BTCUSD", date_from=start, date_to=end) - approx_total = (end - start).total_seconds() // 2 # assuming 2 ticks per second at least + ticks = await self.bte.get_ticks_range( + symbol="BTCUSD", date_from=start, date_to=end + ) + approx_total = ( + end - start + ).total_seconds() // 2 # assuming 2 ticks per second at least assert len(ticks) >= approx_total async def test_get_ticks_range2(self, bte2): start = datetime(2024, 2, 3, 12, tzinfo=UTC) end = datetime(2024, 2, 3, 15, tzinfo=UTC) - ticks = await bte2.get_ticks_range(symbol="BTCUSD", date_from=start, date_to=end) - approx_total = (end - start).total_seconds() // 2 # assuming 2 ticks per second at least + ticks = await bte2.get_ticks_range( + symbol="BTCUSD", date_from=start, date_to=end + ) + approx_total = ( + end - start + ).total_seconds() // 2 # assuming 2 ticks per second at least assert len(ticks) >= approx_total async def test_price_tick(self, bte2): moment = datetime(2024, 2, 3, 12, 12, tzinfo=UTC) self.bte.reset() self.bte.go_to(time=moment) - tick = await self.bte.get_price_tick(symbol='BTCUSD', time=self.bte.cursor.time) + tick = await self.bte.get_price_tick(symbol="BTCUSD", time=self.bte.cursor.time) assert tick is not None assert isinstance(tick.ask, float) assert tick.ask > 0 bte2.reset() bte2.go_to(time=moment) - tick2 = await bte2.get_price_tick(symbol='BTCUSD', time=bte2.cursor.time) + tick2 = await bte2.get_price_tick(symbol="BTCUSD", time=bte2.cursor.time) assert tick.ask == tick2.ask async def test_get_symbol_info(self, bte2): @@ -206,7 +262,7 @@ class TestBackTestEngine: self.bte.go_to(time=moment) bte2.reset() bte2.go_to(time=moment) - sym = 'BTCUSD' + sym = "BTCUSD" sym_info = await self.bte.get_symbol_info(symbol=sym) assert sym_info is not None assert sym_info.name == sym @@ -219,21 +275,29 @@ class TestBackTestEngine: self.bte.go_to(time=moment) bte2.reset() bte2.go_to(time=moment) - sym = 'BTCUSD' + sym = "BTCUSD" sym_info = await self.bte.get_symbol_info(symbol=sym) dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point tp = sym_info.ask + dsl - profit = await self.bte.order_calc_profit(action=OrderType.BUY, symbol=sym, - volume=sym_info.volume_min, price_open=sym_info.ask, - price_close=tp) + profit = await self.bte.order_calc_profit( + action=OrderType.BUY, + symbol=sym, + volume=sym_info.volume_min, + price_open=sym_info.ask, + price_close=tp, + ) assert profit > 0 sym_info2 = await bte2.get_symbol_info(symbol=sym) dsl2 = (sym_info2.trade_stops_level + sym_info2.spread) * 2 * sym_info2.point tp2 = sym_info2.ask + dsl2 - profit2 = await bte2.order_calc_profit(action=OrderType.BUY, symbol=sym, - volume=sym_info2.volume_min, price_open=sym_info2.ask, - price_close=tp2) + profit2 = await bte2.order_calc_profit( + action=OrderType.BUY, + symbol=sym, + volume=sym_info2.volume_min, + price_open=sym_info2.ask, + price_close=tp2, + ) assert profit == profit2 async def test_order_margin(self, bte2): @@ -242,14 +306,22 @@ class TestBackTestEngine: self.bte.go_to(time=moment) bte2.reset() bte2.go_to(time=moment) - sym = 'BTCUSD' + sym = "BTCUSD" sym_info = await self.bte.get_symbol_info(symbol=sym) - margin = await self.bte.order_calc_margin(action=OrderType.SELL, symbol=sym, - volume=sym_info.volume_min, price=sym_info.bid) + margin = await self.bte.order_calc_margin( + action=OrderType.SELL, + symbol=sym, + volume=sym_info.volume_min, + price=sym_info.bid, + ) assert margin > 0 sym_info2 = await self.bte.get_symbol_info(symbol=sym) - margin2 = await bte2.order_calc_margin(action=OrderType.SELL, symbol=sym, - volume=sym_info2.volume_min, price=sym_info2.bid) + margin2 = await bte2.order_calc_margin( + action=OrderType.SELL, + symbol=sym, + volume=sym_info2.volume_min, + price=sym_info2.bid, + ) assert margin2 > 0 async def test_order_check(self, buy_order, sell_order): diff --git a/tests/live/unit/test_base.py b/tests/live/unit/test_base.py index 1349fee..4cbe612 100644 --- a/tests/live/unit/test_base.py +++ b/tests/live/unit/test_base.py @@ -20,22 +20,22 @@ class TestBaseClass: assert "attr2=test" in repr_str def test_set_attributes(self, child): - child.set_attributes(attr3=3.14, attr2='str') - assert child.attr2 == 'str' - assert getattr(child, 'attr3', None) is None + child.set_attributes(attr3=3.14, attr2="str") + assert child.attr2 == "str" + assert getattr(child, "attr3", None) is None def test_annotations(self, child): annotations = child.annotations assert isinstance(annotations, dict) def test_get_dict(self, child): - child.set_attributes(attr2='test') + child.set_attributes(attr2="test") result = child.get_dict() assert result["attr"] == 1 assert result["attr2"] == "test" def test_get_dict_with_exclude(self, child): - child.set_attributes(attr2='test') + child.set_attributes(attr2="test") result = child.get_dict(exclude={"attr"}) assert "attr" not in result assert result["attr2"] == "test" @@ -49,8 +49,8 @@ class TestBaseClass: def test_class_vars(self, child): class_vars = child.class_vars assert isinstance(class_vars, dict) - assert 'cls_attr' in class_vars - assert 'attr' not in class_vars + assert "cls_attr" in class_vars + assert "attr" not in class_vars def test_dict_property(self, child): child.set_attributes(attr2="test") diff --git a/tests/live/unit/test_bot_and_executor.py b/tests/live/unit/test_bot_and_executor.py index 227587c..925fb4a 100644 --- a/tests/live/unit/test_bot_and_executor.py +++ b/tests/live/unit/test_bot_and_executor.py @@ -10,7 +10,7 @@ class TestBotFactoryAndExecutor: def setup_class(cls): cls.bot = Bot() - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) async def initialize(self): self.bot.add_coroutine(coroutine=self.coro_one) self.bot.add_coroutine(coroutine=self.coro_two) @@ -20,24 +20,24 @@ class TestBotFactoryAndExecutor: @staticmethod def fun_one(): - print('function one') + print("function one") @staticmethod async def coro_thread(): while True: - print('coroutine thread') + print("coroutine thread") await asyncio.sleep(1) @staticmethod async def coro_one(): while True: - print('coroutine one') + print("coroutine one") await asyncio.sleep(1) @staticmethod async def coro_two(): while True: - print('coroutine two') + print("coroutine two") await asyncio.sleep(1) def test_add_workers(self): @@ -46,5 +46,4 @@ class TestBotFactoryAndExecutor: # task_queue already added coroutine_thread assert len(self.bot.executor.coroutine_threads) == 2 - # def diff --git a/tests/live/unit/test_candles.py b/tests/live/unit/test_candles.py index ddea024..f8825ca 100644 --- a/tests/live/unit/test_candles.py +++ b/tests/live/unit/test_candles.py @@ -11,8 +11,12 @@ from aiomql.core.constants import TimeFrame class TestCandle: @classmethod def setup_class(cls): - cls.bullish_candle = Candle(open=1.3421, high=1.3462, low=1.3405, close=1.3452, time=0, Index=0) - cls.bearish_candle = Candle(open=1.3452, high=1.3405, low=1.3462, close=1.3421, time=1, Index=1) + cls.bullish_candle = Candle( + open=1.3421, high=1.3462, low=1.3405, close=1.3452, time=0, Index=0 + ) + cls.bearish_candle = Candle( + open=1.3452, high=1.3405, low=1.3462, close=1.3421, time=1, Index=1 + ) def test_repr(self): repr_str = repr(self.bearish_candle) @@ -33,16 +37,16 @@ class TestCandle: def test_dict(self): self.bearish_candle.set_attributes(ema=10) - result = self.bearish_candle.dict(exclude={'time'}) - result2 = self.bearish_candle.dict(include={'close', 'high'}) - assert result['open'] == 1.3452 - assert result['ema'] == 10 - assert 'time' not in result - assert set(result2.keys()) == {'close', 'high'} + result = self.bearish_candle.dict(exclude={"time"}) + result2 = self.bearish_candle.dict(include={"close", "high"}) + assert result["open"] == 1.3452 + assert result["ema"] == 10 + assert "time" not in result + assert set(result2.keys()) == {"close", "high"} def test_dictionary_properties(self): - self.bearish_candle['ema'] = 4 - assert self.bearish_candle['ema'] == 4 + self.bearish_candle["ema"] = 4 + assert self.bearish_candle["ema"] == 4 def test_candle_type(self): assert self.bearish_candle.is_bearish() @@ -50,16 +54,15 @@ class TestCandle: class TestCandles: - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") async def candles(self): mt = MetaTrader() start = datetime(day=5, month=10, year=2023) rates = await mt.copy_rates_from("BTCUSD", mt.TIMEFRAME_H1, start, 200) return Candles(data=rates) - def test_get_series(self, candles): - series = candles['open'] + series = candles["open"] assert isinstance(series, pd.Series) assert len(series) == 200 @@ -77,13 +80,13 @@ class TestCandles: def test_setitem(self, candles): new_series = candles.open new_series = new_series * 2 - candles['double_open'] = new_series - assert 'double_open' in candles.data.columns + candles["double_open"] = new_series + assert "double_open" in candles.data.columns def test_getattr(self, candles): open_series = candles.open assert isinstance(open_series, pd.Series) - assert open_series.equals(candles.data['open']) + assert open_series.equals(candles.data["open"]) def test_iter(self, candles): l_5 = candles[-5:] @@ -94,13 +97,13 @@ class TestCandles: assert tf == TimeFrame.H1 def test_ta_and_rename(self, candles): - ema = candles.ta.ema(close='open', length=10, append=True) - assert 'EMA_10' in candles.data.columns - candles.rename(inplace=True, EMA_10='ema') - assert 'ema' in candles.data.columns + ema = candles.ta.ema(close="open", length=10, append=True) + assert "EMA_10" in candles.data.columns + candles.rename(inplace=True, EMA_10="ema") + assert "ema" in candles.data.columns def test_ta_lib(self, candles): fas = candles.ta_lib.above(candles.open, candles.close) assert isinstance(fas, pd.Series) - candles['fas'] = fas - assert 'fas' in candles.data.columns + candles["fas"] = fas + assert "fas" in candles.data.columns diff --git a/tests/live/unit/test_config.py b/tests/live/unit/test_config.py index 48d7e45..3b58b93 100644 --- a/tests/live/unit/test_config.py +++ b/tests/live/unit/test_config.py @@ -1,10 +1,10 @@ from aiomql.core.config import Config -from aiomql.contrib.backtesting import BackTestEngine +from aiomql.core.backtesting import BackTestEngine class TestConfig: def test_singleton(self, config): - config2 = Config(filename='test.json') + config2 = Config(filename="test.json") assert config is config2 def test_set_attributes(self, config): @@ -20,10 +20,10 @@ class TestConfig: def test_account_info(self, config): account_info = config.account_info() assert isinstance(account_info, dict) - assert 'login' in account_info - assert 'password' in account_info - assert 'server' in account_info + assert "login" in account_info + assert "password" in account_info + assert "server" in account_info def test_load_config(self, config): - config.load_config(file='tests/live/configs/test2.json') - assert config.filename == 'test2.json' + config.load_config(file="tests/live/configs/test2.json") + assert config.filename == "test2.json" diff --git a/tests/live/unit/test_get_data.py b/tests/live/unit/test_get_data.py index c763ec1..8aed8e8 100644 --- a/tests/live/unit/test_get_data.py +++ b/tests/live/unit/test_get_data.py @@ -3,7 +3,7 @@ from datetime import datetime, UTC import pytest -from aiomql.contrib.backtesting.get_data import GetData +from aiomql.core.backtesting.get_data import GetData from aiomql.core.constants import TimeFrame @@ -12,12 +12,17 @@ class TestGetData: def setup_class(cls): cls.start = datetime(2024, 2, 1, tzinfo=UTC) cls.end = datetime(2024, 2, 2, tzinfo=UTC) - cls.symbols = ['BTCUSD', "ETHUSD"] + cls.symbols = ["BTCUSD", "ETHUSD"] cls.timeframes = [TimeFrame.H1, TimeFrame.H2] - cls.g_data = GetData(start=cls.start, end=cls.end, symbols=cls.symbols, timeframes=cls.timeframes, - name='test_data') + cls.g_data = GetData( + start=cls.start, + end=cls.end, + symbols=cls.symbols, + timeframes=cls.timeframes, + name="test_data", + ) - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) async def get_data(self): await self.g_data.get_data() self.g_data.save_data() @@ -27,9 +32,11 @@ class TestGetData: assert self.g_data.end == self.end assert self.g_data.symbols == set(self.symbols) assert self.g_data.timeframes == set(self.timeframes) - assert self.g_data.name == 'test_data' + assert self.g_data.name == "test_data" assert self.g_data.range == range(int((self.end - self.start).total_seconds())) - assert self.g_data.span == range(int(self.start.timestamp()), int(self.end.timestamp())) + assert self.g_data.span == range( + int(self.start.timestamp()), int(self.end.timestamp()) + ) async def test_get_data(self): assert self.g_data.data.fully_loaded is True @@ -37,12 +44,12 @@ class TestGetData: assert len(self.g_data.data.symbols.keys()) == 2 async def test_save_data(self): - file = Path(self.g_data.config.backtest_dir / 'test_data.pkl') + file = Path(self.g_data.config.backtest_dir / "test_data.pkl") assert file.exists() async def test_load_data(self): - data = GetData.load_data(name='tests/live/backtesting/test_data.pkl') - assert data.name == 'test_data' + data = GetData.load_data(name="tests/live/backtesting/test_data.pkl") + assert data.name == "test_data" assert data.fully_loaded is True assert len(data.ticks.keys()) == 2 assert len(data.symbols.keys()) == 2 diff --git a/tests/live/unit/test_history.py b/tests/live/unit/test_history.py index 94b2da4..c8bef57 100644 --- a/tests/live/unit/test_history.py +++ b/tests/live/unit/test_history.py @@ -3,8 +3,9 @@ from datetime import datetime import pytest from aiomql.lib.history import History + class TestHistory: - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) async def init(self, make_buy_sell_orders): await self.history.initialize() diff --git a/tests/live/unit/test_meta_trader.py b/tests/live/unit/test_meta_trader.py index 9315f00..01becec 100644 --- a/tests/live/unit/test_meta_trader.py +++ b/tests/live/unit/test_meta_trader.py @@ -33,7 +33,7 @@ class TestMetaTrader: res = await self.mt.last_error() assert isinstance(res, tuple) assert res[0] == 1 - assert res[1] == 'Success' + assert res[1] == "Success" async def test_version(self): res = await self.mt.version() @@ -111,13 +111,19 @@ class TestMetaTrader: assert res.shape[0] == 10 async def test_copy_ticks_from(self): - res = await self.mt.copy_ticks_from(self.symbol, self.start, 10, self.mt.COPY_TICKS_ALL) + res = await self.mt.copy_ticks_from( + self.symbol, self.start, 10, self.mt.COPY_TICKS_ALL + ) assert res is not None assert res.shape[0] == 10 async def test_copy_ticks_range(self): - res = await self.mt.copy_ticks_range(self.symbol, self.start, self.end, self.mt.COPY_TICKS_ALL) - res2 = self.mt5.copy_ticks_range(self.symbol, self.start, self.end, self.mt5.COPY_TICKS_ALL) + res = await self.mt.copy_ticks_range( + self.symbol, self.start, self.end, self.mt.COPY_TICKS_ALL + ) + res2 = self.mt5.copy_ticks_range( + self.symbol, self.start, self.end, self.mt5.COPY_TICKS_ALL + ) assert res is not None assert res.shape[0] == res2.shape[0] @@ -132,18 +138,20 @@ class TestMetaTrader: assert len(res) == 0 async def test_order_calc_margin(self, sell_order): - price = sell_order['price'] - volume = sell_order['volume'] - type_ = sell_order['type'] + price = sell_order["price"] + volume = sell_order["volume"] + type_ = sell_order["type"] res = await self.mt.order_calc_margin(type_, self.symbol, volume, price) assert isinstance(res, float) async def test_order_calc_profit(self, buy_order): - volume = buy_order['volume'] - price_open = buy_order['price'] - price_close = buy_order['tp'] - type_ = buy_order['type'] - res = await self.mt.order_calc_profit(type_, self.symbol, volume, price_open, price_close) + volume = buy_order["volume"] + price_open = buy_order["price"] + price_close = buy_order["tp"] + type_ = buy_order["type"] + res = await self.mt.order_calc_profit( + type_, self.symbol, volume, price_open, price_close + ) assert isinstance(res, float) async def test_order_check(self, buy_order): diff --git a/tests/live/unit/test_positions.py b/tests/live/unit/test_positions.py index 75364f3..befa762 100644 --- a/tests/live/unit/test_positions.py +++ b/tests/live/unit/test_positions.py @@ -4,7 +4,7 @@ from aiomql.lib.positions import Positions class TestPositions: - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) async def init(self, make_buy_sell_orders): await self.positions.get_positions() diff --git a/tests/live/unit/test_ram.py b/tests/live/unit/test_ram.py index ca025f9..7282972 100644 --- a/tests/live/unit/test_ram.py +++ b/tests/live/unit/test_ram.py @@ -11,7 +11,7 @@ class TestRAM: assert self.ram.min_amount <= res <= self.ram.max_amount async def test_checks(self, buy_order, sell_order, mt): - for i in range(self.ram.open_limit+1): + for i in range(self.ram.open_limit + 1): if i % 2 == 0: await mt.order_send(buy_order) else: diff --git a/tests/live/unit/test_result.py b/tests/live/unit/test_result.py index aa34390..c1b7be0 100644 --- a/tests/live/unit/test_result.py +++ b/tests/live/unit/test_result.py @@ -7,12 +7,11 @@ from aiomql.core.models import OrderSendResult class TestResult: - - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def parameters(self): - return {'name': 'test_trades', 'ema': 20, 'rsi': 14} + return {"name": "test_trades", "ema": 20, "rsi": 14} - @pytest.fixture(scope='function') + @pytest.fixture(scope="function") async def order_results(self, mt, sell_order, buy_order, parameters): res1 = await mt.order_send(sell_order) res2 = await mt.order_send(buy_order) @@ -24,10 +23,9 @@ class TestResult: res1, res2 = order_results data1 = res1.get_data() data2 = res2.get_data() - assert data1['actual_profit'] == data2['actual_profit'] == 0 - assert data1['closed'] == data2['closed'] == False - assert data1['win'] == data2['win'] == False - + assert data1["actual_profit"] == data2["actual_profit"] == 0 + assert data1["closed"] == data2["closed"] == False + assert data1["win"] == data2["win"] == False async def test_csv(self, order_results): res1, res2 = order_results @@ -38,7 +36,9 @@ class TestResult: async def test_json(self, order_results): res1, res2 = order_results - await asyncio.gather(res1.save(trade_record_mode='json'), res2.save(trade_record_mode='json')) + await asyncio.gather( + res1.save(trade_record_mode="json"), res2.save(trade_record_mode="json") + ) assert res1.config.records_dir.exists() record = res1.config.records_dir / f"{res1.name}.json" assert record.exists() diff --git a/tests/live/unit/test_sessions.py b/tests/live/unit/test_sessions.py index bd6c07e..97ca8cd 100644 --- a/tests/live/unit/test_sessions.py +++ b/tests/live/unit/test_sessions.py @@ -6,25 +6,27 @@ from aiomql.lib.sessions import Session, Sessions, delta class TestSessions: - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def make_sessions(self, make_session): london, all_day, over_night = make_session return Sessions(sessions=[london, all_day, over_night]) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def make_session(self): - end = time(hour=16, minute=59, second=59, microsecond=999_999, tzinfo=UTC) - london = Session(start=8, end=end, name='London', on_end='close_all') - start, end = time(hour=0, tzinfo=UTC), time(hour=23, minute=59, second=59, tzinfo=UTC) - all_day = Session(start=start, end=end, name='AllDay', on_end='close_all') + end = time(hour=16, minute=59, second=59, microsecond=999_999, tzinfo=UTC) + london = Session(start=8, end=end, name="London", on_end="close_all") + start, end = time(hour=0, tzinfo=UTC), time( + hour=23, minute=59, second=59, tzinfo=UTC + ) + all_day = Session(start=start, end=end, name="AllDay", on_end="close_all") end = time(hour=6, minute=59, second=59, microsecond=999_999, tzinfo=UTC) - over_night = Session(start=18, end=end, name='OverNight', on_end='close_all') + over_night = Session(start=18, end=end, name="OverNight", on_end="close_all") return london, all_day, over_night def test_session_attributes(self, make_session): london, all_day, over_night = make_session period = over_night.duration() - assert london.name == 'London' + assert london.name == "London" assert london.start == time(hour=8, tzinfo=UTC) assert london.end.hour == 16 assert period.hours == 12 @@ -55,13 +57,13 @@ class TestSessions: no_sess = sessions.find(moment=time(hour=17, tzinfo=UTC)) mid_nite_sess = sessions.find(moment=mid_nite) current_sess = sessions.find(moment=now) - assert current_sess.name == 'OverNight' - assert noon_sess.name == 'London' + assert current_sess.name == "OverNight" + assert noon_sess.name == "London" assert no_sess is None - assert next_sess.name == 'London' - assert mid_nite_sess.name == 'OverNight' + assert next_sess.name == "London" + assert mid_nite_sess.name == "OverNight" current = datetime.now(UTC).time() if current.hour not in (7, 17): await sessions.check() assert sessions.current_session is not None - assert sessions.current_session.name in ('London', 'OverNight') + assert sessions.current_session.name in ("London", "OverNight") diff --git a/tests/live/unit/test_symbol.py b/tests/live/unit/test_symbol.py index e7069bf..7b4eb9e 100644 --- a/tests/live/unit/test_symbol.py +++ b/tests/live/unit/test_symbol.py @@ -8,16 +8,16 @@ from aiomql.lib.ticks import Ticks class TestSymbol: - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) async def btc(self): - symbol = Symbol(name='BTCUSD') - select = getattr(symbol, 'select', False) + symbol = Symbol(name="BTCUSD") + select = getattr(symbol, "select", False) if select is False: await symbol.initialize() return symbol async def test_symbol_attributes(self, btc): - assert btc.name == 'BTCUSD' + assert btc.name == "BTCUSD" assert btc.select is True assert btc.tick is not None @@ -35,13 +35,19 @@ class TestSymbol: async def test_rates(self, btc): 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) + 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) + 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) + 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) diff --git a/tests/live/unit/test_task_queue.py b/tests/live/unit/test_task_queue.py index ba83fc9..31d0cf4 100644 --- a/tests/live/unit/test_task_queue.py +++ b/tests/live/unit/test_task_queue.py @@ -7,21 +7,21 @@ class TestTaskQueue: @classmethod def setup_class(cls): cls.task_queue = TaskQueue(timeout=5, worker_timeout=1) - cls.data = {} + cls.data = {} async def task_one(self): for i in range(10): await asyncio.sleep(0.5) - self.data.setdefault('task_one', {})[i] = f"task_one_{i}" + self.data.setdefault("task_one", {})[i] = f"task_one_{i}" async def task_two(self): for i in range(10): await asyncio.sleep(0.5) - self.data.setdefault('task_two', {})[i] = f"task_two_{i}" + self.data.setdefault("task_two", {})[i] = f"task_two_{i}" async def task_three(self): for i in range(10): - self.data.setdefault('task_three', {})[i] = f"task_three_{i}" + self.data.setdefault("task_three", {})[i] = f"task_three_{i}" await asyncio.sleep(10) async def test_queue(self): @@ -34,7 +34,7 @@ class TestTaskQueue: assert self.task_queue.queue.qsize() == 2 self.task_queue.add(item=QueueItem(self.task_three), must_complete=False) await self.task_queue.run() - assert len(self.data['task_one']) >= 2 - assert len(self.data['task_two']) == 10 - assert len(self.data['task_three']) == 1 + assert len(self.data["task_one"]) >= 2 + assert len(self.data["task_two"]) == 10 + assert len(self.data["task_three"]) == 1 assert len(self.task_queue.priority_tasks) == 0 diff --git a/tests/live/unit/test_terminal.py b/tests/live/unit/test_terminal.py index 9d83cfc..7b8a96c 100644 --- a/tests/live/unit/test_terminal.py +++ b/tests/live/unit/test_terminal.py @@ -4,7 +4,7 @@ from aiomql.lib.terminal import Terminal class TestTerminal: - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) async def init_terminal(self): terminal = Terminal() init = await terminal.initialize() diff --git a/tests/live/unit/test_ticks.py b/tests/live/unit/test_ticks.py index 6d6799e..838f718 100644 --- a/tests/live/unit/test_ticks.py +++ b/tests/live/unit/test_ticks.py @@ -3,16 +3,17 @@ from datetime import datetime from aiomql.lib.ticks import Ticks, Tick from pandas import Series + class TestTicks: async def test_tick(self, mt): btc_tick = await mt.symbol_info_tick("BTCUSD") btc_tick = Tick(**btc_tick._asdict()) - tick_dict = btc_tick.dict(include={'ask', 'bid', 'time', 'volume'}) + tick_dict = btc_tick.dict(include={"ask", "bid", "time", "volume"}) assert isinstance(btc_tick, Tick) assert isinstance(tick_dict, dict) - assert 'ask' in tick_dict - assert 'bid' in tick_dict - assert 'volume_real' not in tick_dict + assert "ask" in tick_dict + assert "bid" in tick_dict + assert "volume_real" not in tick_dict async def test_ticks(self, mt): start = datetime(year=2023, month=10, day=5) @@ -21,6 +22,6 @@ class TestTicks: assert isinstance(ticks, Ticks) assert len(ticks) == 10 assert isinstance(ticks[0], Tick) - bids = ticks['bid'] + bids = ticks["bid"] assert len(bids) == 10 assert isinstance(bids, Series) diff --git a/tests/live/unit/test_trader.py b/tests/live/unit/test_trader.py index de80f2d..3fe9907 100644 --- a/tests/live/unit/test_trader.py +++ b/tests/live/unit/test_trader.py @@ -11,10 +11,10 @@ class TestTrader: @classmethod def setup_class(cls): ram = RAM(fixed_amount=10) - cls.trader = SimpleTrader(symbol=ForexSymbol(name='BTCUSD'), ram=ram) - cls.simple_trader2 = SimpleTrader(symbol=ForexSymbol(name='EURJPY'), ram=ram) + cls.trader = SimpleTrader(symbol=ForexSymbol(name="BTCUSD"), ram=ram) + cls.simple_trader2 = SimpleTrader(symbol=ForexSymbol(name="EURJPY"), ram=ram) - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) async def initialize(self): await self.trader.symbol.initialize() await self.simple_trader2.symbol.initialize() @@ -27,22 +27,26 @@ class TestTrader: assert res.retcode == 10009 async def test_create_order_with_sl(self): - sl = (self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread) * self.trader.symbol.point + sl = ( + self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread + ) * self.trader.symbol.point tick = await self.trader.symbol.info_tick() sl = tick.bid + sl await self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl) res = await self.trader.order.send() profit = floor(await self.trader.order.calc_profit()) loss = -floor(abs(await self.trader.order.calc_loss())) - assert profit == -loss*self.trader.ram.risk_to_reward + assert profit == -loss * self.trader.ram.risk_to_reward assert profit == self.trader.ram.fixed_amount * self.trader.ram.risk_to_reward assert loss == -self.trader.ram.fixed_amount assert res is not None assert res.retcode == 10009 async def test_create_order_with_points(self): - points = (self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread) - await self.trader.create_order_with_points(order_type=OrderType.BUY, points=points) + points = self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread + await self.trader.create_order_with_points( + order_type=OrderType.BUY, points=points + ) res = await self.trader.order.send() profit = floor(await self.trader.order.calc_profit()) loss = -floor(abs(await self.trader.order.calc_loss())) @@ -53,12 +57,16 @@ class TestTrader: assert res.retcode == 10009 async def test_create_order_with_stops(self): - sl = (self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread) * self.trader.symbol.point + sl = ( + self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread + ) * self.trader.symbol.point tp = sl * self.trader.ram.risk_to_reward tick = await self.trader.symbol.info_tick() sl = tick.ask - sl tp = tick.ask + tp - await self.trader.create_order_with_stops(order_type=OrderType.BUY, sl=sl, tp=tp) + await self.trader.create_order_with_stops( + order_type=OrderType.BUY, sl=sl, tp=tp + ) res = await self.trader.order.send() profit = floor(await self.trader.order.calc_profit()) loss = -floor(abs(await self.trader.order.calc_loss())) diff --git a/trade_records/Chaos.csv b/trade_records/Chaos.csv new file mode 100644 index 0000000..1620ea5 --- /dev/null +++ b/trade_records/Chaos.csv @@ -0,0 +1,560 @@ +ltf,order,closed,slow_ema,expected_profit,actual_profit,price,ask,name,symbol,htf,volume,deal,lcc,hcc,fast_ema,win,date,bid +TIMEFRAME_M1,886230009,False,20,0,0,6262.478,6262.652,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,154685316,100,100,8,False,2024-05-01 00:00:00.000000,6262.478 +TIMEFRAME_M1,818800346,False,20,0,0,1530.17,1530.17,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,199322348,100,100,8,False,2024-05-01 00:00:00.000000,1529.72 +TIMEFRAME_M1,824117887,False,20,0,0,213106.78,213158.32,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,152577421,100,100,8,False,2024-05-01 00:00:00.000000,213106.78 +TIMEFRAME_M1,840615159,False,20,0,0,2223.172,2223.324,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,148894471,100,100,8,False,2024-05-01 00:00:00.000000,2223.172 +TIMEFRAME_M1,832006417,False,20,0,0,2223.611,2223.611,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,157549838,100,100,8,False,2024-05-01 01:00:00.000000,2223.459 +TIMEFRAME_M1,891580450,False,20,0,0,213252.53,213252.53,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,189233562,100,100,8,False,2024-05-01 01:00:00.000000,213200.99 +TIMEFRAME_M1,883226922,False,20,0,0,6274.875,6274.875,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,116228913,100,100,8,False,2024-05-01 01:00:00.000000,6274.701 +TIMEFRAME_M1,825733295,False,20,0,0,1554.22,1554.67,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,133076187,100,100,8,False,2024-05-01 01:00:00.000000,1554.22 +TIMEFRAME_M1,835959975,False,20,0,0,6274.875,6274.875,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,151843042,100,100,8,False,2024-05-01 01:00:00.000000,6274.701 +TIMEFRAME_M1,899397720,False,20,0,0,213252.53,213252.53,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,159466123,100,100,8,False,2024-05-01 02:00:00.000000,213200.99 +TIMEFRAME_M1,832460780,False,20,0,0,1530.07,1530.52,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,195240829,100,100,8,False,2024-05-01 02:00:00.000000,1530.07 +TIMEFRAME_M1,848797971,False,20,0,0,2211.584,2211.736,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,181896201,100,100,8,False,2024-05-01 02:00:00.000000,2211.584 +TIMEFRAME_M1,812724764,False,20,0,0,2211.584,2211.736,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,144941102,100,100,8,False,2024-05-01 02:00:00.000000,2211.584 +TIMEFRAME_M1,879484673,False,20,0,0,1530.07,1530.52,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,143691551,100,100,8,False,2024-05-01 02:00:00.000000,1530.07 +TIMEFRAME_M1,847437487,False,20,0,0,214422.41,214422.41,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,154231964,100,100,8,False,2024-05-01 02:00:00.000000,214370.87 +TIMEFRAME_M1,854944510,False,20,0,0,6275.654,6275.654,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,197148138,100,100,8,False,2024-05-01 02:00:00.000000,6275.48 +TIMEFRAME_M1,802229749,False,20,0,0,1530.07,1530.52,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,195123454,100,100,8,False,2024-05-01 02:00:00.000000,1530.07 +TIMEFRAME_M1,804734589,False,20,0,0,214370.87,214422.41,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,136559114,100,100,8,False,2024-05-01 02:00:00.000000,214370.87 +TIMEFRAME_M1,847797400,False,20,0,0,6275.48,6275.654,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,104268697,100,100,8,False,2024-05-01 02:00:00.000000,6275.48 +TIMEFRAME_M1,826672754,False,20,0,0,2211.584,2211.736,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,188862104,100,100,8,False,2024-05-01 03:00:00.000000,2211.584 +TIMEFRAME_M1,815774872,False,20,0,0,6266.085,6266.259,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,191523865,100,100,8,False,2024-05-01 03:00:00.000000,6266.085 +TIMEFRAME_M1,879681465,False,20,0,0,1541.23,1541.23,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,134497288,100,100,8,False,2024-05-01 03:00:00.000000,1540.78 +TIMEFRAME_M1,842067711,False,20,0,0,2211.905,2212.057,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,137841048,100,100,8,False,2024-05-01 03:00:00.000000,2211.905 +TIMEFRAME_M1,815928725,False,20,0,0,214248.02,214248.02,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,159027141,100,100,8,False,2024-05-01 03:00:00.000000,214196.48 +TIMEFRAME_M1,815607591,False,20,0,0,6266.259,6266.259,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,185885364,100,100,8,False,2024-05-01 03:00:00.000000,6266.085 +TIMEFRAME_M1,878715323,False,20,0,0,214196.48,214248.02,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,156743080,100,100,8,False,2024-05-01 03:00:00.000000,214196.48 +TIMEFRAME_M1,831823605,False,20,0,0,2212.057,2212.057,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,106731482,100,100,8,False,2024-05-01 03:00:00.000000,2211.905 +TIMEFRAME_M1,893884954,False,20,0,0,1540.78,1541.23,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,188457468,100,100,8,False,2024-05-01 04:00:00.000000,1540.78 +TIMEFRAME_M1,800910450,False,20,0,0,214342.15,214393.69,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,112282997,100,100,8,False,2024-05-01 04:00:00.000000,214342.15 +TIMEFRAME_M1,852023795,False,20,0,0,1532.57,1532.57,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,140708487,100,100,8,False,2024-05-01 04:00:00.000000,1532.12 +TIMEFRAME_M1,897907042,False,20,0,0,6258.229,6258.229,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,176183081,100,100,8,False,2024-05-01 04:00:00.000000,6258.055 +TIMEFRAME_M1,866519633,False,20,0,0,2204.2,2204.2,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,104910229,100,100,8,False,2024-05-01 04:00:00.000000,2204.048 +TIMEFRAME_M1,884427519,False,20,0,0,2199.649,2199.649,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,153500677,100,100,8,False,2024-05-01 05:00:00.000000,2199.497 +TIMEFRAME_M1,853093161,False,20,0,0,1533.0,1533.0,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,172785584,100,100,8,False,2024-05-01 05:00:00.000000,1532.55 +TIMEFRAME_M1,837875222,False,20,0,0,6264.137,6264.311,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,103264472,100,100,8,False,2024-05-01 05:00:00.000000,6264.137 +TIMEFRAME_M1,839418936,False,20,0,0,214182.95,214234.49,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,132844149,100,100,8,False,2024-05-01 05:00:00.000000,214182.95 +TIMEFRAME_M1,866966782,False,20,0,0,2199.649,2199.649,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,105931959,100,100,8,False,2024-05-01 06:00:00.000000,2199.497 +TIMEFRAME_M1,824546643,False,20,0,0,6260.308,6260.308,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,111703394,100,100,8,False,2024-05-01 06:00:00.000000,6260.134 +TIMEFRAME_M1,820832123,False,20,0,0,212968.89,213020.43,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,139116290,100,100,8,False,2024-05-01 06:00:00.000000,212968.89 +TIMEFRAME_M1,862821647,False,20,0,0,1551.45,1551.9,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,191941564,100,100,8,False,2024-05-01 06:00:00.000000,1551.45 +TIMEFRAME_M1,848080053,False,20,0,0,2197.204,2197.204,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,116849443,100,100,8,False,2024-05-01 07:00:00.000000,2197.052 +TIMEFRAME_M1,855583070,False,20,0,0,1551.45,1551.9,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,190631650,100,100,8,False,2024-05-01 07:00:00.000000,1551.45 +TIMEFRAME_M1,883520444,False,20,0,0,214162.55,214214.09,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,196959652,100,100,8,False,2024-05-01 07:00:00.000000,214162.55 +TIMEFRAME_M1,804627400,False,20,0,0,6259.619,6259.793,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,153586985,100,100,8,False,2024-05-01 07:00:00.000000,6259.619 +TIMEFRAME_M1,814245907,False,20,0,0,6259.793,6259.793,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,129740397,100,100,8,False,2024-05-01 08:00:00.000000,6259.619 +TIMEFRAME_M1,822781904,False,20,0,0,1551.23,1551.23,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,192151339,100,100,8,False,2024-05-01 08:00:00.000000,1550.78 +TIMEFRAME_M1,874004780,False,20,0,0,2195.304,2195.456,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,170751978,100,100,8,False,2024-05-01 08:00:00.000000,2195.304 +TIMEFRAME_M1,895798958,False,20,0,0,213829.91,213829.91,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,157855134,100,100,8,False,2024-05-01 08:00:00.000000,213778.37 +TIMEFRAME_M1,875931816,False,20,0,0,213778.37,213829.91,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,117742584,100,100,8,False,2024-05-01 09:00:00.000000,213778.37 +TIMEFRAME_M1,837004097,False,20,0,0,6264.526,6264.7,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,143825092,100,100,8,False,2024-05-01 09:00:00.000000,6264.526 +TIMEFRAME_M1,817943945,False,20,0,0,2197.637,2197.637,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,177508656,100,100,8,False,2024-05-01 09:00:00.000000,2197.485 +TIMEFRAME_M1,877548537,False,20,0,0,1556.55,1556.55,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,182929760,100,100,8,False,2024-05-01 09:00:00.000000,1556.1 +TIMEFRAME_M1,829493020,False,20,0,0,6264.3,6264.3,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,148331296,100,100,8,False,2024-05-01 09:00:00.000000,6264.126 +TIMEFRAME_M1,810940641,False,20,0,0,1556.1,1556.55,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,116158522,100,100,8,False,2024-05-01 09:00:00.000000,1556.1 +TIMEFRAME_M1,833601811,False,20,0,0,2197.485,2197.637,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,182187936,100,100,8,False,2024-05-01 10:00:00.000000,2197.485 +TIMEFRAME_M1,834194989,False,20,0,0,210894.16,210894.16,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,167549556,100,100,8,False,2024-05-01 10:00:00.000000,210842.62 +TIMEFRAME_M1,873145527,False,20,0,0,1548.61,1548.61,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,165069584,100,100,8,False,2024-05-01 10:00:00.000000,1548.16 +TIMEFRAME_M1,877200295,False,20,0,0,2186.012,2186.164,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,136606585,100,100,8,False,2024-05-01 10:00:00.000000,2186.012 +TIMEFRAME_M1,860081944,False,20,0,0,6265.107,6265.107,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,163182382,100,100,8,False,2024-05-01 10:00:00.000000,6264.933 +TIMEFRAME_M1,825264955,False,20,0,0,210842.62,210894.16,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,193138027,100,100,8,False,2024-05-01 10:00:00.000000,210842.62 +TIMEFRAME_M1,820706991,False,20,0,0,210894.16,210894.16,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,151219573,100,100,8,False,2024-05-01 11:00:00.000000,210842.62 +TIMEFRAME_M1,876503235,False,20,0,0,2186.012,2186.164,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,172063067,100,100,8,False,2024-05-01 11:00:00.000000,2186.012 +TIMEFRAME_M1,833718813,False,20,0,0,6261.79,6261.79,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,173435482,100,100,8,False,2024-05-01 11:00:00.000000,6261.616 +TIMEFRAME_M1,895776702,False,20,0,0,1571.51,1571.51,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,196404389,100,100,8,False,2024-05-01 11:00:00.000000,1571.06 +TIMEFRAME_M1,885421811,False,20,0,0,1571.06,1571.51,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,123117071,100,100,8,False,2024-05-01 11:00:00.000000,1571.06 +TIMEFRAME_M1,864482696,False,20,0,0,2182.459,2182.459,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,175066340,100,100,8,False,2024-05-01 12:00:00.000000,2182.307 +TIMEFRAME_M1,841281606,False,20,0,0,6261.79,6261.79,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,133251524,100,100,8,False,2024-05-01 12:00:00.000000,6261.616 +TIMEFRAME_M1,876482228,False,20,0,0,211308.28,211308.28,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,145169261,100,100,8,False,2024-05-01 12:00:00.000000,211256.74 +TIMEFRAME_M1,825865721,False,20,0,0,6262.48,6262.48,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,157705520,100,100,8,False,2024-05-01 12:00:00.000000,6262.306 +TIMEFRAME_M1,816737054,False,20,0,0,1551.57,1551.57,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,139140641,100,100,8,False,2024-05-01 12:00:00.000000,1551.12 +TIMEFRAME_M1,834956670,False,20,0,0,211308.28,211308.28,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,171775221,100,100,8,False,2024-05-01 12:00:00.000000,211256.74 +TIMEFRAME_M1,825625533,False,20,0,0,2184.36,2184.36,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,129122029,100,100,8,False,2024-05-01 12:00:00.000000,2184.208 +TIMEFRAME_M1,803161306,False,20,0,0,6262.48,6262.48,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,117287266,100,100,8,False,2024-05-01 13:00:00.000000,6262.306 +TIMEFRAME_M1,884648919,False,20,0,0,211308.28,211308.28,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,114251981,100,100,8,False,2024-05-01 13:00:00.000000,211256.74 +TIMEFRAME_M1,819139407,False,20,0,0,2174.936,2175.088,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,176730700,100,100,8,False,2024-05-01 13:00:00.000000,2174.936 +TIMEFRAME_M1,865925662,False,20,0,0,1565.71,1565.71,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,189236759,100,100,8,False,2024-05-01 13:00:00.000000,1565.26 +TIMEFRAME_M1,892269334,False,20,0,0,6261.755,6261.929,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,159455069,100,100,8,False,2024-05-01 14:00:00.000000,6261.755 +TIMEFRAME_M1,869566314,False,20,0,0,211384.64,211384.64,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,100012743,100,100,8,False,2024-05-01 14:00:00.000000,211333.1 +TIMEFRAME_M1,818203027,False,20,0,0,1578.53,1578.53,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,107251637,100,100,8,False,2024-05-01 14:00:00.000000,1578.08 +TIMEFRAME_M1,837358875,False,20,0,0,2179.785,2179.785,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,157476716,100,100,8,False,2024-05-01 14:00:00.000000,2179.633 +TIMEFRAME_M1,873297859,False,20,0,0,2179.633,2179.785,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,199717246,100,100,8,False,2024-05-01 14:00:00.000000,2179.633 +TIMEFRAME_M1,839197865,False,20,0,0,6258.072,6258.246,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,118765057,100,100,8,False,2024-05-01 15:00:00.000000,6258.072 +TIMEFRAME_M1,853682529,False,20,0,0,209416.76,209468.3,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,134161139,100,100,8,False,2024-05-01 15:00:00.000000,209416.76 +TIMEFRAME_M1,804309542,False,20,0,0,1598.29,1598.74,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,198392205,100,100,8,False,2024-05-01 15:00:00.000000,1598.29 +TIMEFRAME_M1,842539853,False,20,0,0,2174.784,2174.784,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,164349594,100,100,8,False,2024-05-01 15:00:00.000000,2174.632 +TIMEFRAME_M1,866523974,False,20,0,0,1598.74,1598.74,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,111946513,100,100,8,False,2024-05-01 15:00:00.000000,1598.29 +TIMEFRAME_M1,865058306,False,20,0,0,209416.76,209468.3,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,119102412,100,100,8,False,2024-05-01 15:00:00.000000,209416.76 +TIMEFRAME_M1,851545021,False,20,0,0,6254.581,6254.581,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,140990609,100,100,8,False,2024-05-01 15:00:00.000000,6254.407 +TIMEFRAME_M1,800285874,False,20,0,0,6254.407,6254.581,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,160083401,100,100,8,False,2024-05-01 15:00:00.000000,6254.407 +TIMEFRAME_M1,805392867,False,20,0,0,209468.3,209468.3,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,154094537,100,100,8,False,2024-05-01 16:00:00.000000,209416.76 +TIMEFRAME_M1,818229286,False,20,0,0,1598.29,1598.74,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,186456339,100,100,8,False,2024-05-01 16:00:00.000000,1598.29 +TIMEFRAME_M1,837917107,False,20,0,0,2175.149,2175.301,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,109315421,100,100,8,False,2024-05-01 16:00:00.000000,2175.149 +TIMEFRAME_M1,897017708,False,20,0,0,209039.43,209090.97,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,167381126,100,100,8,False,2024-05-01 16:00:00.000000,209039.43 +TIMEFRAME_M1,801780706,False,20,0,0,2175.149,2175.301,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,187087789,100,100,8,False,2024-05-01 16:00:00.000000,2175.149 +TIMEFRAME_M1,823844234,False,20,0,0,1630.55,1630.55,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,165816278,100,100,8,False,2024-05-01 16:00:00.000000,1630.1 +TIMEFRAME_M1,816713934,False,20,0,0,6256.754,6256.754,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,102945279,100,100,8,False,2024-05-01 16:00:00.000000,6256.58 +TIMEFRAME_M1,803560402,False,20,0,0,1630.55,1630.55,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,179121491,100,100,8,False,2024-05-01 16:00:00.000000,1630.1 +TIMEFRAME_M1,892787673,False,20,0,0,209039.43,209090.97,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,157779224,100,100,8,False,2024-05-01 16:00:00.000000,209039.43 +TIMEFRAME_M1,869645943,False,20,0,0,2175.301,2175.301,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,123022389,100,100,8,False,2024-05-01 16:00:00.000000,2175.149 +TIMEFRAME_M1,862476644,False,20,0,0,6256.754,6256.754,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,138416328,100,100,8,False,2024-05-01 17:00:00.000000,6256.58 +TIMEFRAME_M1,896597218,False,20,0,0,211372.18,211423.72,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,119778573,100,100,8,False,2024-05-01 17:00:00.000000,211372.18 +TIMEFRAME_M1,894618868,False,20,0,0,1632.52,1632.52,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,100314831,100,100,8,False,2024-05-01 17:00:00.000000,1632.07 +TIMEFRAME_M1,850203651,False,20,0,0,6260.235,6260.409,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,193120879,100,100,8,False,2024-05-01 17:00:00.000000,6260.235 +TIMEFRAME_M1,834269318,False,20,0,0,2177.192,2177.192,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,158205709,100,100,8,False,2024-05-01 17:00:00.000000,2177.04 +TIMEFRAME_M1,843567767,False,20,0,0,2177.04,2177.192,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,181093259,100,100,8,False,2024-05-01 17:00:00.000000,2177.04 +TIMEFRAME_M1,866856991,False,20,0,0,6260.409,6260.409,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,110226922,100,100,8,False,2024-05-01 17:00:00.000000,6260.235 +TIMEFRAME_M1,810571750,False,20,0,0,1632.07,1632.52,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,189407662,100,100,8,False,2024-05-01 17:00:00.000000,1632.07 +TIMEFRAME_M1,801110858,False,20,0,0,211423.72,211423.72,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,174796512,100,100,8,False,2024-05-01 18:00:00.000000,211372.18 +TIMEFRAME_M1,855097767,False,20,0,0,209131.24,209131.24,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,158553193,100,100,8,False,2024-05-01 18:00:00.000000,209079.7 +TIMEFRAME_M1,890350910,False,20,0,0,6260.906,6260.906,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,115167157,100,100,8,False,2024-05-01 18:00:00.000000,6260.732 +TIMEFRAME_M1,829477511,False,20,0,0,2178.37,2178.522,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,181545172,100,100,8,False,2024-05-01 18:00:00.000000,2178.37 +TIMEFRAME_M1,845660659,False,20,0,0,1628.0,1628.45,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,131417310,100,100,8,False,2024-05-01 18:00:00.000000,1628.0 +TIMEFRAME_M1,881208716,False,20,0,0,1628.0,1628.45,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,193483996,100,100,8,False,2024-05-01 18:00:00.000000,1628.0 +TIMEFRAME_M1,888026508,False,20,0,0,6260.732,6260.906,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,155590401,100,100,8,False,2024-05-01 18:00:00.000000,6260.732 +TIMEFRAME_M1,884486895,False,20,0,0,2178.522,2178.522,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,104320247,100,100,8,False,2024-05-01 18:00:00.000000,2178.37 +TIMEFRAME_M1,831110890,False,20,0,0,209079.7,209131.24,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,110727148,100,100,8,False,2024-05-01 19:00:00.000000,209079.7 +TIMEFRAME_M1,823186284,False,20,0,0,208042.57,208042.57,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,192488546,100,100,8,False,2024-05-01 19:00:00.000000,207991.03 +TIMEFRAME_M1,884894496,False,20,0,0,1673.81,1673.81,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,172590967,100,100,8,False,2024-05-01 19:00:00.000000,1673.36 +TIMEFRAME_M1,847672522,False,20,0,0,6254.249,6254.249,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,196116972,100,100,8,False,2024-05-01 19:00:00.000000,6254.075 +TIMEFRAME_M1,847557012,False,20,0,0,2182.508,2182.66,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,147601202,100,100,8,False,2024-05-01 19:00:00.000000,2182.508 +TIMEFRAME_M1,861734192,False,20,0,0,6254.249,6254.249,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,178672626,100,100,8,False,2024-05-01 19:00:00.000000,6254.075 +TIMEFRAME_M1,879960485,False,20,0,0,207991.03,208042.57,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,127205999,100,100,8,False,2024-05-01 19:00:00.000000,207991.03 +TIMEFRAME_M1,829239662,False,20,0,0,2182.66,2182.66,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,149515483,100,100,8,False,2024-05-01 20:00:00.000000,2182.508 +TIMEFRAME_M1,858931495,False,20,0,0,1655.68,1656.13,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,195236511,100,100,8,False,2024-05-01 20:00:00.000000,1655.68 +TIMEFRAME_M1,806336879,False,20,0,0,1656.13,1656.13,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,103789705,100,100,8,False,2024-05-01 20:00:00.000000,1655.68 +TIMEFRAME_M1,873026231,False,20,0,0,208629.83,208681.37,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,142036712,100,100,8,False,2024-05-01 20:00:00.000000,208629.83 +TIMEFRAME_M1,865122610,False,20,0,0,2185.439,2185.591,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,127648763,100,100,8,False,2024-05-01 20:00:00.000000,2185.439 +TIMEFRAME_M1,899476905,False,20,0,0,6254.646,6254.646,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,137426042,100,100,8,False,2024-05-01 20:00:00.000000,6254.472 +TIMEFRAME_M1,813540562,False,20,0,0,2185.439,2185.591,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,182228280,100,100,8,False,2024-05-01 20:00:00.000000,2185.439 +TIMEFRAME_M1,876405434,False,20,0,0,208681.37,208681.37,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,119673020,100,100,8,False,2024-05-01 20:00:00.000000,208629.83 +TIMEFRAME_M1,887426160,False,20,0,0,1656.13,1656.13,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,190657483,100,100,8,False,2024-05-01 21:00:00.000000,1655.68 +TIMEFRAME_M1,856472276,False,20,0,0,6254.472,6254.646,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,187107012,100,100,8,False,2024-05-01 21:00:00.000000,6254.472 +TIMEFRAME_M1,814753722,False,20,0,0,2185.537,2185.689,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,187932097,100,100,8,False,2024-05-01 21:00:00.000000,2185.537 +TIMEFRAME_M1,813857415,False,20,0,0,6254.71,6254.884,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,139601944,100,100,8,False,2024-05-01 21:00:00.000000,6254.71 +TIMEFRAME_M1,852835151,False,20,0,0,206793.79,206845.33,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,187620790,100,100,8,False,2024-05-01 21:00:00.000000,206793.79 +TIMEFRAME_M1,835221379,False,20,0,0,1659.26,1659.26,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,134878804,100,100,8,False,2024-05-01 21:00:00.000000,1658.81 +TIMEFRAME_M1,812557321,False,20,0,0,1659.26,1659.26,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,109835979,100,100,8,False,2024-05-01 21:00:00.000000,1658.81 +TIMEFRAME_M1,801854518,False,20,0,0,206845.33,206845.33,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,120167653,100,100,8,False,2024-05-01 21:00:00.000000,206793.79 +TIMEFRAME_M1,838492734,False,20,0,0,6254.884,6254.884,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,112584669,100,100,8,False,2024-05-01 21:00:00.000000,6254.71 +TIMEFRAME_M1,814171531,False,20,0,0,2185.537,2185.689,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,120352320,100,100,8,False,2024-05-01 21:00:00.000000,2185.537 +TIMEFRAME_M1,820794630,False,20,0,0,6251.177,6251.351,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,169887822,100,100,8,False,2024-05-01 22:00:00.000000,6251.177 +TIMEFRAME_M1,871646189,False,20,0,0,2189.841,2189.993,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,157027480,100,100,8,False,2024-05-01 22:00:00.000000,2189.841 +TIMEFRAME_M1,801896894,False,20,0,0,206396.02,206447.56,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,186972656,100,100,8,False,2024-05-01 22:00:00.000000,206396.02 +TIMEFRAME_M1,881400160,False,20,0,0,1626.37,1626.37,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,147258576,100,100,8,False,2024-05-01 22:00:00.000000,1625.92 +TIMEFRAME_M1,836877692,False,20,0,0,1626.37,1626.37,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,183808251,100,100,8,False,2024-05-01 22:00:00.000000,1625.92 +TIMEFRAME_M1,858086064,False,20,0,0,206396.02,206447.56,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,120955599,100,100,8,False,2024-05-01 22:00:00.000000,206396.02 +TIMEFRAME_M1,886747168,False,20,0,0,2189.993,2189.993,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,168720960,100,100,8,False,2024-05-01 22:00:00.000000,2189.841 +TIMEFRAME_M1,864065076,False,20,0,0,6251.351,6251.351,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,108460864,100,100,8,False,2024-05-01 22:00:00.000000,6251.177 +TIMEFRAME_M1,833843635,False,20,0,0,6254.514,6254.688,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,182161781,100,100,8,False,2024-05-01 23:00:00.000000,6254.514 +TIMEFRAME_M1,886942098,False,20,0,0,2183.483,2183.635,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,129860311,100,100,8,False,2024-05-01 23:00:00.000000,2183.483 +TIMEFRAME_M1,834466498,False,20,0,0,1635.15,1635.15,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,165699481,100,100,8,False,2024-05-01 23:00:00.000000,1634.7 +TIMEFRAME_M1,809479093,False,20,0,0,205355.18,205355.18,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,111373512,100,100,8,False,2024-05-01 23:00:00.000000,205303.64 +TIMEFRAME_M1,809354492,False,20,0,0,205355.18,205355.18,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,166513788,100,100,8,False,2024-05-01 23:00:00.000000,205303.64 +TIMEFRAME_M1,884619296,False,20,0,0,2183.483,2183.635,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,106511457,100,100,8,False,2024-05-01 23:00:00.000000,2183.483 +TIMEFRAME_M1,816564138,False,20,0,0,6254.514,6254.688,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,160237481,100,100,8,False,2024-05-01 23:00:00.000000,6254.514 +TIMEFRAME_M1,810398437,False,20,0,0,1634.7,1635.15,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,164995297,100,100,8,False,2024-05-01 23:00:00.000000,1634.7 +TIMEFRAME_M1,813647457,False,20,0,0,2223.324,2223.324,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,164185406,100,100,8,False,2024-05-01 00:00:00.000000,2223.172 +TIMEFRAME_M1,891788918,False,20,0,0,213106.78,213158.32,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,159167300,100,100,8,False,2024-05-01 00:00:00.000000,213106.78 +TIMEFRAME_M1,875608093,False,20,0,0,1530.17,1530.17,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,120149568,100,100,8,False,2024-05-01 00:00:00.000000,1529.72 +TIMEFRAME_M1,878164158,False,20,0,0,6262.478,6262.652,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,157583052,100,100,8,False,2024-05-01 00:00:00.000000,6262.478 +TIMEFRAME_M1,858619681,False,20,0,0,6262.478,6262.652,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,194390818,100,100,8,False,2024-05-01 00:05:00.000000,6262.478 +TIMEFRAME_M1,809169812,False,20,0,0,213106.78,213158.32,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,132625939,100,100,8,False,2024-05-01 00:05:00.000000,213106.78 +TIMEFRAME_M1,849883480,False,20,0,0,1529.72,1530.17,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,143776572,100,100,8,False,2024-05-01 00:05:00.000000,1529.72 +TIMEFRAME_M1,897826859,False,20,0,0,2221.795,2221.947,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,180003087,100,100,8,False,2024-05-01 00:05:00.000000,2221.795 +TIMEFRAME_M1,831646695,False,20,0,0,213010.54,213062.08,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,180965885,100,100,8,False,2024-05-01 00:10:00.000000,213010.54 +TIMEFRAME_M1,806825464,False,20,0,0,6264.476,6264.476,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,187567595,100,100,8,False,2024-05-01 00:10:00.000000,6264.302 +TIMEFRAME_M1,851114862,False,20,0,0,2223.85,2224.002,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,108011123,100,100,8,False,2024-05-01 00:10:00.000000,2223.85 +TIMEFRAME_M1,805572079,False,20,0,0,1540.14,1540.59,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,138116860,100,100,8,False,2024-05-01 00:10:00.000000,1540.14 +TIMEFRAME_M1,896342911,False,20,0,0,1540.14,1540.59,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,189799108,100,100,8,False,2024-05-01 00:10:00.000000,1540.14 +TIMEFRAME_M1,801745796,False,20,0,0,6264.302,6264.476,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,115839986,100,100,8,False,2024-05-01 00:10:00.000000,6264.302 +TIMEFRAME_M1,835801457,False,20,0,0,213010.54,213062.08,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,104896198,100,100,8,False,2024-05-01 00:15:00.000000,213010.54 +TIMEFRAME_M1,869174078,False,20,0,0,2222.809,2222.961,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,158840486,100,100,8,False,2024-05-01 00:15:00.000000,2222.809 +TIMEFRAME_M1,806477906,False,20,0,0,6267.198,6267.198,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,158356405,100,100,8,False,2024-05-01 00:15:00.000000,6267.024 +TIMEFRAME_M1,831410886,False,20,0,0,1534.89,1534.89,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,185507630,100,100,8,False,2024-05-01 00:15:00.000000,1534.44 +TIMEFRAME_M1,889265719,False,20,0,0,6267.17,6267.344,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,185130318,100,100,8,False,2024-05-01 00:20:00.000000,6267.17 +TIMEFRAME_M1,878314774,False,20,0,0,1535.98,1536.43,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,122395479,100,100,8,False,2024-05-01 00:20:00.000000,1535.98 +TIMEFRAME_M1,899763471,False,20,0,0,2222.586,2222.586,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,122790912,100,100,8,False,2024-05-01 00:20:00.000000,2222.434 +TIMEFRAME_M1,899321316,False,20,0,0,211387.44,211387.44,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,187681881,100,100,8,False,2024-05-01 00:20:00.000000,211335.9 +TIMEFRAME_M1,896653163,False,20,0,0,2222.434,2222.586,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,175205216,100,100,8,False,2024-05-01 00:25:00.000000,2222.434 +TIMEFRAME_M1,826353499,False,20,0,0,210657.36,210708.9,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,190973717,100,100,8,False,2024-05-01 00:25:00.000000,210657.36 +TIMEFRAME_M1,873710066,False,20,0,0,6272.139,6272.139,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,122987216,100,100,8,False,2024-05-01 00:25:00.000000,6271.965 +TIMEFRAME_M1,845233268,False,20,0,0,1539.6,1539.6,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,170632583,100,100,8,False,2024-05-01 00:25:00.000000,1539.15 +TIMEFRAME_M1,809141306,False,20,0,0,6271.679,6271.853,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,114492217,100,100,8,False,2024-05-01 00:30:00.000000,6271.679 +TIMEFRAME_M1,875526824,False,20,0,0,2222.713,2222.713,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,130495696,100,100,8,False,2024-05-01 00:30:00.000000,2222.561 +TIMEFRAME_M1,804305576,False,20,0,0,211263.98,211315.52,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,132285811,100,100,8,False,2024-05-01 00:30:00.000000,211263.98 +TIMEFRAME_M1,818903696,False,20,0,0,1540.02,1540.47,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,147302725,100,100,8,False,2024-05-01 00:30:00.000000,1540.02 +TIMEFRAME_M1,867097858,False,20,0,0,212426.77,212478.31,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,186434884,100,100,8,False,2024-05-01 00:35:00.000000,212426.77 +TIMEFRAME_M1,858557209,False,20,0,0,1534.41,1534.41,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,160796676,100,100,8,False,2024-05-01 00:35:00.000000,1533.96 +TIMEFRAME_M1,806211700,False,20,0,0,2222.105,2222.257,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,134701994,100,100,8,False,2024-05-01 00:35:00.000000,2222.105 +TIMEFRAME_M1,861303468,False,20,0,0,6271.42,6271.594,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,188652261,100,100,8,False,2024-05-01 00:35:00.000000,6271.42 +TIMEFRAME_M1,897492124,False,20,0,0,1533.0,1533.0,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,166122372,100,100,8,False,2024-05-01 00:40:00.000000,1532.55 +TIMEFRAME_M1,897208780,False,20,0,0,2220.719,2220.719,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,144030361,100,100,8,False,2024-05-01 00:40:00.000000,2220.567 +TIMEFRAME_M1,834689906,False,20,0,0,6275.533,6275.707,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,111091179,100,100,8,False,2024-05-01 00:40:00.000000,6275.533 +TIMEFRAME_M1,839291782,False,20,0,0,212986.12,213037.66,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,139678412,100,100,8,False,2024-05-01 00:40:00.000000,212986.12 +TIMEFRAME_M1,833383481,False,20,0,0,212986.12,213037.66,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,111008665,100,100,8,False,2024-05-01 00:45:00.000000,212986.12 +TIMEFRAME_M1,823300139,False,20,0,0,1532.55,1533.0,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,144842348,100,100,8,False,2024-05-01 00:45:00.000000,1532.55 +TIMEFRAME_M1,846613454,False,20,0,0,2221.375,2221.375,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,155161900,100,100,8,False,2024-05-01 00:45:00.000000,2221.223 +TIMEFRAME_M1,885737508,False,20,0,0,6273.701,6273.875,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,114678159,100,100,8,False,2024-05-01 00:45:00.000000,6273.701 +TIMEFRAME_M1,829094718,False,20,0,0,1544.37,1544.37,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,141656062,100,100,8,False,2024-05-01 00:50:00.000000,1543.92 +TIMEFRAME_M1,803312811,False,20,0,0,213097.5,213149.04,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,118130188,100,100,8,False,2024-05-01 00:50:00.000000,213097.5 +TIMEFRAME_M1,824205887,False,20,0,0,6275.315,6275.315,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,129024534,100,100,8,False,2024-05-01 00:50:00.000000,6275.141 +TIMEFRAME_M1,835536615,False,20,0,0,2222.518,2222.67,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,101515692,100,100,8,False,2024-05-01 00:50:00.000000,2222.518 +TIMEFRAME_M1,877475681,False,20,0,0,2222.518,2222.67,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,191599422,100,100,8,False,2024-05-01 00:50:00.000000,2222.518 +TIMEFRAME_M1,845078615,False,20,0,0,1543.92,1544.37,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,112966990,100,100,8,False,2024-05-01 00:55:00.000000,1543.92 +TIMEFRAME_M1,853735984,False,20,0,0,6276.081,6276.255,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,125696414,100,100,8,False,2024-05-01 00:55:00.000000,6276.081 +TIMEFRAME_M1,860388916,False,20,0,0,213175.57,213227.11,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,113188178,100,100,8,False,2024-05-01 00:55:00.000000,213175.57 +TIMEFRAME_M1,881047860,False,20,0,0,2225.522,2225.522,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,104612902,100,100,8,False,2024-05-01 00:55:00.000000,2225.37 +TIMEFRAME_M1,807995774,False,20,0,0,213200.99,213252.53,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,117652518,100,100,8,False,2024-05-01 01:00:00.000000,213200.99 +TIMEFRAME_M1,822617777,False,20,0,0,2223.611,2223.611,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,195955126,100,100,8,False,2024-05-01 01:00:00.000000,2223.459 +TIMEFRAME_M1,885723455,False,20,0,0,1554.22,1554.67,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,154463490,100,100,8,False,2024-05-01 01:00:00.000000,1554.22 +TIMEFRAME_M1,884403906,False,20,0,0,6274.701,6274.875,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,137046181,100,100,8,False,2024-05-01 01:00:00.000000,6274.701 +TIMEFRAME_M1,878842659,False,20,0,0,1554.67,1554.67,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,150364516,100,100,8,False,2024-05-01 01:05:00.000000,1554.22 +TIMEFRAME_M1,880468319,False,20,0,0,2223.611,2223.611,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,120606133,100,100,8,False,2024-05-01 01:05:00.000000,2223.459 +TIMEFRAME_M1,827596248,False,20,0,0,213501.1,213501.1,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,133560473,100,100,8,False,2024-05-01 01:05:00.000000,213449.56 +TIMEFRAME_M1,836312656,False,20,0,0,6275.76,6275.76,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,108354424,100,100,8,False,2024-05-01 01:05:00.000000,6275.586 +TIMEFRAME_M1,889268848,False,20,0,0,2222.927,2222.927,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,142452980,100,100,8,False,2024-05-01 01:10:00.000000,2222.775 +TIMEFRAME_M1,869587474,False,20,0,0,1541.29,1541.74,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,162442913,100,100,8,False,2024-05-01 01:10:00.000000,1541.29 +TIMEFRAME_M1,863600301,False,20,0,0,6271.755,6271.929,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,137800260,100,100,8,False,2024-05-01 01:10:00.000000,6271.755 +TIMEFRAME_M1,832810234,False,20,0,0,213266.78,213266.78,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,119821302,100,100,8,False,2024-05-01 01:10:00.000000,213215.24 +TIMEFRAME_M1,899975465,False,20,0,0,1541.29,1541.74,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,123477105,100,100,8,False,2024-05-01 01:15:00.000000,1541.29 +TIMEFRAME_M1,892496463,False,20,0,0,6271.755,6271.929,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,170598836,100,100,8,False,2024-05-01 01:15:00.000000,6271.755 +TIMEFRAME_M1,884657960,False,20,0,0,2220.755,2220.755,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,158149617,100,100,8,False,2024-05-01 01:15:00.000000,2220.603 +TIMEFRAME_M1,836299997,False,20,0,0,213666.86,213718.4,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,134413258,100,100,8,False,2024-05-01 01:15:00.000000,213666.86 +TIMEFRAME_M1,801102995,False,20,0,0,213314.09,213314.09,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,110998080,100,100,8,False,2024-05-01 01:20:00.000000,213262.55 +TIMEFRAME_M1,831062354,False,20,0,0,1545.48,1545.48,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,181073555,100,100,8,False,2024-05-01 01:20:00.000000,1545.03 +TIMEFRAME_M1,827557116,False,20,0,0,2217.826,2217.978,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,150894441,100,100,8,False,2024-05-01 01:20:00.000000,2217.826 +TIMEFRAME_M1,868265326,False,20,0,0,6271.243,6271.243,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,194931811,100,100,8,False,2024-05-01 01:20:00.000000,6271.069 +TIMEFRAME_M1,812428764,False,20,0,0,6271.243,6271.243,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,140010496,100,100,8,False,2024-05-01 01:20:00.000000,6271.069 +TIMEFRAME_M1,842445809,False,20,0,0,213262.55,213314.09,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,153628132,100,100,8,False,2024-05-01 01:25:00.000000,213262.55 +TIMEFRAME_M1,874191732,False,20,0,0,2217.826,2217.978,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,113681223,100,100,8,False,2024-05-01 01:25:00.000000,2217.826 +TIMEFRAME_M1,853748062,False,20,0,0,1547.74,1547.74,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,139919667,100,100,8,False,2024-05-01 01:25:00.000000,1547.29 +TIMEFRAME_M1,800175196,False,20,0,0,6274.143,6274.143,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,191699989,100,100,8,False,2024-05-01 01:25:00.000000,6273.969 +TIMEFRAME_M1,827061779,False,20,0,0,1551.0,1551.45,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,150683965,100,100,8,False,2024-05-01 01:30:00.000000,1551.0 +TIMEFRAME_M1,888535035,False,20,0,0,6274.789,6274.789,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,145290823,100,100,8,False,2024-05-01 01:30:00.000000,6274.615 +TIMEFRAME_M1,830754629,False,20,0,0,213775.57,213775.57,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,173792535,100,100,8,False,2024-05-01 01:30:00.000000,213724.03 +TIMEFRAME_M1,836332132,False,20,0,0,2217.261,2217.413,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,154708278,100,100,8,False,2024-05-01 01:30:00.000000,2217.261 +TIMEFRAME_M1,834560145,False,20,0,0,213724.03,213775.57,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,147076403,100,100,8,False,2024-05-01 01:30:00.000000,213724.03 +TIMEFRAME_M1,846761444,False,20,0,0,2217.261,2217.413,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,114296197,100,100,8,False,2024-05-01 01:35:00.000000,2217.261 +TIMEFRAME_M1,809632936,False,20,0,0,1551.45,1551.45,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,176057964,100,100,8,False,2024-05-01 01:35:00.000000,1551.0 +TIMEFRAME_M1,829927352,False,20,0,0,6275.264,6275.264,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,188598447,100,100,8,False,2024-05-01 01:35:00.000000,6275.09 +TIMEFRAME_M1,857945556,False,20,0,0,213630.68,213682.22,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,124712442,100,100,8,False,2024-05-01 01:35:00.000000,213630.68 +TIMEFRAME_M1,807195183,False,20,0,0,2214.859,2214.859,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,152244561,100,100,8,False,2024-05-01 01:40:00.000000,2214.707 +TIMEFRAME_M1,803469424,False,20,0,0,214375.4,214426.94,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,109926468,100,100,8,False,2024-05-01 01:40:00.000000,214375.4 +TIMEFRAME_M1,862904712,False,20,0,0,6271.861,6272.035,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,181503350,100,100,8,False,2024-05-01 01:40:00.000000,6271.861 +TIMEFRAME_M1,825860406,False,20,0,0,1547.12,1547.57,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,178598075,100,100,8,False,2024-05-01 01:40:00.000000,1547.12 +TIMEFRAME_M1,817949111,False,20,0,0,214426.94,214426.94,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,118927225,100,100,8,False,2024-05-01 01:40:00.000000,214375.4 +TIMEFRAME_M1,876351577,False,20,0,0,1547.57,1547.57,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,192152635,100,100,8,False,2024-05-01 01:45:00.000000,1547.12 +TIMEFRAME_M1,836620378,False,20,0,0,2214.707,2214.859,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,122142728,100,100,8,False,2024-05-01 01:45:00.000000,2214.707 +TIMEFRAME_M1,893380828,False,20,0,0,6273.918,6273.918,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,188774368,100,100,8,False,2024-05-01 01:45:00.000000,6273.744 +TIMEFRAME_M1,848052305,False,20,0,0,214998.59,215050.13,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,144744745,100,100,8,False,2024-05-01 01:45:00.000000,214998.59 +TIMEFRAME_M1,858361557,False,20,0,0,2212.964,2213.116,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,172024529,100,100,8,False,2024-05-01 01:50:00.000000,2212.964 +TIMEFRAME_M1,882350356,False,20,0,0,214284.42,214284.42,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,105160845,100,100,8,False,2024-05-01 01:50:00.000000,214232.88 +TIMEFRAME_M1,886117590,False,20,0,0,1541.12,1541.57,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,199175438,100,100,8,False,2024-05-01 01:50:00.000000,1541.12 +TIMEFRAME_M1,820743140,False,20,0,0,6272.113,6272.113,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,133732745,100,100,8,False,2024-05-01 01:50:00.000000,6271.939 +TIMEFRAME_M1,883841275,False,20,0,0,6271.939,6272.113,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,153242805,100,100,8,False,2024-05-01 01:50:00.000000,6271.939 +TIMEFRAME_M1,870299384,False,20,0,0,214232.88,214284.42,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,119987852,100,100,8,False,2024-05-01 01:55:00.000000,214232.88 +TIMEFRAME_M1,884213594,False,20,0,0,2212.964,2213.116,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,168564850,100,100,8,False,2024-05-01 01:55:00.000000,2212.964 +TIMEFRAME_M1,871843905,False,20,0,0,1539.53,1539.53,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,135347120,100,100,8,False,2024-05-01 01:55:00.000000,1539.08 +TIMEFRAME_M1,863527190,False,20,0,0,6272.416,6272.416,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,194024592,100,100,8,False,2024-05-01 01:55:00.000000,6272.242 +TIMEFRAME_M1,864303882,False,20,0,0,6275.48,6275.654,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,108236657,100,100,8,False,2024-05-01 02:00:00.000000,6275.48 +TIMEFRAME_M1,893870307,False,20,0,0,2211.736,2211.736,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,125477888,100,100,8,False,2024-05-01 02:00:00.000000,2211.584 +TIMEFRAME_M1,851368116,False,20,0,0,214370.87,214422.41,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,147687039,100,100,8,False,2024-05-01 02:00:00.000000,214370.87 +TIMEFRAME_M1,870968196,False,20,0,0,1530.07,1530.52,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,182965202,100,100,8,False,2024-05-01 02:00:00.000000,1530.07 +TIMEFRAME_M1,876579331,False,20,0,0,1530.07,1530.52,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,116277352,100,100,8,False,2024-05-01 02:00:00.000000,1530.07 +TIMEFRAME_M1,807608270,False,20,0,0,6275.48,6275.654,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,190983997,100,100,8,False,2024-05-01 02:00:00.000000,6275.48 +TIMEFRAME_M1,855016139,False,20,0,0,214370.87,214422.41,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,190364393,100,100,8,False,2024-05-01 02:05:00.000000,214370.87 +TIMEFRAME_M1,882526415,False,20,0,0,2212.325,2212.325,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,158436629,100,100,8,False,2024-05-01 02:05:00.000000,2212.173 +TIMEFRAME_M1,888512570,False,20,0,0,6274.087,6274.087,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,155446611,100,100,8,False,2024-05-01 02:05:00.000000,6273.913 +TIMEFRAME_M1,875287739,False,20,0,0,1540.2,1540.2,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,102210902,100,100,8,False,2024-05-01 02:05:00.000000,1539.75 +TIMEFRAME_M1,857388555,False,20,0,0,1541.71,1541.71,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,133999873,100,100,8,False,2024-05-01 02:10:00.000000,1541.26 +TIMEFRAME_M1,853294326,False,20,0,0,6270.819,6270.993,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,108251081,100,100,8,False,2024-05-01 02:10:00.000000,6270.819 +TIMEFRAME_M1,885102997,False,20,0,0,213901.04,213952.58,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,142600335,100,100,8,False,2024-05-01 02:10:00.000000,213901.04 +TIMEFRAME_M1,830674337,False,20,0,0,2211.001,2211.153,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,155372510,100,100,8,False,2024-05-01 02:10:00.000000,2211.001 +TIMEFRAME_M1,807303904,False,20,0,0,2211.153,2211.153,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,155942430,100,100,8,False,2024-05-01 02:10:00.000000,2211.001 +TIMEFRAME_M1,816104240,False,20,0,0,213952.58,213952.58,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,188863495,100,100,8,False,2024-05-01 02:10:00.000000,213901.04 +TIMEFRAME_M1,867057630,False,20,0,0,1541.26,1541.71,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,109439383,100,100,8,False,2024-05-01 02:10:00.000000,1541.26 +TIMEFRAME_M1,847827718,False,20,0,0,6270.819,6270.993,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,171077785,100,100,8,False,2024-05-01 02:15:00.000000,6270.819 +TIMEFRAME_M1,835839887,False,20,0,0,2209.249,2209.249,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,123280961,100,100,8,False,2024-05-01 02:15:00.000000,2209.097 +TIMEFRAME_M1,887418483,False,20,0,0,213905.69,213905.69,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,190114228,100,100,8,False,2024-05-01 02:15:00.000000,213854.15 +TIMEFRAME_M1,833648793,False,20,0,0,1537.99,1537.99,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,156850161,100,100,8,False,2024-05-01 02:15:00.000000,1537.54 +TIMEFRAME_M1,843235603,False,20,0,0,1537.17,1537.62,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,126448099,100,100,8,False,2024-05-01 02:20:00.000000,1537.17 +TIMEFRAME_M1,807014723,False,20,0,0,6265.013,6265.013,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,114345720,100,100,8,False,2024-05-01 02:20:00.000000,6264.839 +TIMEFRAME_M1,840631087,False,20,0,0,214120.19,214120.19,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,118702907,100,100,8,False,2024-05-01 02:20:00.000000,214068.65 +TIMEFRAME_M1,843811533,False,20,0,0,2207.658,2207.81,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,181440065,100,100,8,False,2024-05-01 02:20:00.000000,2207.658 +TIMEFRAME_M1,859615738,False,20,0,0,1537.17,1537.62,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,121354830,100,100,8,False,2024-05-01 02:20:00.000000,1537.17 +TIMEFRAME_M1,893453483,False,20,0,0,214068.65,214120.19,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,141394012,100,100,8,False,2024-05-01 02:20:00.000000,214068.65 +TIMEFRAME_M1,836656715,False,20,0,0,2207.658,2207.81,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,162224458,100,100,8,False,2024-05-01 02:20:00.000000,2207.658 +TIMEFRAME_M1,841760836,False,20,0,0,6263.501,6263.501,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,132536727,100,100,8,False,2024-05-01 02:25:00.000000,6263.327 +TIMEFRAME_M1,806610211,False,20,0,0,2207.75,2207.75,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,159139789,100,100,8,False,2024-05-01 02:25:00.000000,2207.598 +TIMEFRAME_M1,827139189,False,20,0,0,213821.59,213821.59,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,193111748,100,100,8,False,2024-05-01 02:25:00.000000,213770.05 +TIMEFRAME_M1,861094827,False,20,0,0,1537.14,1537.14,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,131486408,100,100,8,False,2024-05-01 02:25:00.000000,1536.69 +TIMEFRAME_M1,873690054,False,20,0,0,214506.75,214506.75,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,100596787,100,100,8,False,2024-05-01 02:30:00.000000,214455.21 +TIMEFRAME_M1,803634531,False,20,0,0,2210.111,2210.263,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,113078885,100,100,8,False,2024-05-01 02:30:00.000000,2210.111 +TIMEFRAME_M1,879692007,False,20,0,0,6262.748,6262.748,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,158002864,100,100,8,False,2024-05-01 02:30:00.000000,6262.574 +TIMEFRAME_M1,855160151,False,20,0,0,1535.39,1535.39,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,115100896,100,100,8,False,2024-05-01 02:30:00.000000,1534.94 +TIMEFRAME_M1,891982022,False,20,0,0,1534.94,1535.39,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,131300833,100,100,8,False,2024-05-01 02:30:00.000000,1534.94 +TIMEFRAME_M1,898791958,False,20,0,0,2210.263,2210.263,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,138254850,100,100,8,False,2024-05-01 02:30:00.000000,2210.111 +TIMEFRAME_M1,892304980,False,20,0,0,6262.748,6262.748,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,106192985,100,100,8,False,2024-05-01 02:30:00.000000,6262.574 +TIMEFRAME_M1,816742590,False,20,0,0,214506.75,214506.75,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,102686563,100,100,8,False,2024-05-01 02:35:00.000000,214455.21 +TIMEFRAME_M1,848417692,False,20,0,0,6263.802,6263.802,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,144854349,100,100,8,False,2024-05-01 02:35:00.000000,6263.628 +TIMEFRAME_M1,883909444,False,20,0,0,1530.98,1530.98,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,111325839,100,100,8,False,2024-05-01 02:35:00.000000,1530.53 +TIMEFRAME_M1,886430419,False,20,0,0,2210.46,2210.46,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,156362502,100,100,8,False,2024-05-01 02:35:00.000000,2210.308 +TIMEFRAME_M1,883781430,False,20,0,0,1542.37,1542.37,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,136145966,100,100,8,False,2024-05-01 02:40:00.000000,1541.92 +TIMEFRAME_M1,869800120,False,20,0,0,6264.834,6264.834,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,197090536,100,100,8,False,2024-05-01 02:40:00.000000,6264.66 +TIMEFRAME_M1,886804375,False,20,0,0,213943.01,213994.55,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,191003529,100,100,8,False,2024-05-01 02:40:00.000000,213943.01 +TIMEFRAME_M1,869833004,False,20,0,0,2211.851,2212.003,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,185359973,100,100,8,False,2024-05-01 02:40:00.000000,2211.851 +TIMEFRAME_M1,862086197,False,20,0,0,6264.834,6264.834,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,140178779,100,100,8,False,2024-05-01 02:40:00.000000,6264.66 +TIMEFRAME_M1,835457997,False,20,0,0,1541.92,1542.37,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,105665039,100,100,8,False,2024-05-01 02:40:00.000000,1541.92 +TIMEFRAME_M1,810581432,False,20,0,0,2212.003,2212.003,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,182058923,100,100,8,False,2024-05-01 02:40:00.000000,2211.851 +TIMEFRAME_M1,840509593,False,20,0,0,213994.55,213994.55,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,150500876,100,100,8,False,2024-05-01 02:40:00.000000,213943.01 +TIMEFRAME_M1,848175833,False,20,0,0,6265.628,6265.628,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,195192503,100,100,8,False,2024-05-01 02:45:00.000000,6265.454 +TIMEFRAME_M1,804849962,False,20,0,0,213381.59,213381.59,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,102649946,100,100,8,False,2024-05-01 02:45:00.000000,213330.05 +TIMEFRAME_M1,845920662,False,20,0,0,2211.739,2211.891,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,119388740,100,100,8,False,2024-05-01 02:45:00.000000,2211.739 +TIMEFRAME_M1,890953003,False,20,0,0,1540.82,1540.82,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,145133600,100,100,8,False,2024-05-01 02:45:00.000000,1540.37 +TIMEFRAME_M1,833472796,False,20,0,0,6264.073,6264.247,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,124043190,100,100,8,False,2024-05-01 02:50:00.000000,6264.073 +TIMEFRAME_M1,884069400,False,20,0,0,1538.86,1538.86,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,111347920,100,100,8,False,2024-05-01 02:50:00.000000,1538.41 +TIMEFRAME_M1,849393840,False,20,0,0,213569.33,213569.33,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,115103677,100,100,8,False,2024-05-01 02:50:00.000000,213517.79 +TIMEFRAME_M1,882006833,False,20,0,0,2214.006,2214.158,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,183786604,100,100,8,False,2024-05-01 02:50:00.000000,2214.006 +TIMEFRAME_M1,865785906,False,20,0,0,2214.006,2214.158,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,105800841,100,100,8,False,2024-05-01 02:50:00.000000,2214.006 +TIMEFRAME_M1,881935501,False,20,0,0,1538.41,1538.86,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,110812559,100,100,8,False,2024-05-01 02:50:00.000000,1538.41 +TIMEFRAME_M1,816381287,False,20,0,0,6264.073,6264.247,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,198698612,100,100,8,False,2024-05-01 02:50:00.000000,6264.073 +TIMEFRAME_M1,831337439,False,20,0,0,213569.33,213569.33,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,160098949,100,100,8,False,2024-05-01 02:55:00.000000,213517.79 +TIMEFRAME_M1,837527099,False,20,0,0,1538.11,1538.56,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,181578776,100,100,8,False,2024-05-01 02:55:00.000000,1538.11 +TIMEFRAME_M1,831923656,False,20,0,0,2213.029,2213.029,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,180311728,100,100,8,False,2024-05-01 02:55:00.000000,2212.877 +TIMEFRAME_M1,873930819,False,20,0,0,6264.196,6264.37,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,192626625,100,100,8,False,2024-05-01 02:55:00.000000,6264.196 +TIMEFRAME_M1,825090459,False,20,0,0,2211.905,2212.057,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,137743625,100,100,8,False,2024-05-01 03:00:00.000000,2211.905 +TIMEFRAME_M1,842650222,False,20,0,0,6266.085,6266.259,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,166368050,100,100,8,False,2024-05-01 03:00:00.000000,6266.085 +TIMEFRAME_M1,863766065,False,20,0,0,214248.02,214248.02,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,179586815,100,100,8,False,2024-05-01 03:00:00.000000,214196.48 +TIMEFRAME_M1,870920528,False,20,0,0,1541.23,1541.23,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,199448844,100,100,8,False,2024-05-01 03:00:00.000000,1540.78 +TIMEFRAME_M1,832573477,False,20,0,0,2212.057,2212.057,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,164682862,100,100,8,False,2024-05-01 03:00:00.000000,2211.905 +TIMEFRAME_M1,805442282,False,20,0,0,6266.085,6266.259,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,152588856,100,100,8,False,2024-05-01 03:00:00.000000,6266.085 +TIMEFRAME_M1,861024584,False,20,0,0,1540.78,1541.23,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,131211190,100,100,8,False,2024-05-01 03:05:00.000000,1540.78 +TIMEFRAME_M1,866309695,False,20,0,0,213923.36,213974.9,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,133444942,100,100,8,False,2024-05-01 03:05:00.000000,213923.36 +TIMEFRAME_M1,805305638,False,20,0,0,2211.542,2211.694,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,195366233,100,100,8,False,2024-05-01 03:05:00.000000,2211.542 +TIMEFRAME_M1,844729760,False,20,0,0,6266.49,6266.49,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,155697303,100,100,8,False,2024-05-01 03:05:00.000000,6266.316 +TIMEFRAME_M1,806979451,False,20,0,0,214172.29,214223.83,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,172871751,100,100,8,False,2024-05-01 03:10:00.000000,214172.29 +TIMEFRAME_M1,813811443,False,20,0,0,2211.066,2211.066,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,163694749,100,100,8,False,2024-05-01 03:10:00.000000,2210.914 +TIMEFRAME_M1,831073106,False,20,0,0,6265.342,6265.342,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,185477161,100,100,8,False,2024-05-01 03:10:00.000000,6265.168 +TIMEFRAME_M1,829862507,False,20,0,0,1551.68,1551.68,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,138687274,100,100,8,False,2024-05-01 03:10:00.000000,1551.23 +TIMEFRAME_M1,888324304,False,20,0,0,214172.29,214223.83,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,154012973,100,100,8,False,2024-05-01 03:10:00.000000,214172.29 +TIMEFRAME_M1,867385588,False,20,0,0,1551.23,1551.68,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,145713775,100,100,8,False,2024-05-01 03:10:00.000000,1551.23 +TIMEFRAME_M1,885826282,False,20,0,0,2210.914,2211.066,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,176460465,100,100,8,False,2024-05-01 03:10:00.000000,2210.914 +TIMEFRAME_M1,804619490,False,20,0,0,6265.342,6265.342,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,183673143,100,100,8,False,2024-05-01 03:10:00.000000,6265.168 +TIMEFRAME_M1,860699330,False,20,0,0,6265.585,6265.585,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,168898765,100,100,8,False,2024-05-01 03:15:00.000000,6265.411 +TIMEFRAME_M1,878293552,False,20,0,0,1540.46,1540.91,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,121029113,100,100,8,False,2024-05-01 03:15:00.000000,1540.46 +TIMEFRAME_M1,893597174,False,20,0,0,2209.702,2209.702,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,141997904,100,100,8,False,2024-05-01 03:15:00.000000,2209.55 +TIMEFRAME_M1,801369754,False,20,0,0,214474.87,214526.41,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,175951928,100,100,8,False,2024-05-01 03:15:00.000000,214474.87 +TIMEFRAME_M1,813272064,False,20,0,0,6264.258,6264.258,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,174547006,100,100,8,False,2024-05-01 03:20:00.000000,6264.084 +TIMEFRAME_M1,863654258,False,20,0,0,2210.9,2210.9,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,150719173,100,100,8,False,2024-05-01 03:20:00.000000,2210.748 +TIMEFRAME_M1,884764245,False,20,0,0,1541.01,1541.46,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,169862943,100,100,8,False,2024-05-01 03:20:00.000000,1541.01 +TIMEFRAME_M1,804850179,False,20,0,0,214029.83,214029.83,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,133482640,100,100,8,False,2024-05-01 03:20:00.000000,213978.29 +TIMEFRAME_M1,837489901,False,20,0,0,213978.29,214029.83,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,142098035,100,100,8,False,2024-05-01 03:20:00.000000,213978.29 +TIMEFRAME_M1,850559056,False,20,0,0,6264.258,6264.258,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,161568768,100,100,8,False,2024-05-01 03:20:00.000000,6264.084 +TIMEFRAME_M1,873061102,False,20,0,0,1541.46,1541.46,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,159362655,100,100,8,False,2024-05-01 03:20:00.000000,1541.01 +TIMEFRAME_M1,843827547,False,20,0,0,2210.748,2210.9,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,152544842,100,100,8,False,2024-05-01 03:20:00.000000,2210.748 +TIMEFRAME_M1,878469744,False,20,0,0,6262.373,6262.373,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,146965822,100,100,8,False,2024-05-01 03:25:00.000000,6262.199 +TIMEFRAME_M1,898612313,False,20,0,0,214836.22,214887.76,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,118646802,100,100,8,False,2024-05-01 03:25:00.000000,214836.22 +TIMEFRAME_M1,847334395,False,20,0,0,1543.97,1544.42,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,158700869,100,100,8,False,2024-05-01 03:25:00.000000,1543.97 +TIMEFRAME_M1,852002088,False,20,0,0,2207.509,2207.661,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,166416443,100,100,8,False,2024-05-01 03:25:00.000000,2207.509 +TIMEFRAME_M1,810424492,False,20,0,0,2207.553,2207.705,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,134736211,100,100,8,False,2024-05-01 03:30:00.000000,2207.553 +TIMEFRAME_M1,883671344,False,20,0,0,215264.51,215316.05,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,108238281,100,100,8,False,2024-05-01 03:30:00.000000,215264.51 +TIMEFRAME_M1,861242017,False,20,0,0,1543.45,1543.9,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,120581832,100,100,8,False,2024-05-01 03:30:00.000000,1543.45 +TIMEFRAME_M1,823127157,False,20,0,0,6262.855,6263.029,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,115491488,100,100,8,False,2024-05-01 03:30:00.000000,6262.855 +TIMEFRAME_M1,821341515,False,20,0,0,6262.855,6263.029,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,199760903,100,100,8,False,2024-05-01 03:30:00.000000,6262.855 +TIMEFRAME_M1,831007051,False,20,0,0,2207.705,2207.705,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,139708865,100,100,8,False,2024-05-01 03:30:00.000000,2207.553 +TIMEFRAME_M1,828692836,False,20,0,0,1543.9,1543.9,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,123266507,100,100,8,False,2024-05-01 03:30:00.000000,1543.45 +TIMEFRAME_M1,847920958,False,20,0,0,215316.05,215316.05,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,177672736,100,100,8,False,2024-05-01 03:30:00.000000,215264.51 +TIMEFRAME_M1,805669473,False,20,0,0,2206.332,2206.484,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,164308648,100,100,8,False,2024-05-01 03:35:00.000000,2206.332 +TIMEFRAME_M1,836460253,False,20,0,0,1540.9,1540.9,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,191778412,100,100,8,False,2024-05-01 03:35:00.000000,1540.45 +TIMEFRAME_M1,853071236,False,20,0,0,6261.518,6261.692,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,174502640,100,100,8,False,2024-05-01 03:35:00.000000,6261.518 +TIMEFRAME_M1,857419311,False,20,0,0,215063.1,215063.1,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,120444036,100,100,8,False,2024-05-01 03:35:00.000000,215011.56 +TIMEFRAME_M1,884905089,False,20,0,0,1540.07,1540.52,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,145194183,100,100,8,False,2024-05-01 03:40:00.000000,1540.07 +TIMEFRAME_M1,810491227,False,20,0,0,214134.66,214134.66,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,115171107,100,100,8,False,2024-05-01 03:40:00.000000,214083.12 +TIMEFRAME_M1,883050144,False,20,0,0,6261.269,6261.443,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,187496128,100,100,8,False,2024-05-01 03:40:00.000000,6261.269 +TIMEFRAME_M1,840067196,False,20,0,0,2201.881,2201.881,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,108002787,100,100,8,False,2024-05-01 03:40:00.000000,2201.729 +TIMEFRAME_M1,878585268,False,20,0,0,6261.269,6261.443,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,167026812,100,100,8,False,2024-05-01 03:40:00.000000,6261.269 +TIMEFRAME_M1,859539643,False,20,0,0,2201.729,2201.881,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,131734681,100,100,8,False,2024-05-01 03:40:00.000000,2201.729 +TIMEFRAME_M1,818235664,False,20,0,0,1540.07,1540.52,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,167325133,100,100,8,False,2024-05-01 03:40:00.000000,1540.07 +TIMEFRAME_M1,804186546,False,20,0,0,214083.12,214134.66,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,101594399,100,100,8,False,2024-05-01 03:40:00.000000,214083.12 +TIMEFRAME_M1,839967042,False,20,0,0,6259.828,6259.828,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,115299120,100,100,8,False,2024-05-01 03:45:00.000000,6259.654 +TIMEFRAME_M1,852462232,False,20,0,0,214729.09,214729.09,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,120785581,100,100,8,False,2024-05-01 03:45:00.000000,214677.55 +TIMEFRAME_M1,812933267,False,20,0,0,2201.266,2201.418,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,138685708,100,100,8,False,2024-05-01 03:45:00.000000,2201.266 +TIMEFRAME_M1,829392677,False,20,0,0,1535.46,1535.46,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,180763310,100,100,8,False,2024-05-01 03:45:00.000000,1535.01 +TIMEFRAME_M1,808513509,False,20,0,0,2200.73,2200.73,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,185004366,100,100,8,False,2024-05-01 03:50:00.000000,2200.578 +TIMEFRAME_M1,857229704,False,20,0,0,1537.04,1537.04,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,103926089,100,100,8,False,2024-05-01 03:50:00.000000,1536.59 +TIMEFRAME_M1,828601693,False,20,0,0,214621.54,214673.08,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,159318744,100,100,8,False,2024-05-01 03:50:00.000000,214621.54 +TIMEFRAME_M1,887321334,False,20,0,0,6258.097,6258.271,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,184733170,100,100,8,False,2024-05-01 03:50:00.000000,6258.097 +TIMEFRAME_M1,805427693,False,20,0,0,6258.271,6258.271,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,175112116,100,100,8,False,2024-05-01 03:50:00.000000,6258.097 +TIMEFRAME_M1,815128677,False,20,0,0,2200.73,2200.73,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,138826820,100,100,8,False,2024-05-01 03:50:00.000000,2200.578 +TIMEFRAME_M1,849062499,False,20,0,0,1536.59,1537.04,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,193861516,100,100,8,False,2024-05-01 03:50:00.000000,1536.59 +TIMEFRAME_M1,819061267,False,20,0,0,214621.54,214673.08,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,101843239,100,100,8,False,2024-05-01 03:50:00.000000,214621.54 +TIMEFRAME_M1,851853637,False,20,0,0,6258.023,6258.023,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,185376602,100,100,8,False,2024-05-01 03:55:00.000000,6257.849 +TIMEFRAME_M1,831402814,False,20,0,0,2202.863,2202.863,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,154909082,100,100,8,False,2024-05-01 03:55:00.000000,2202.711 +TIMEFRAME_M1,831392985,False,20,0,0,1532.69,1532.69,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,169502454,100,100,8,False,2024-05-01 03:55:00.000000,1532.24 +TIMEFRAME_M1,890329455,False,20,0,0,214843.02,214843.02,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,167096499,100,100,8,False,2024-05-01 03:55:00.000000,214791.48 +TIMEFRAME_M1,897641684,False,20,0,0,2204.048,2204.2,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,176425023,100,100,8,False,2024-05-01 04:00:00.000000,2204.048 +TIMEFRAME_M1,867097195,False,20,0,0,6258.229,6258.229,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,104170334,100,100,8,False,2024-05-01 04:00:00.000000,6258.055 +TIMEFRAME_M1,865170864,False,20,0,0,214342.15,214393.69,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,189426153,100,100,8,False,2024-05-01 04:00:00.000000,214342.15 +TIMEFRAME_M1,870356096,False,20,0,0,1532.57,1532.57,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,150982696,100,100,8,False,2024-05-01 04:00:00.000000,1532.12 +TIMEFRAME_M1,887500507,False,20,0,0,6258.229,6258.229,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,109534193,100,100,8,False,2024-05-01 04:00:00.000000,6258.055 +TIMEFRAME_M1,861173893,False,20,0,0,2204.048,2204.2,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,102185302,100,100,8,False,2024-05-01 04:00:00.000000,2204.048 +TIMEFRAME_M1,872684680,False,20,0,0,1532.12,1532.57,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,176758550,100,100,8,False,2024-05-01 04:00:00.000000,1532.12 +TIMEFRAME_M1,826029823,False,20,0,0,214393.69,214393.69,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,122504463,100,100,8,False,2024-05-01 04:00:00.000000,214342.15 +TIMEFRAME_M1,896399904,False,20,0,0,1539.45,1539.45,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,136609471,100,100,8,False,2024-05-01 04:05:00.000000,1539.0 +TIMEFRAME_M1,885278881,False,20,0,0,6260.28,6260.454,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,167243081,100,100,8,False,2024-05-01 04:05:00.000000,6260.28 +TIMEFRAME_M1,834210790,False,20,0,0,2203.457,2203.457,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,102550775,100,100,8,False,2024-05-01 04:05:00.000000,2203.305 +TIMEFRAME_M1,850946647,False,20,0,0,214895.67,214895.67,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,192668061,100,100,8,False,2024-05-01 04:05:00.000000,214844.13 +TIMEFRAME_M1,877240482,False,20,0,0,6260.652,6260.652,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,143101246,100,100,8,False,2024-05-01 04:10:00.000000,6260.478 +TIMEFRAME_M1,821803779,False,20,0,0,1537.82,1537.82,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,170069609,100,100,8,False,2024-05-01 04:10:00.000000,1537.37 +TIMEFRAME_M1,808644535,False,20,0,0,2205.158,2205.31,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,186780653,100,100,8,False,2024-05-01 04:10:00.000000,2205.158 +TIMEFRAME_M1,873597612,False,20,0,0,214200.41,214251.95,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,196471062,100,100,8,False,2024-05-01 04:10:00.000000,214200.41 +TIMEFRAME_M1,817128227,False,20,0,0,214200.41,214251.95,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,100136409,100,100,8,False,2024-05-01 04:10:00.000000,214200.41 +TIMEFRAME_M1,830779929,False,20,0,0,1537.82,1537.82,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,129012368,100,100,8,False,2024-05-01 04:10:00.000000,1537.37 +TIMEFRAME_M1,836415771,False,20,0,0,6260.478,6260.652,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,181718956,100,100,8,False,2024-05-01 04:10:00.000000,6260.478 +TIMEFRAME_M1,862242754,False,20,0,0,2205.31,2205.31,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,110959870,100,100,8,False,2024-05-01 04:10:00.000000,2205.158 +TIMEFRAME_M1,879506480,False,20,0,0,1539.86,1540.31,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,162733895,100,100,8,False,2024-05-01 04:15:00.000000,1539.86 +TIMEFRAME_M1,816361305,False,20,0,0,214778.27,214778.27,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,141595546,100,100,8,False,2024-05-01 04:15:00.000000,214726.73 +TIMEFRAME_M1,800224054,False,20,0,0,6262.227,6262.227,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,162218457,100,100,8,False,2024-05-01 04:15:00.000000,6262.053 +TIMEFRAME_M1,832861747,False,20,0,0,2206.322,2206.322,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,107972643,100,100,8,False,2024-05-01 04:15:00.000000,2206.17 +TIMEFRAME_M1,841625160,False,20,0,0,6265.903,6266.077,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,163174016,100,100,8,False,2024-05-01 04:20:00.000000,6265.903 +TIMEFRAME_M1,879253805,False,20,0,0,214606.73,214606.73,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,137499371,100,100,8,False,2024-05-01 04:20:00.000000,214555.19 +TIMEFRAME_M1,890363432,False,20,0,0,1540.38,1540.83,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,164633662,100,100,8,False,2024-05-01 04:20:00.000000,1540.38 +TIMEFRAME_M1,898691461,False,20,0,0,2205.756,2205.756,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,151002710,100,100,8,False,2024-05-01 04:20:00.000000,2205.604 +TIMEFRAME_M1,818288823,False,20,0,0,2205.604,2205.756,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,134323107,100,100,8,False,2024-05-01 04:20:00.000000,2205.604 +TIMEFRAME_M1,837477576,False,20,0,0,214606.73,214606.73,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,124587857,100,100,8,False,2024-05-01 04:20:00.000000,214555.19 +TIMEFRAME_M1,859955854,False,20,0,0,1540.83,1540.83,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,111783007,100,100,8,False,2024-05-01 04:20:00.000000,1540.38 +TIMEFRAME_M1,871183705,False,20,0,0,6265.903,6266.077,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,106918300,100,100,8,False,2024-05-01 04:20:00.000000,6265.903 +TIMEFRAME_M1,812298489,False,20,0,0,2206.056,2206.056,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,155316773,100,100,8,False,2024-05-01 04:25:00.000000,2205.904 +TIMEFRAME_M1,899060092,False,20,0,0,213984.27,213984.27,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,121929021,100,100,8,False,2024-05-01 04:25:00.000000,213932.73 +TIMEFRAME_M1,821456966,False,20,0,0,6267.131,6267.305,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,175486351,100,100,8,False,2024-05-01 04:25:00.000000,6267.131 +TIMEFRAME_M1,880313076,False,20,0,0,1537.44,1537.89,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,194425345,100,100,8,False,2024-05-01 04:25:00.000000,1537.44 +TIMEFRAME_M1,852560059,False,20,0,0,213936.09,213987.63,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,142870920,100,100,8,False,2024-05-01 04:30:00.000000,213936.09 +TIMEFRAME_M1,813431425,False,20,0,0,6267.036,6267.21,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,121715442,100,100,8,False,2024-05-01 04:30:00.000000,6267.036 +TIMEFRAME_M1,867583670,False,20,0,0,1537.53,1537.53,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,138079929,100,100,8,False,2024-05-01 04:30:00.000000,1537.08 +TIMEFRAME_M1,847390640,False,20,0,0,2204.47,2204.47,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,186739716,100,100,8,False,2024-05-01 04:30:00.000000,2204.318 +TIMEFRAME_M1,827593276,False,20,0,0,6267.036,6267.21,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,126542202,100,100,8,False,2024-05-01 04:30:00.000000,6267.036 +TIMEFRAME_M1,855440287,False,20,0,0,213987.63,213987.63,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,128571701,100,100,8,False,2024-05-01 04:30:00.000000,213936.09 +TIMEFRAME_M1,856715711,False,20,0,0,2204.318,2204.47,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,144770183,100,100,8,False,2024-05-01 04:30:00.000000,2204.318 +TIMEFRAME_M1,835135126,False,20,0,0,1537.53,1537.53,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,100557665,100,100,8,False,2024-05-01 04:30:00.000000,1537.08 +TIMEFRAME_M1,832116084,False,20,0,0,2201.381,2201.381,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,189423212,100,100,8,False,2024-05-01 04:35:00.000000,2201.229 +TIMEFRAME_M1,834042066,False,20,0,0,214126.89,214178.43,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,112752769,100,100,8,False,2024-05-01 04:35:00.000000,214126.89 +TIMEFRAME_M1,872298952,False,20,0,0,6265.871,6266.045,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,172305725,100,100,8,False,2024-05-01 04:35:00.000000,6265.871 +TIMEFRAME_M1,841424964,False,20,0,0,1545.47,1545.47,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,103844321,100,100,8,False,2024-05-01 04:35:00.000000,1545.02 +TIMEFRAME_M1,890693315,False,20,0,0,1545.55,1545.55,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,123203861,100,100,8,False,2024-05-01 04:40:00.000000,1545.1 +TIMEFRAME_M1,840260282,False,20,0,0,2200.932,2201.084,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,141429652,100,100,8,False,2024-05-01 04:40:00.000000,2200.932 +TIMEFRAME_M1,853643858,False,20,0,0,6266.575,6266.749,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,176961059,100,100,8,False,2024-05-01 04:40:00.000000,6266.575 +TIMEFRAME_M1,803365029,False,20,0,0,213367.04,213367.04,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,146051892,100,100,8,False,2024-05-01 04:40:00.000000,213315.5 +TIMEFRAME_M1,812936128,False,20,0,0,2201.084,2201.084,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,190086345,100,100,8,False,2024-05-01 04:40:00.000000,2200.932 +TIMEFRAME_M1,817474603,False,20,0,0,213315.5,213367.04,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,188514165,100,100,8,False,2024-05-01 04:40:00.000000,213315.5 +TIMEFRAME_M1,839545000,False,20,0,0,1545.1,1545.55,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,158300920,100,100,8,False,2024-05-01 04:40:00.000000,1545.1 +TIMEFRAME_M1,876833486,False,20,0,0,6266.575,6266.749,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,197429838,100,100,8,False,2024-05-01 04:40:00.000000,6266.575 +TIMEFRAME_M1,802142610,False,20,0,0,214017.89,214017.89,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,122511870,100,100,8,False,2024-05-01 04:45:00.000000,213966.35 +TIMEFRAME_M1,874455759,False,20,0,0,1541.45,1541.45,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,120250835,100,100,8,False,2024-05-01 04:45:00.000000,1541.0 +TIMEFRAME_M1,842995220,False,20,0,0,2200.502,2200.502,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,187488416,100,100,8,False,2024-05-01 04:45:00.000000,2200.35 +TIMEFRAME_M1,822109953,False,20,0,0,6263.114,6263.114,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,146886913,100,100,8,False,2024-05-01 04:45:00.000000,6262.94 +TIMEFRAME_M1,884738687,False,20,0,0,1536.47,1536.47,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,161585165,100,100,8,False,2024-05-01 04:50:00.000000,1536.02 +TIMEFRAME_M1,840267221,False,20,0,0,213417.96,213469.5,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,122453083,100,100,8,False,2024-05-01 04:50:00.000000,213417.96 +TIMEFRAME_M1,822769412,False,20,0,0,2200.421,2200.573,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,190777605,100,100,8,False,2024-05-01 04:50:00.000000,2200.421 +TIMEFRAME_M1,868986578,False,20,0,0,6259.307,6259.481,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,131780500,100,100,8,False,2024-05-01 04:50:00.000000,6259.307 +TIMEFRAME_M1,875075366,False,20,0,0,6259.481,6259.481,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,193455838,100,100,8,False,2024-05-01 04:50:00.000000,6259.307 +TIMEFRAME_M1,879567775,False,20,0,0,1536.02,1536.47,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,154068132,100,100,8,False,2024-05-01 04:50:00.000000,1536.02 +TIMEFRAME_M1,812994331,False,20,0,0,213417.96,213469.5,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,118752845,100,100,8,False,2024-05-01 04:50:00.000000,213417.96 +TIMEFRAME_M1,848171735,False,20,0,0,2200.573,2200.573,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,169652491,100,100,8,False,2024-05-01 04:50:00.000000,2200.421 +TIMEFRAME_M1,884918689,False,20,0,0,6264.612,6264.786,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,153358884,100,100,8,False,2024-05-01 04:55:00.000000,6264.612 +TIMEFRAME_M1,802236113,False,20,0,0,2201.834,2201.986,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,173311592,100,100,8,False,2024-05-01 04:55:00.000000,2201.834 +TIMEFRAME_M1,871049425,False,20,0,0,1535.75,1535.75,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,128642224,100,100,8,False,2024-05-01 04:55:00.000000,1535.3 +TIMEFRAME_M1,839461564,False,20,0,0,214381.96,214433.5,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,117116659,100,100,8,False,2024-05-01 04:55:00.000000,214381.96 +TIMEFRAME_M1,836561870,False,20,0,0,2199.497,2199.649,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,111676808,100,100,8,False,2024-05-01 05:00:00.000000,2199.497 +TIMEFRAME_M1,823785397,False,20,0,0,6264.311,6264.311,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,196728015,100,100,8,False,2024-05-01 05:00:00.000000,6264.137 +TIMEFRAME_M1,802063298,False,20,0,0,1533.0,1533.0,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,179274455,100,100,8,False,2024-05-01 05:00:00.000000,1532.55 +TIMEFRAME_M1,894167469,False,20,0,0,214234.49,214234.49,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,188972856,100,100,8,False,2024-05-01 05:00:00.000000,214182.95 +TIMEFRAME_M1,810594009,False,20,0,0,2199.497,2199.649,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,193458681,100,100,8,False,2024-05-01 05:00:00.000000,2199.497 +TIMEFRAME_M1,811091299,False,20,0,0,214182.95,214234.49,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,107164544,100,100,8,False,2024-05-01 05:00:00.000000,214182.95 +TIMEFRAME_M1,819266384,False,20,0,0,1533.0,1533.0,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,165292959,100,100,8,False,2024-05-01 05:00:00.000000,1532.55 +TIMEFRAME_M1,805378138,False,20,0,0,6264.311,6264.311,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,117590098,100,100,8,False,2024-05-01 05:00:00.000000,6264.137 +TIMEFRAME_M1,891437751,False,20,0,0,213981.67,214033.21,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,171146667,100,100,8,False,2024-05-01 05:05:00.000000,213981.67 +TIMEFRAME_M1,805128831,False,20,0,0,1529.95,1529.95,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,159473275,100,100,8,False,2024-05-01 05:05:00.000000,1529.5 +TIMEFRAME_M1,856848530,False,20,0,0,6264.655,6264.655,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,158379471,100,100,8,False,2024-05-01 05:05:00.000000,6264.481 +TIMEFRAME_M1,878388895,False,20,0,0,2199.303,2199.303,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,173585722,100,100,8,False,2024-05-01 05:05:00.000000,2199.151 +TIMEFRAME_M1,845514205,False,20,0,0,6264.623,6264.623,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,116042912,100,100,8,False,2024-05-01 05:10:00.000000,6264.449 +TIMEFRAME_M1,874207148,False,20,0,0,1531.12,1531.57,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,112416339,100,100,8,False,2024-05-01 05:10:00.000000,1531.12 +TIMEFRAME_M1,852585618,False,20,0,0,2198.827,2198.979,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,172436620,100,100,8,False,2024-05-01 05:10:00.000000,2198.827 +TIMEFRAME_M1,843064350,False,20,0,0,214131.56,214183.1,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,163931629,100,100,8,False,2024-05-01 05:10:00.000000,214131.56 +TIMEFRAME_M1,825289865,False,20,0,0,214131.56,214183.1,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,164053877,100,100,8,False,2024-05-01 05:10:00.000000,214131.56 +TIMEFRAME_M1,879479873,False,20,0,0,6264.449,6264.623,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,104747486,100,100,8,False,2024-05-01 05:10:00.000000,6264.449 +TIMEFRAME_M1,855148329,False,20,0,0,1531.12,1531.57,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,146052487,100,100,8,False,2024-05-01 05:10:00.000000,1531.12 +TIMEFRAME_M1,882831247,False,20,0,0,2198.979,2198.979,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,165313720,100,100,8,False,2024-05-01 05:10:00.000000,2198.827 +TIMEFRAME_M1,893248254,False,20,0,0,214924.49,214976.03,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,192343572,100,100,8,False,2024-05-01 05:15:00.000000,214924.49 +TIMEFRAME_M1,875553058,False,20,0,0,1532.28,1532.73,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,152857276,100,100,8,False,2024-05-01 05:15:00.000000,1532.28 +TIMEFRAME_M1,881843302,False,20,0,0,2196.88,2196.88,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,189666234,100,100,8,False,2024-05-01 05:15:00.000000,2196.728 +TIMEFRAME_M1,800863017,False,20,0,0,6265.776,6265.95,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,107381879,100,100,8,False,2024-05-01 05:15:00.000000,6265.776 +TIMEFRAME_M1,856387949,False,20,0,0,1526.52,1526.97,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,100871452,100,100,8,False,2024-05-01 05:20:00.000000,1526.52 +TIMEFRAME_M1,819754448,False,20,0,0,2200.7,2200.7,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,134458847,100,100,8,False,2024-05-01 05:20:00.000000,2200.548 +TIMEFRAME_M1,827578356,False,20,0,0,214711.04,214711.04,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,194137313,100,100,8,False,2024-05-01 05:20:00.000000,214659.5 +TIMEFRAME_M1,818203971,False,20,0,0,6264.764,6264.764,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,122899221,100,100,8,False,2024-05-01 05:20:00.000000,6264.59 +TIMEFRAME_M1,801907135,False,20,0,0,6264.764,6264.764,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,134150624,100,100,8,False,2024-05-01 05:20:00.000000,6264.59 +TIMEFRAME_M1,881932771,False,20,0,0,1526.52,1526.97,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,150759683,100,100,8,False,2024-05-01 05:20:00.000000,1526.52 +TIMEFRAME_M1,838431107,False,20,0,0,214659.5,214711.04,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,158762817,100,100,8,False,2024-05-01 05:25:00.000000,214659.5 +TIMEFRAME_M1,882024844,False,20,0,0,2201.197,2201.197,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,146126848,100,100,8,False,2024-05-01 05:25:00.000000,2201.045 +TIMEFRAME_M1,873736646,False,20,0,0,6265.69,6265.69,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,177412704,100,100,8,False,2024-05-01 05:25:00.000000,6265.516 +TIMEFRAME_M1,835840224,False,20,0,0,1536.0,1536.45,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,172476164,100,100,8,False,2024-05-01 05:25:00.000000,1536.0 +TIMEFRAME_M1,856047139,False,20,0,0,6265.225,6265.225,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,124096179,100,100,8,False,2024-05-01 05:30:00.000000,6265.051 +TIMEFRAME_M1,808155143,False,20,0,0,2197.526,2197.678,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,118815752,100,100,8,False,2024-05-01 05:30:00.000000,2197.526 +TIMEFRAME_M1,849113688,False,20,0,0,1537.62,1537.62,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,103900594,100,100,8,False,2024-05-01 05:30:00.000000,1537.17 +TIMEFRAME_M1,820415663,False,20,0,0,214220.04,214271.58,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,162773351,100,100,8,False,2024-05-01 05:30:00.000000,214220.04 +TIMEFRAME_M1,877115337,False,20,0,0,214271.58,214271.58,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,165272149,100,100,8,False,2024-05-01 05:30:00.000000,214220.04 +TIMEFRAME_M1,834090593,False,20,0,0,2197.678,2197.678,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,133690874,100,100,8,False,2024-05-01 05:30:00.000000,2197.526 +TIMEFRAME_M1,874427351,False,20,0,0,6265.051,6265.225,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,196946228,100,100,8,False,2024-05-01 05:30:00.000000,6265.051 +TIMEFRAME_M1,808936638,False,20,0,0,1537.62,1537.62,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,174917416,100,100,8,False,2024-05-01 05:30:00.000000,1537.17 +TIMEFRAME_M1,857087414,False,20,0,0,214635.36,214686.9,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,191119127,100,100,8,False,2024-05-01 05:35:00.000000,214635.36 +TIMEFRAME_M1,800402563,False,20,0,0,1537.14,1537.59,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,150045311,100,100,8,False,2024-05-01 05:35:00.000000,1537.14 +TIMEFRAME_M1,894996935,False,20,0,0,6264.765,6264.765,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,142705693,100,100,8,False,2024-05-01 05:35:00.000000,6264.591 +TIMEFRAME_M1,862937386,False,20,0,0,2199.022,2199.022,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,171688140,100,100,8,False,2024-05-01 05:35:00.000000,2198.87 +TIMEFRAME_M1,837022144,False,20,0,0,214382.59,214434.13,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,167851968,100,100,8,False,2024-05-01 05:40:00.000000,214382.59 +TIMEFRAME_M1,884898037,False,20,0,0,2199.349,2199.501,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,153171098,100,100,8,False,2024-05-01 05:40:00.000000,2199.349 +TIMEFRAME_M1,892705783,False,20,0,0,6266.637,6266.811,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,175456491,100,100,8,False,2024-05-01 05:40:00.000000,6266.637 +TIMEFRAME_M1,873932950,False,20,0,0,1532.75,1533.2,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,141983081,100,100,8,False,2024-05-01 05:40:00.000000,1532.75 +TIMEFRAME_M1,897708308,False,20,0,0,6266.637,6266.811,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,184672307,100,100,8,False,2024-05-01 05:40:00.000000,6266.637 +TIMEFRAME_M1,864942885,False,20,0,0,2199.501,2199.501,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,191884250,100,100,8,False,2024-05-01 05:40:00.000000,2199.349 +TIMEFRAME_M1,825082643,False,20,0,0,1532.75,1533.2,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,134591417,100,100,8,False,2024-05-01 05:40:00.000000,1532.75 +TIMEFRAME_M1,851021505,False,20,0,0,214434.13,214434.13,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,178756609,100,100,8,False,2024-05-01 05:40:00.000000,214382.59 +TIMEFRAME_M1,892558276,False,20,0,0,1536.77,1536.77,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,107853230,100,100,8,False,2024-05-01 05:45:00.000000,1536.32 +TIMEFRAME_M1,850365851,False,20,0,0,6263.382,6263.382,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,139971403,100,100,8,False,2024-05-01 05:45:00.000000,6263.208 +TIMEFRAME_M1,894265592,False,20,0,0,213826.17,213826.17,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,135472113,100,100,8,False,2024-05-01 05:45:00.000000,213774.63 +TIMEFRAME_M1,854207635,False,20,0,0,2200.353,2200.353,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,107970182,100,100,8,False,2024-05-01 05:45:00.000000,2200.201 +TIMEFRAME_M1,864094673,False,20,0,0,2199.054,2199.054,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,194251085,100,100,8,False,2024-05-01 05:50:00.000000,2198.902 +TIMEFRAME_M1,869461827,False,20,0,0,6262.14,6262.314,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,181791864,100,100,8,False,2024-05-01 05:50:00.000000,6262.14 +TIMEFRAME_M1,865092948,False,20,0,0,214145.92,214197.46,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,174246117,100,100,8,False,2024-05-01 05:50:00.000000,214145.92 +TIMEFRAME_M1,852624094,False,20,0,0,1545.66,1545.66,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,195039240,100,100,8,False,2024-05-01 05:50:00.000000,1545.21 +TIMEFRAME_M1,830534181,False,20,0,0,214145.92,214197.46,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,109942018,100,100,8,False,2024-05-01 05:50:00.000000,214145.92 +TIMEFRAME_M1,800936860,False,20,0,0,1545.66,1545.66,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,198409109,100,100,8,False,2024-05-01 05:50:00.000000,1545.21 +TIMEFRAME_M1,872706358,False,20,0,0,2199.054,2199.054,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,126160311,100,100,8,False,2024-05-01 05:55:00.000000,2198.902 +TIMEFRAME_M1,872332022,False,20,0,0,6262.314,6262.314,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,139901287,100,100,8,False,2024-05-01 05:55:00.000000,6262.14 +TIMEFRAME_M1,886192426,False,20,0,0,1542.73,1542.73,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,164448282,100,100,8,False,2024-05-01 05:55:00.000000,1542.28 +TIMEFRAME_M1,834923135,False,20,0,0,213632.82,213632.82,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,112636617,100,100,8,False,2024-05-01 05:55:00.000000,213581.28 +TIMEFRAME_M1,870604088,False,20,0,0,212968.89,213020.43,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,113955536,100,100,8,False,2024-05-01 06:00:00.000000,212968.89 +TIMEFRAME_M1,886127892,False,20,0,0,6260.134,6260.308,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,102748906,100,100,8,False,2024-05-01 06:00:00.000000,6260.134 +TIMEFRAME_M1,805836961,False,20,0,0,1551.9,1551.9,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,135848963,100,100,8,False,2024-05-01 06:00:00.000000,1551.45 +TIMEFRAME_M1,818917143,False,20,0,0,2197.052,2197.204,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,174717065,100,100,8,False,2024-05-01 06:00:00.000000,2197.052 +TIMEFRAME_M1,849689991,False,20,0,0,6260.134,6260.308,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,199749699,100,100,8,False,2024-05-01 06:00:00.000000,6260.134 +TIMEFRAME_M1,854437912,False,20,0,0,213020.43,213020.43,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,116564940,100,100,8,False,2024-05-01 06:00:00.000000,212968.89 +TIMEFRAME_M1,822945220,False,20,0,0,1551.9,1551.9,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,186521446,100,100,8,False,2024-05-01 06:00:00.000000,1551.45 +TIMEFRAME_M1,847494888,False,20,0,0,2197.052,2197.204,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,140423662,100,100,8,False,2024-05-01 06:00:00.000000,2197.052 +TIMEFRAME_M1,853578364,False,20,0,0,213020.43,213020.43,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,139757288,100,100,8,False,2024-05-01 06:00:00.000000,212968.89 +TIMEFRAME_M1,816839311,False,20,0,0,2197.052,2197.204,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,124839364,100,100,8,False,2024-05-01 06:00:00.000000,2197.052 +TIMEFRAME_M1,890821589,False,20,0,0,1551.9,1551.9,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,186595522,100,100,8,False,2024-05-01 06:00:00.000000,1551.45 +TIMEFRAME_M1,833249846,False,20,0,0,6260.308,6260.308,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,113890739,100,100,8,False,2024-05-01 06:00:00.000000,6260.134 +TIMEFRAME_M1,854896764,False,20,0,0,6260.134,6260.308,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,173794697,100,100,8,False,2024-05-01 06:00:00.000000,6260.134 +TIMEFRAME_M1,819313914,False,20,0,0,213020.43,213020.43,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,144234828,100,100,8,False,2024-05-01 06:00:00.000000,212968.89 +TIMEFRAME_M1,835509530,False,20,0,0,1551.45,1551.9,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,136859877,100,100,8,False,2024-05-01 06:00:00.000000,1551.45 +TIMEFRAME_M1,834587800,False,20,0,0,2197.204,2197.204,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,171159240,100,100,8,False,2024-05-01 06:00:00.000000,2197.052 +TIMEFRAME_M1,811903982,False,20,0,0,6260.308,6260.308,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,152978350,100,100,8,False,2024-05-01 06:00:00.000000,6260.134 +TIMEFRAME_M1,806564812,False,20,0,0,1551.9,1551.9,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,115641076,100,100,8,False,2024-05-01 06:00:00.000000,1551.45 +TIMEFRAME_M1,886177956,False,20,0,0,2197.204,2197.204,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,125649441,100,100,8,False,2024-05-01 06:00:00.000000,2197.052 +TIMEFRAME_M1,806666420,False,20,0,0,212968.89,213020.43,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,130254041,100,100,8,False,2024-05-01 06:00:00.000000,212968.89 +TIMEFRAME_M1,888101195,False,20,0,0,212865.2,212865.2,Chaos,Volatility 75 Index,TIMEFRAME_M2,0.001,154037803,100,100,8,False,2024-05-01 06:05:00.000000,212813.66 +TIMEFRAME_M1,812323607,False,20,0,0,6257.749,6257.749,Chaos,Volatility 10 Index,TIMEFRAME_M2,0.5,169502509,100,100,8,False,2024-05-01 06:05:00.000000,6257.575 +TIMEFRAME_M1,859538461,False,20,0,0,1550.69,1551.14,Chaos,Volatility 100 Index,TIMEFRAME_M2,0.5,154084616,100,100,8,False,2024-05-01 06:05:00.000000,1550.69 +TIMEFRAME_M1,821353761,False,20,0,0,2196.283,2196.435,Chaos,Volatility 25 Index,TIMEFRAME_M2,0.5,164088168,100,100,8,False,2024-05-01 06:05:00.000000,2196.283