diff --git a/.gitignore b/.gitignore
index b89c4ab..6410b1a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,3 +75,4 @@ target/
config.json
aiomql.json
config/
+test_data/
diff --git a/Untitled.ipynb b/Untitled.ipynb
deleted file mode 100644
index fbb3d4e..0000000
--- a/Untitled.ipynb
+++ /dev/null
@@ -1,730 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": 2,
- "id": "f4500c8d-0e58-4d3f-8dd3-06a4896f397f",
- "metadata": {},
- "outputs": [],
- "source": [
- "from datetime import datetime, timedelta\n",
- "from aiomql import MetaTrader, TimeFrame, AccountInfo, TimeFrame, CopyTicks, Account, Symbol\n",
- "from MetaTrader5 import SymbolInfo\n",
- "import pandas as pd\n",
- "from pandas import DataFrame\n",
- "import pytz"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "id": "f2ef126c-6edc-4651-8a81-06bb97eed6f9",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "True\n"
- ]
- }
- ],
- "source": [
- "res = await Account().sign_in()\n",
- "print(res)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 21,
- "id": "113d9327-bebc-4e99-8562-e6dbef29b108",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "0.58\n",
- "OrderCheckResult(retcode=0, balance=65.76, equity=63.35, profit=-2.41, margin=1.74, margin_free=61.61, margin_level=3640.8045977011498, comment='Done', request=TradeRequest(action=1, magic=0, order=0, symbol='Volatility 25 (1s) Index', volume=0.005, price=464167.9, stoplimit=0.0, sl=0.0, tp=0.0, deviation=0, type=0, type_filling=0, type_time=0, expiration=0, comment='', position=0, position_by=0))\n"
- ]
- },
- {
- "data": {
- "text/plain": [
- "OrderSendResult(retcode=10009, deal=3978355266, order=8068179971, volume=0.005, price=464076.06, bid=464042.01, ask=464076.06, comment='Request executed', request_id=943226517, retcode_external=0, request=TradeRequest(action=1, magic=0, order=0, symbol='Volatility 25 (1s) Index', volume=0.005, price=464167.9, stoplimit=0.0, sl=0.0, tp=0.0, deviation=0, type=0, type_filling=0, type_time=0, expiration=0, comment='', position=0, position_by=0))"
- ]
- },
- "execution_count": 21,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "sym = 'Volatility 25 (1s) Index'\n",
- "# sym = 'EURUSD'\n",
- "symb = Symbol(name=sym)\n",
- "await symb.init()\n",
- "op = symb.tick.ask + 100\n",
- "cl = round((symb.trade_stops_level + symb.spread) * symb.point + symb.ask, symb.digits)\n",
- "pr = await symb.mt5.order_calc_profit(action=0, symbol=sym, volume=symb.volume_min, price_open=op, price_close=cl)\n",
- "rp = symb.volume_min * symb.trade_contract_size * (cl - op)\n",
- "order = {'symbol': sym, 'price': op, 'volume': symb.volume_min, 'action': MetaTrader._TRADE_ACTION_A}\n",
- "res = await symb.mt5.order_calc_margin(MetaTrader._ORDER_TYPE_BUY, sym, symb.volume_min, op)\n",
- "print(res)\n",
- "ocr = await symb.mt5.order_check(order)\n",
- "print(ocr)\n",
- "await symb.mt5.order_send(order)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 22,
- "id": "f9189016-55fb-42d4-af77-e389767bc96c",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "(410, 7710.71, 0.01, 553.9999999999964)"
- ]
- },
- "execution_count": 22,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "symb.trade_stops_level, op, symb.point, abs(cl-op) / symb.point"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "id": "73109885-8291-42cd-90a6-8a04f5f25466",
- "metadata": {},
- "outputs": [],
- "source": [
- "acc = Account()\n",
- "await acc.refresh()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 16,
- "id": "dbb5e4f3-286d-4a1f-b59c-60a5af1dc94a",
- "metadata": {},
- "outputs": [
- {
- "ename": "AttributeError",
- "evalue": "readonly attribute",
- "output_type": "error",
- "traceback": [
- "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
- "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)",
- "Cell \u001b[1;32mIn[16], line 2\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[38;5;66;03m# acc = await acc.mt5.account_info()\u001b[39;00m\n\u001b[1;32m----> 2\u001b[0m \u001b[43macc\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmargin\u001b[49m \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m9\u001b[39m\n\u001b[0;32m 3\u001b[0m \u001b[38;5;66;03m# acc._replace(margin=0)\u001b[39;00m\n",
- "\u001b[1;31mAttributeError\u001b[0m: readonly attribute"
- ]
- }
- ],
- "source": [
- "# acc = await acc.mt5.account_info()\n",
- "acc.margin += 9\n",
- "# acc._replace(margin=0)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "2c598a85-1e90-49b0-abf1-87329597feae",
- "metadata": {},
- "outputs": [],
- "source": [
- "sym = Symbol(name='Volatility 25 Index')\n",
- "await sym.init()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "0a7d319c-5760-4058-994f-27e8d10df108",
- "metadata": {},
- "outputs": [],
- "source": [
- "await sym.mt5.order_calc_margin(0, 'Volatility 25 Index', 1, sym.tick.ask)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "4ec498ea-5e4e-4ff6-85da-051acc2eaf28",
- "metadata": {},
- "outputs": [],
- "source": [
- "sy = await sym.mt5.symbol_info('Volatility 25 Index')\n",
- "await sym.mt5.symbol_select('Volatility 25 Index', enable=True)\n",
- "sy.trade_calc_mode"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "9307e3df-a5f3-42cf-9d3d-e65e5254c0e5",
- "metadata": {},
- "outputs": [],
- "source": [
- "# margin\n",
- "# worked for forex.\n",
- "tcs = 1 * sym.trade_contract_size\n",
- "lv = Account().leverage /0.125\n",
- "tcs * sym.tick.ask / lv"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "e8413f70-244f-47a2-97b3-2be6fcd87589",
- "metadata": {},
- "outputs": [],
- "source": []
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "63050d62-443a-4d30-b2a6-4ec6d4f00362",
- "metadata": {},
- "outputs": [],
- "source": [
- "symbols = {'Volatility 10 Index', 'Volatility 100 (1s) Index', 'Volatility 25 Index'}\n",
- "timeframes = {TimeFrame.M5, TimeFrame.H1, TimeFrame.M1, TimeFrame.H4, TimeFrame.M30, TimeFrame.M15}\n",
- "gd = GetData(st, et, timeframes, symbols, name='data')\n",
- "await gd.fail()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "49d86767-d5cc-40a8-894e-c8f5b920f8cf",
- "metadata": {},
- "outputs": [],
- "source": [
- "symbols = {'Volatility 10 Index', 'Volatility 100 (1s) Index', 'Volatility 25 Index'}\n",
- "timeframes = {TimeFrame.M5, TimeFrame.H1, TimeFrame.M1, TimeFrame.H4, TimeFrame.M30, TimeFrame.M15}\n",
- "async with MetaTester(st, et, timeframes, symbols, name='data') as mt:\n",
- " # res = await mt.copy_ticks_range('Volatility 100 Index', st, et, CopyTicks.ALL)\n",
- " # print(res)\n",
- " await mt.get_and_save_data()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "c4f8c64b-dcba-40cf-8342-53bb4b3fa6e6",
- "metadata": {},
- "outputs": [],
- "source": [
- "df = pd.DataFrame(res)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "970126ba-a143-4b45-a82e-8cf6b885763c",
- "metadata": {},
- "outputs": [],
- "source": [
- "# df.set_index(list(range(secs)))\n",
- "df.drop_duplicates(subset=['time'], keep='last', )"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "bf6d4786-4ee1-4bd4-81e2-fa43bdc527a4",
- "metadata": {},
- "outputs": [],
- "source": [
- "df = df.set_index('time', drop=False, verify_integrity=True)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "e788c43f-42d3-4c01-9bd9-5224e2175c1f",
- "metadata": {},
- "outputs": [],
- "source": [
- "bg = int(st.timestamp())\n",
- "en = int(secs) + bg\n",
- "index = range(bg, en)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "fd9cffe6-c550-4f54-be8a-5e55b23b81a9",
- "metadata": {},
- "outputs": [],
- "source": [
- "bf = len(df.index)\n",
- "df = df.reindex(index=index, method='nearest')"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "c0b60fb9-0c27-4435-900d-898ea6979e11",
- "metadata": {},
- "outputs": [],
- "source": [
- "last = df.iloc[-1].time\n",
- "print(datetime.fromtimestamp(last, tz=tz), et)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "60afd149-9600-4f0e-b3ff-1e8ad10247f3",
- "metadata": {},
- "outputs": [],
- "source": [
- "acc = AccountInfo(balance=500)\n",
- "data = MetaTester(start, end).load_data('data')\n",
- "td = TestData(acc, data)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "be5704d5-4bff-4ec1-8cbe-2e96bfaf4f11",
- "metadata": {},
- "outputs": [],
- "source": [
- "ticks = data['ticks']['Volatility 10 Index']"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "12b3d5cb-0d31-4b70-b583-1f2becb74a0f",
- "metadata": {},
- "outputs": [],
- "source": [
- "ticks[-1]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "ee0c4ccb-0e7c-434e-a655-fe89b09e606c",
- "metadata": {},
- "outputs": [],
- "source": [
- "y = TimeFrame.M5\n",
- "y.time"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "7a28ce72-0409-4231-92f7-f4e61fc0be94",
- "metadata": {},
- "outputs": [],
- "source": [
- "end.timestamp()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "4aeeb64b-d3d2-4053-9808-2539e2755d09",
- "metadata": {},
- "outputs": [],
- "source": [
- "len(ticks)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "29647ef4-01ad-46a0-afb2-f43a4988a529",
- "metadata": {},
- "outputs": [],
- "source": [
- "type(ticks[0])"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "bc593a4b-53d6-4203-a986-33b5267b9244",
- "metadata": {},
- "outputs": [],
- "source": [
- "ticks.reshape(8, 86160)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "dc6440a3-4333-4363-b462-edee4facdd0f",
- "metadata": {},
- "outputs": [],
- "source": [
- "import numpy as np"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "0a246c6d-d35c-4878-a128-52ed1abb0108",
- "metadata": {},
- "outputs": [],
- "source": [
- "res = np.reshape(ticks, (-1, 8))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "6e9de365-0f13-4d88-8302-f5e3418d489b",
- "metadata": {},
- "outputs": [],
- "source": [
- "ticks.shape = (86160, 8)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "451b8e09-d17c-4662-a25d-39f00ff01788",
- "metadata": {},
- "outputs": [],
- "source": [
- "res = np.hstack(ticks)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "395a1b7b-309d-4e38-b579-1b6b4854b19d",
- "metadata": {},
- "outputs": [],
- "source": [
- "v = np.array((*ticks[0]))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "1bb16ff4-d92e-4474-a235-d8be87d094d6",
- "metadata": {},
- "outputs": [],
- "source": [
- "r = next(iter(ticks))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "f2241c9c-68eb-4371-b3e3-202ebe25f7cd",
- "metadata": {},
- "outputs": [],
- "source": [
- "v.shape"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "ee0d3713-8578-46e2-969c-aebc295fae98",
- "metadata": {},
- "outputs": [],
- "source": [
- "ar = list(range(4))\n",
- "t = np.array(ar)\n",
- "t.shape = (1, 4)\n",
- "t"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "8502f7da-793e-4bfe-b469-bc109f051507",
- "metadata": {},
- "outputs": [],
- "source": [
- "mt.config.login"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "4d818ddd-f78c-4a50-b4a7-dc266321091c",
- "metadata": {},
- "outputs": [],
- "source": [
- "acc = Account()\n",
- "await acc.sign_in()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "4c4c32d9-9c45-4ee7-afee-e04d1f5dca4a",
- "metadata": {},
- "outputs": [],
- "source": [
- "mt5 = MetaTrader()\n",
- "sym = await mt5.symbol_info('Volatility 100 (1s) Index')\n",
- "print(sym._asdict)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 70,
- "id": "e47bd638-42fa-40d9-b573-474a39884dbc",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- " time open high low close tick_volume spread \\\n",
- "0 1724859360 6770.27 6775.26 6765.13 6765.13 60 144 \n",
- "1 1724859420 6766.28 6769.70 6761.25 6761.35 60 144 \n",
- "2 1724859480 6762.58 6777.91 6762.58 6776.98 58 144 \n",
- "3 1724859540 6778.78 6784.64 6771.89 6782.54 60 144 \n",
- "4 1724859600 6781.56 6787.20 6779.02 6784.20 60 144 \n",
- ".. ... ... ... ... ... ... ... \n",
- "495 1724889060 6622.30 6626.43 6620.74 6620.76 60 144 \n",
- "496 1724889120 6620.70 6623.06 6611.94 6623.06 60 144 \n",
- "497 1724889180 6623.13 6625.32 6618.51 6623.52 60 144 \n",
- "498 1724889240 6622.50 6627.44 6621.98 6626.63 60 144 \n",
- "499 1724889300 6626.77 6626.77 6626.48 6626.48 2 144 \n",
- "\n",
- " real_volume \n",
- "0 0 \n",
- "1 0 \n",
- "2 0 \n",
- "3 0 \n",
- "4 0 \n",
- ".. ... \n",
- "495 0 \n",
- "496 0 \n",
- "497 0 \n",
- "498 0 \n",
- "499 0 \n",
- "\n",
- "[500 rows x 8 columns]"
- ]
- },
- "execution_count": 70,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "await symb.copy_rates_from_pos(timeframe=TimeFrame.M1)\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 72,
- "id": "a4bc7b28-6dac-4863-b257-64d2c5c62575",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "Empty DataFrame\n",
- "Columns: [time, bid, ask, last, volume, time_msc, flags, volume_real]\n",
- "Index: []"
- ]
- },
- "execution_count": 72,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "tz = pytz.timezone('Etc/UTC')\n",
- "now = datetime.now(tz=tz)\n",
- "# now.replace(tz=tz-tz)\n",
- "await symb.copy_ticks_from(date_from=now)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "90c17d01-4ed0-468d-85c4-64988338b8a9",
- "metadata": {},
- "outputs": [],
- "source": [
- "print(start)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "9a9141a1-7049-48e5-a96f-0b637b817d40",
- "metadata": {},
- "outputs": [],
- "source": [
- "data = shelve.open('./data/01-08-24_17-08-24', writeback=True)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "e2d4d4d7-5bfa-43e3-a455-012cf754a106",
- "metadata": {},
- "outputs": [],
- "source": [
- "data = dict(data)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "95931276-fb8c-4a97-b440-330efc5e4d93",
- "metadata": {},
- "outputs": [],
- "source": [
- "data = pickle.dumps(data)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "57c620ad-3fa5-41b9-a372-9a090e18ff85",
- "metadata": {},
- "outputs": [],
- "source": [
- "data = zlib.compress(data, level=9)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "2d85bcdb-a86a-43f0-9b93-7d8c0ce7cf9e",
- "metadata": {},
- "outputs": [],
- "source": [
- "_data = lzma.compress(data)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "eec34d71-7455-4064-9367-52156407335e",
- "metadata": {},
- "outputs": [],
- "source": [
- "fh = lzma.open('./data/ldata.xz', 'w')\n",
- "fh.write(_data)\n",
- "fh.close()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "6f38b857-ed2d-4819-ba7f-4f70dc2fb3f9",
- "metadata": {},
- "outputs": [],
- "source": [
- "rb = lzma.open('./data/ldata.xz')\n",
- "rbb = rb.read()\n",
- "rb.close()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "28220ba7-46af-409b-826d-2979ceb19f65",
- "metadata": {},
- "outputs": [],
- "source": [
- "rbd = lzma.decompress(rbb)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "b3275fd6-70ff-4596-b4ba-9d9e4b6e52fe",
- "metadata": {},
- "outputs": [],
- "source": [
- "rdata = pickle.loads(rbd)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "5bbcbc06-f353-4255-9792-89263793ba0f",
- "metadata": {},
- "outputs": [],
- "source": [
- "data.keys()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "f9830b77-5e73-45c9-bc6a-40c19c3816c5",
- "metadata": {},
- "outputs": [],
- "source": [
- "fh = open('./data/pdata', 'wb')\n",
- "pickle.dump(rdata, fh)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "064b8628-1331-4fa3-abf3-66f88c75da33",
- "metadata": {},
- "outputs": [],
- "source": [
- "fh.close()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "fe60fc41-8844-4e00-b254-bb95d28528f2",
- "metadata": {},
- "outputs": [],
- "source": [
- "6 / (0 or 5)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "ceb79fa6-4713-43a5-9850-884cf6262f87",
- "metadata": {},
- "outputs": [],
- "source": []
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3 (ipykernel)",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.11.6"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 5
-}
diff --git a/Untitled1.ipynb b/Untitled1.ipynb
deleted file mode 100644
index c3a0395..0000000
--- a/Untitled1.ipynb
+++ /dev/null
@@ -1,443 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": 1,
- "id": "f1039720-9692-4605-adf9-d37651958e4c",
- "metadata": {},
- "outputs": [],
- "source": [
- "import pandas as pd\n",
- "import numpy as np\n",
- "from pandas import DataFrame, Series"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "id": "8d39819f-2cac-437f-b5fc-633ca7443f8a",
- "metadata": {},
- "outputs": [],
- "source": [
- "rs = DataFrame({0: range(10, 101, 10), 1: range(10, 20), 2: range(20, 40, 2), \"symbols\": [chr(i) for i in [65]*5 + [68]*5]})"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "id": "d7976bb8-05cb-4924-a6e2-90ea8af85d9d",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- " \n",
- " | \n",
- " 0 | \n",
- " 1 | \n",
- " 2 | \n",
- " symbols | \n",
- "
\n",
- " \n",
- " \n",
- " \n",
- " | 0 | \n",
- " 10 | \n",
- " 10 | \n",
- " 20 | \n",
- " A | \n",
- "
\n",
- " \n",
- " | 1 | \n",
- " 20 | \n",
- " 11 | \n",
- " 22 | \n",
- " A | \n",
- "
\n",
- " \n",
- " | 2 | \n",
- " 30 | \n",
- " 12 | \n",
- " 24 | \n",
- " A | \n",
- "
\n",
- " \n",
- " | 3 | \n",
- " 40 | \n",
- " 13 | \n",
- " 26 | \n",
- " A | \n",
- "
\n",
- " \n",
- " | 4 | \n",
- " 50 | \n",
- " 14 | \n",
- " 28 | \n",
- " A | \n",
- "
\n",
- " \n",
- "
\n",
- "
"
- ],
- "text/plain": [
- " 0 1 2 symbols\n",
- "0 10 10 20 A\n",
- "1 20 11 22 A\n",
- "2 30 12 24 A\n",
- "3 40 13 26 A\n",
- "4 50 14 28 A"
- ]
- },
- "execution_count": 6,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "rs[rs.symbols == 'A']"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 71,
- "id": "eb69c292-79e3-4105-b10c-6f4f5c22074f",
- "metadata": {},
- "outputs": [
- {
- "ename": "KeyError",
- "evalue": "None",
- "output_type": "error",
- "traceback": [
- "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
- "\u001b[1;31mKeyError\u001b[0m Traceback (most recent call last)",
- "Cell \u001b[1;32mIn[71], line 2\u001b[0m\n\u001b[0;32m 1\u001b[0m ind \u001b[38;5;241m=\u001b[39m rs[rs[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124msymbols\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m!=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mA\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39miloc[\u001b[38;5;241m0\u001b[39m]\u001b[38;5;241m.\u001b[39mindex\u001b[38;5;241m.\u001b[39mname\n\u001b[1;32m----> 2\u001b[0m \u001b[43mrs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mloc\u001b[49m\u001b[43m[\u001b[49m\u001b[43mind\u001b[49m\u001b[43m]\u001b[49m\n",
- "File \u001b[1;32m~\\OneDrive - UBA\\Documents\\Personal\\VS\\aiomql\\venv\\Lib\\site-packages\\pandas\\core\\indexing.py:1191\u001b[0m, in \u001b[0;36m_LocationIndexer.__getitem__\u001b[1;34m(self, key)\u001b[0m\n\u001b[0;32m 1189\u001b[0m maybe_callable \u001b[38;5;241m=\u001b[39m com\u001b[38;5;241m.\u001b[39mapply_if_callable(key, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mobj)\n\u001b[0;32m 1190\u001b[0m maybe_callable \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_check_deprecated_callable_usage(key, maybe_callable)\n\u001b[1;32m-> 1191\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_getitem_axis\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmaybe_callable\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43maxis\u001b[49m\u001b[43m)\u001b[49m\n",
- "File \u001b[1;32m~\\OneDrive - UBA\\Documents\\Personal\\VS\\aiomql\\venv\\Lib\\site-packages\\pandas\\core\\indexing.py:1431\u001b[0m, in \u001b[0;36m_LocIndexer._getitem_axis\u001b[1;34m(self, key, axis)\u001b[0m\n\u001b[0;32m 1429\u001b[0m \u001b[38;5;66;03m# fall thru to straight lookup\u001b[39;00m\n\u001b[0;32m 1430\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_validate_key(key, axis)\n\u001b[1;32m-> 1431\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_get_label\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43maxis\u001b[49m\u001b[43m)\u001b[49m\n",
- "File \u001b[1;32m~\\OneDrive - UBA\\Documents\\Personal\\VS\\aiomql\\venv\\Lib\\site-packages\\pandas\\core\\indexing.py:1381\u001b[0m, in \u001b[0;36m_LocIndexer._get_label\u001b[1;34m(self, label, axis)\u001b[0m\n\u001b[0;32m 1379\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_get_label\u001b[39m(\u001b[38;5;28mself\u001b[39m, label, axis: AxisInt):\n\u001b[0;32m 1380\u001b[0m \u001b[38;5;66;03m# GH#5567 this will fail if the label is not present in the axis.\u001b[39;00m\n\u001b[1;32m-> 1381\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mxs\u001b[49m\u001b[43m(\u001b[49m\u001b[43mlabel\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43maxis\u001b[49m\u001b[43m)\u001b[49m\n",
- "File \u001b[1;32m~\\OneDrive - UBA\\Documents\\Personal\\VS\\aiomql\\venv\\Lib\\site-packages\\pandas\\core\\generic.py:4301\u001b[0m, in \u001b[0;36mNDFrame.xs\u001b[1;34m(self, key, axis, level, drop_level)\u001b[0m\n\u001b[0;32m 4299\u001b[0m new_index \u001b[38;5;241m=\u001b[39m index[loc]\n\u001b[0;32m 4300\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m-> 4301\u001b[0m loc \u001b[38;5;241m=\u001b[39m \u001b[43mindex\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_loc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 4303\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(loc, np\u001b[38;5;241m.\u001b[39mndarray):\n\u001b[0;32m 4304\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m loc\u001b[38;5;241m.\u001b[39mdtype \u001b[38;5;241m==\u001b[39m np\u001b[38;5;241m.\u001b[39mbool_:\n",
- "File \u001b[1;32m~\\OneDrive - UBA\\Documents\\Personal\\VS\\aiomql\\venv\\Lib\\site-packages\\pandas\\core\\indexes\\range.py:417\u001b[0m, in \u001b[0;36mRangeIndex.get_loc\u001b[1;34m(self, key)\u001b[0m\n\u001b[0;32m 415\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(key) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01merr\u001b[39;00m\n\u001b[0;32m 416\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(key, Hashable):\n\u001b[1;32m--> 417\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(key)\n\u001b[0;32m 418\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_check_indexing_error(key)\n\u001b[0;32m 419\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(key)\n",
- "\u001b[1;31mKeyError\u001b[0m: None"
- ]
- }
- ],
- "source": [
- "ind = rs[rs['symbols'] != 'A'].iloc[0].index.name\n",
- "rs.loc[ind]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 44,
- "id": "cce9fa9e-d841-4f45-8813-313f17e7b12f",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "\n",
- "\n",
- "
\n",
- " \n",
- " \n",
- " | \n",
- " 0 | \n",
- " 1 | \n",
- " 2 | \n",
- " symbols | \n",
- "
\n",
- " \n",
- " \n",
- " \n",
- " | 0 | \n",
- " 10 | \n",
- " 10 | \n",
- " 20 | \n",
- " A | \n",
- "
\n",
- " \n",
- " | 1 | \n",
- " 20 | \n",
- " 11 | \n",
- " 22 | \n",
- " A | \n",
- "
\n",
- " \n",
- " | 2 | \n",
- " 30 | \n",
- " 12 | \n",
- " 24 | \n",
- " A | \n",
- "
\n",
- " \n",
- " | 3 | \n",
- " 40 | \n",
- " 13 | \n",
- " 26 | \n",
- " A | \n",
- "
\n",
- " \n",
- " | 4 | \n",
- " 50 | \n",
- " 14 | \n",
- " 28 | \n",
- " A | \n",
- "
\n",
- " \n",
- " | 5 | \n",
- " 60 | \n",
- " 15 | \n",
- " 30 | \n",
- " D | \n",
- "
\n",
- " \n",
- " | 6 | \n",
- " 70 | \n",
- " 16 | \n",
- " 32 | \n",
- " D | \n",
- "
\n",
- " \n",
- " | 7 | \n",
- " 80 | \n",
- " 17 | \n",
- " 34 | \n",
- " D | \n",
- "
\n",
- " \n",
- " | 8 | \n",
- " 90 | \n",
- " 18 | \n",
- " 36 | \n",
- " D | \n",
- "
\n",
- " \n",
- " | 9 | \n",
- " 100 | \n",
- " 19 | \n",
- " 38 | \n",
- " D | \n",
- "
\n",
- " \n",
- "
\n",
- "
"
- ],
- "text/plain": [
- " 0 1 2 symbols\n",
- "0 10 10 20 A\n",
- "1 20 11 22 A\n",
- "2 30 12 24 A\n",
- "3 40 13 26 A\n",
- "4 50 14 28 A\n",
- "5 60 15 30 D\n",
- "6 70 16 32 D\n",
- "7 80 17 34 D\n",
- "8 90 18 36 D\n",
- "9 100 19 38 D"
- ]
- },
- "execution_count": 44,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "rs"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 31,
- "id": "3c456100-4931-4fbd-a63d-531eaf735e70",
- "metadata": {},
- "outputs": [
- {
- "ename": "InvalidIndexError",
- "evalue": "RangeIndex(start=0, stop=10, step=1)",
- "output_type": "error",
- "traceback": [
- "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
- "\u001b[1;31mInvalidIndexError\u001b[0m Traceback (most recent call last)",
- "Cell \u001b[1;32mIn[31], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[43mrs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mindex\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_loc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrs\u001b[49m\u001b[43m[\u001b[49m\u001b[43mrs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mindex\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m<\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m31\u001b[39;49m\u001b[43m]\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mindex\u001b[49m\u001b[43m)\u001b[49m\n",
- "File \u001b[1;32m~\\OneDrive - UBA\\Documents\\Personal\\VS\\aiomql\\venv\\Lib\\site-packages\\pandas\\core\\indexes\\range.py:418\u001b[0m, in \u001b[0;36mRangeIndex.get_loc\u001b[1;34m(self, key)\u001b[0m\n\u001b[0;32m 416\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(key, Hashable):\n\u001b[0;32m 417\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(key)\n\u001b[1;32m--> 418\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_check_indexing_error\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 419\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(key)\n",
- "File \u001b[1;32m~\\OneDrive - UBA\\Documents\\Personal\\VS\\aiomql\\venv\\Lib\\site-packages\\pandas\\core\\indexes\\base.py:6059\u001b[0m, in \u001b[0;36mIndex._check_indexing_error\u001b[1;34m(self, key)\u001b[0m\n\u001b[0;32m 6055\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_check_indexing_error\u001b[39m(\u001b[38;5;28mself\u001b[39m, key):\n\u001b[0;32m 6056\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_scalar(key):\n\u001b[0;32m 6057\u001b[0m \u001b[38;5;66;03m# if key is not a scalar, directly raise an error (the code below\u001b[39;00m\n\u001b[0;32m 6058\u001b[0m \u001b[38;5;66;03m# would convert to numpy arrays and raise later any way) - GH29926\u001b[39;00m\n\u001b[1;32m-> 6059\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m InvalidIndexError(key)\n",
- "\u001b[1;31mInvalidIndexError\u001b[0m: RangeIndex(start=0, stop=10, step=1)"
- ]
- }
- ],
- "source": [
- "rs.index.get_loc(rs[rs.index <= 31].index)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 73,
- "id": "1a5a99bb-3fc4-458d-95b8-4e031f3e04c1",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "Index([20, 22, 24, 26, 28, 30, 32, 34, 36, 38], dtype='int64', name=2)"
- ]
- },
- "execution_count": 73,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "rs.index"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 28,
- "id": "2d53ccef-72e1-4378-818f-b60682bbd8b7",
- "metadata": {},
- "outputs": [],
- "source": [
- "g = rs[rs.index <= 31].iloc[-1]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 87,
- "id": "7318351e-e185-4695-ada5-43596b4844e6",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "np.int64(20)"
- ]
- },
- "execution_count": 87,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "rs.index[0]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 114,
- "id": "55441116-8c00-4fd1-94c8-939320c6dfc4",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "[25, 26, 27, 28, 29]"
- ]
- },
- "execution_count": 114,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "t = list(range(30))\n",
- "t[25:30]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 121,
- "id": "6ef28e4c-15a5-4fe5-aa66-10ba464f1256",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "[6, 7, 8, 9]"
- ]
- },
- "execution_count": 121,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "t[6: 10]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 126,
- "id": "f15cf18c-54cb-4624-8d18-3998bde2b9ba",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "None\n"
- ]
- }
- ],
- "source": [
- "p = 0 or None\n",
- "print(p)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "47dd4064-dde6-4824-a12b-ecf334bf4ebe",
- "metadata": {},
- "outputs": [],
- "source": []
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3 (ipykernel)",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.11.6"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 5
-}
diff --git a/bot_testet.py b/bot_testet.py
new file mode 100644
index 0000000..ac6b0a3
--- /dev/null
+++ b/bot_testet.py
@@ -0,0 +1,29 @@
+import asyncio
+
+from aiomql import MetaTester, StrategyTester, FingerTrapTest, ForexSymbol, Account, TestData, GetData, Config, \
+ TestStrategy, EventManager
+
+
+async def st_testr():
+ config = Config()
+ da = GetData.load_data(name=f"{config.test_data_dir}/01-08-24_31-08-24")
+ # print(da)
+ td = TestData(da)
+ st1 = FingerTrapTest(symbol=ForexSymbol(name='Volatility 100 (1s) Index'))
+ st2 = FingerTrapTest(symbol=ForexSymbol(name='Volatility 25 Index'))
+ st = StrategyTester(strategies=[st1, st2], test_data=td)
+ await st.run()
+
+
+async def st_one():
+ sym = ForexSymbol(name='Volatility 100 (1s) Index')
+ st1 = FingerTrapTest(symbol=sym)
+ config = Config()
+ da = GetData.load_data(name=f"{config.test_data_dir}/01-08-24_31-08-24")
+ config.test_data = TestData(da)
+ st1.set_up()
+ await sym.init()
+ await st1.test_single()
+
+
+asyncio.run(st_testr())
diff --git a/src/aiomql/__init__.py b/src/aiomql/__init__.py
index fb6a018..7677e20 100644
--- a/src/aiomql/__init__.py
+++ b/src/aiomql/__init__.py
@@ -16,5 +16,6 @@ from .history import History
from .trader import Trader
from .terminal import Terminal
from .sessions import Session, Sessions
-from .utils import dict_to_string, round_off, find_bearish_fractal, find_bullish_fractal
+from .utils import dict_to_string, round_off, backoff_decorator, error_handler, error_handler_sync, round_up, round_down
from .lib import *
+from .contrib import *
diff --git a/src/aiomql/account.py b/src/aiomql/account.py
index 47a294f..47ac91a 100644
--- a/src/aiomql/account.py
+++ b/src/aiomql/account.py
@@ -58,14 +58,14 @@ class Account(AccountInfo):
await self.mt5.shutdown()
self.connected = False
- async def sign_in(self) -> bool:
+ async def sign_in(self, **kwargs) -> bool:
"""Connect to a trading account.
Returns:
bool: True if login was successful else False
"""
acc = self.get_dict(include={'login', 'server', 'password'})
- self.connected = await self._login(acc=acc)
+ self.connected = await self._login(acc=acc, **kwargs)
if self.connected:
await self.refresh()
self.symbols = await self.symbols_get()
@@ -73,18 +73,19 @@ class Account(AccountInfo):
await self.mt5.shutdown()
return False
- async def _login(self, *, acc: dict, tries=3):
+ async def _login(self, *, acc: dict, tries=3, **kwargs) -> bool:
res = False
if tries == 0:
return False
- ini = await self.mt5.initialize(**acc, path=self.config.path)
+ init_args = {**acc} | {'path': self.config.path} | {**kwargs}
+ ini = await self.mt5.initialize(**init_args)
if ini:
res = await self.mt5.login(**acc)
if ini and res:
return True
else:
await asyncio.sleep(5+tries)
- return await self._login(acc=acc, tries=tries-1)
+ return await self._login(acc=acc, tries=tries-1, **kwargs)
def has_symbol(self, symbol: str | SymbolInfo):
"""Checks to see if a symbol is available for a trading account.
diff --git a/src/aiomql/contrib/__init__.py b/src/aiomql/contrib/__init__.py
new file mode 100644
index 0000000..e29223e
--- /dev/null
+++ b/src/aiomql/contrib/__init__.py
@@ -0,0 +1 @@
+from .backtester import *
diff --git a/src/aiomql/contrib/backtester/__init__.py b/src/aiomql/contrib/backtester/__init__.py
new file mode 100644
index 0000000..fcbba4d
--- /dev/null
+++ b/src/aiomql/contrib/backtester/__init__.py
@@ -0,0 +1,7 @@
+from .meta_tester import MetaTester
+from .test_data import TestData
+from .get_data import GetData
+from .test_strategy import TestStrategy
+from .event_manager import EventManager
+from .strategy_tester import StrategyTester
+# from .test_executor import FingerTrapTest
diff --git a/src/aiomql/contrib/backtester/check.py b/src/aiomql/contrib/backtester/check.py
new file mode 100644
index 0000000..b7ccc5b
--- /dev/null
+++ b/src/aiomql/contrib/backtester/check.py
@@ -0,0 +1,95 @@
+import signal
+import asyncio
+from asyncio import Condition, Task
+import random
+
+
+class EventManager:
+
+ def __init__(self, num_tasks: int, lock = None):
+ self.event = Condition(lock=lock)
+ self.num_tasks = num_tasks
+ self.counter = 0
+ self.state = 0
+ self.tasks: list[Task] = []
+
+ async def sleep(self, secs):
+ while secs > self.state:
+ await self.wait()
+
+ async def acquire(self):
+ await self.event.acquire()
+
+ def notify_all(self):
+ self.event.notify_all()
+
+ def sigint_handler(self, sig, frame):
+ for task in self.tasks:
+ print(task.get_name())
+ task.cancel() if not task.done() else ...
+
+ async def event_monitor(self):
+ while True:
+ async with self.event:
+ if self.counter == self.num_tasks:
+ self.counter = 0
+ self.state += 1
+ self.event.notify_all()
+ await asyncio.sleep(0)
+
+ async def wait(self):
+ self.counter += 1
+ await self.event.wait()
+
+ def release(self):
+ self.event.release()
+
+async def long_task1(event: EventManager):
+ counter = 0
+ while True:
+ await event.acquire()
+ try:
+ await event.wait()
+ sleep = random.randint(1, 2)
+ await asyncio.sleep(sleep)
+ counter += 1
+ print(f'task 1: {event.state}-{counter}')
+ finally:
+ event.release()
+
+async def long_task2(event: EventManager):
+ counter = 0
+ while True:
+ await event.acquire()
+ try:
+ await event.wait()
+ sleep = random.randint(1, 2)
+ await asyncio.sleep(sleep)
+ counter += 1
+ print(f'task 2: {event.state}-{counter}')
+ finally:
+ event.release()
+
+async def long_task3(event: EventManager):
+ sleep = 10
+ while True:
+ await event.acquire()
+ try:
+ await event.wait()
+ await event.sleep(sleep)
+ print(f'task 3: {event.state}-{sleep}')
+ sleep += 10
+ finally:
+ event.release()
+
+async def main():
+ manager = EventManager(num_tasks=3)
+ signal.signal(signal.SIGINT, manager.sigint_handler)
+ task1 = asyncio.create_task(long_task1(manager), name='task1')
+ task2 = asyncio.create_task(long_task2(manager), name='task2')
+ task3 = asyncio.create_task(long_task3(manager), name='task3')
+ control = asyncio.create_task(manager.event_monitor(), name='monitor')
+ manager.tasks.extend([task1, task2, task3, control])
+ res = await asyncio.gather(task1, task2, task3, control)
+
+asyncio.run(main())
diff --git a/src/aiomql/contrib/backtester/check1.py b/src/aiomql/contrib/backtester/check1.py
new file mode 100644
index 0000000..db0dd91
--- /dev/null
+++ b/src/aiomql/contrib/backtester/check1.py
@@ -0,0 +1,43 @@
+from functools import wraps
+from dataclasses import dataclass, fields, field
+from typing import ClassVar
+
+def dd(func):
+
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ print(func.__name__)
+ return func(*args, **kwargs)
+ return wrapper
+
+class C:
+ def __new__(cls, *args, **kwargs):
+ if not hasattr(cls, '_instance'):
+ cls._instance = super().__new__(cls)
+ cls._instance.tasks = []
+ # cls.__init__(a)
+ [setattr(cls._instance, k, v) for k, v in kwargs.items()]
+ return cls._instance
+
+ def __init__(self, *args, **kwargs):
+ print('receiving args')
+
+
+
+
+@dataclass
+class D:
+ b: int = 0
+ c: str = ''
+ _fields: list[ClassVar[str]] = field(default_factory=list)
+
+ @dd
+ def setattrs(self, **kwargs):
+ [setattr(self, k, v) for k, v in kwargs.items() if k in self.fields]
+
+ @property
+ def fields(self):
+ return self._fields or [name for f in fields(self) if (name := f.name) != '_fields']
+
+d = D()
+d.setattrs(r=3)
diff --git a/src/aiomql/contrib/backtester/event_manager.py b/src/aiomql/contrib/backtester/event_manager.py
new file mode 100644
index 0000000..d380e3a
--- /dev/null
+++ b/src/aiomql/contrib/backtester/event_manager.py
@@ -0,0 +1,57 @@
+import asyncio
+from asyncio import Condition, Task
+from typing import Self
+
+from ...core 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
+
+ def add_task(self, *task: Task):
+ self.tasks.extend(task)
+
+ def sigint_handler(self, sig, frame):
+ for task in self.tasks:
+ task.cancel() if not task.done() else ...
+
+ async def acquire(self):
+ await self.condition.acquire()
+
+ def notify_all(self):
+ self.condition.notify_all()
+
+ async def event_monitor(self):
+ while True:
+ async with self.condition:
+ if self.task_tracker == self.num_main_tasks:
+ self.task_tracker = 0
+ await self.config.test_data.tracker()
+ self.config.test_data.next()
+ print(self.config.test_data.cursor.time)
+ self.condition.notify_all()
+ await asyncio.sleep(0)
+
+ async def wait(self):
+ self.task_tracker += 1
+ await self.condition.wait()
+
+ def release(self):
+ self.condition.release()
diff --git a/src/aiomql/core/backtester/get_data.py b/src/aiomql/contrib/backtester/get_data.py
similarity index 55%
rename from src/aiomql/core/backtester/get_data.py
rename to src/aiomql/contrib/backtester/get_data.py
index 34a9372..fedaefe 100644
--- a/src/aiomql/core/backtester/get_data.py
+++ b/src/aiomql/contrib/backtester/get_data.py
@@ -1,9 +1,11 @@
-from dataclasses import dataclass
+from dataclasses import dataclass, field, fields
import pickle
+from pathlib import Path
import lzma
from datetime import datetime
from logging import getLogger
import asyncio
+from typing import Sequence, ClassVar
import pytz
import pandas as pd
@@ -18,83 +20,127 @@ from ...utils import backoff_decorator
logger = getLogger(__name__)
from MetaTrader5 import TradePosition, TradeOrder, TradeDeal
-tof = list(TradeOrder._fields)
+tof = list(TradeOrder.__match_args__)
tof.append('symbol')
-tpf = list(TradePosition._fields)
+tpf = list(TradePosition.__match_args__)
tpf.append('symbol')
-tdf = list(TradeDeal._fields)
+tdf = list(TradeDeal.__match_args__)
tdf.append('symbol')
@dataclass
class Data:
- account: dict
- symbols: dict[str, dict]
- prices: dict[str, DataFrame]
- ticks: dict[str, DataFrame]
- rates: dict[str, dict[str, DataFrame]]
- span: range
- range: range
- history_orders: DataFrame = DataFrame([], columns=tof)
- history_deals: DataFrame = DataFrame([], columns=tdf)
- positions: DataFrame = DataFrame([], columns=tpf)
+ name: str = ''
+ terminal: dict[str, [str | int | bool | float]] = field(default_factory=dict)
+ version: tuple[int, int, str] = (0, 0, '')
+ account: dict = field(default_factory=dict)
+ symbols: dict[str, dict] = field(default_factory=dict)
+ prices: dict[str, DataFrame] = field(default_factory=dict)
+ ticks: dict[str, DataFrame] = field(default_factory=dict)
+ rates: dict[str, dict[str, DataFrame]] = field(default_factory=dict)
+ span: range = range(0)
+ range: range = range(0)
+ history_orders: DataFrame = field(default_factory=lambda: DataFrame([], columns=tof))
+ history_deals: DataFrame = field(default_factory=lambda: DataFrame([], columns=tdf))
+ positions: dict[str, DataFrame] = field(default_factory=dict)
+ orders: dict[str, DataFrame] = field(default_factory=dict)
+
+ _fields: list[ClassVar[str]] = field(default_factory=list)
+
+ def setattrs(self, **kwargs):
+ [setattr(self, k, v) for k, v in kwargs.items() if k in self.fields]
+
+ @property
+ def fields(self):
+ return self._fields or [name for f in fields(self) if (name := f.name) != '_fields']
class GetData:
+ data: Data | None
- def __init__(self, *, start: datetime, end: datetime, timeframes: set[TimeFrame], symbols: set[str],
- name: str = '', tz: str = 'Etc/UTC'):
+ def __init__(self, *, start: datetime, end: datetime, symbols: Sequence[str],
+ timeframes: Sequence[TimeFrame], name: str = '', tz: str = 'Etc/UTC'):
""""""
self.config = Config()
self.tz = pytz.timezone(tz)
self.start = start.replace(tzinfo=self.tz)
self.end = end.replace(tzinfo=self.tz)
- self.symbols = symbols
- self.timeframes = timeframes
+ self.symbols = set(symbols)
+ self.timeframes = set(timeframes)
self.name = name or f"{start:%d-%m-%y}_{end:%d-%m-%y}"
diff = int((self.end - self.start).total_seconds())
self.range = range(diff)
self.span = range(start := int(self.start.timestamp()), diff + start)
+ self.data = Data(name=name)
self.mt5 = MetaTrader()
- async def get_data(self) -> Data:
+ async def get_data(self):
""""""
-
rates, ticks, prices, symbols, account = await asyncio.gather(self.get_symbols_rates(), self.get_symbols_ticks(),
self.get_symbols_prices(), self.get_symbols_info(),
self.get_account_info())
- return Data(account=account, symbols=symbols, prices=prices, ticks=ticks, rates=rates,
- span=self.span, range=self.range)
+ terminal, version = await asyncio.gather(self.get_terminal_info(), self.get_version())
- async def pickle_data(self) -> None:
+ self.data.setattrs(account=account, symbols=symbols, prices=prices, ticks=ticks, rates=rates,
+ span=self.span, range=self.range, terminal=terminal, version=version, name=self.name)
+
+ def pickle_data(self):
""""""
- data = await self.get_data()
- fh = open(f'{self.config.root}/data/{self.name}', 'wb')
- pickle.dump(data, fh)
+ fh = open(f'{self.config.test_data_dir}/{self.name}', 'wb')
+ pickle.dump(self.data, fh)
fh.close()
async def compress_data(self):
""""""
- data = await self.get_data()
- bdata = pickle.dumps(data)
+ bdata = pickle.dumps(self.data)
name = self.name + 'xz'
- with lzma.open(name, 'w') as fh:
+ with lzma.open(f'{self.config.test_data_dir}/{name}', 'w') as fh:
fh.write(bdata)
@classmethod
- def load_data(cls, name: str, compressed=False) -> dict:
+ def dump_data(cls, data: Data, name: str | Path, compress: bool = False) -> None:
""""""
- fo = open(f'{cls.config.root}/data/{name}', 'rb')
- data = fo.read()
+ try:
+ fo = open(name, 'wb')
+ if compress:
+ data = lzma.compress(pickle.dumps(data))
+ else:
+ data = pickle.dumps(data)
- if compressed:
- data = lzma.decompress(data)
- else:
- data = pickle.loads(data)
+ fo.write(data)
+ fo.close()
+ except Exception as err:
+ logger.error(f"Error in dump_data: {err}")
- fo.close()
- return data
+ @classmethod
+ def load_data(cls, *, name: str | Path, compressed=False) -> Data | None:
+ """"""
+ try:
+ fo = open(name, 'rb')
+ data = fo.read()
+
+ if compressed:
+ data = lzma.decompress(data)
+ else:
+ data = pickle.loads(data)
+
+ fo.close()
+
+ return data
+ except Exception as err:
+ logger.error(f"Error: {err}")
+ return None
+
+ async def get_terminal_info(self) -> dict[str, [str | int | bool | float]]:
+ """"""
+ terminal = await self.mt5.terminal_info()
+ return terminal._asdict()
+
+ async def get_version(self) -> tuple[int, int, str]:
+ """"""
+ version = await self.mt5.version()
+ return version
async def get_symbols_info(self) -> dict[str, dict]:
""""""
diff --git a/src/aiomql/contrib/backtester/meta_tester.py b/src/aiomql/contrib/backtester/meta_tester.py
new file mode 100644
index 0000000..13c58d0
--- /dev/null
+++ b/src/aiomql/contrib/backtester/meta_tester.py
@@ -0,0 +1,188 @@
+from datetime import datetime
+from logging import getLogger
+
+from numpy import ndarray
+from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TerminalInfo, TradeOrder, TradePosition, TradeDeal,
+ OrderCheckResult, OrderSendResult)
+
+from .test_data import TestData
+from .get_data import GetData
+
+from ...core.meta_trader import MetaTrader
+from ...core.constants import TimeFrame, CopyTicks, OrderType
+from ...utils import error_handler
+
+logger = getLogger(__name__)
+
+
+class MetaTester(MetaTrader):
+ """A class for testing trading strategies in the MetaTrader 5 terminal. A subclass of MetaTrader."""
+
+ def __init__(self, test_data: TestData = None):
+ super().__init__()
+ if self.test_data:
+ self.config.test_data = test_data
+
+ @property
+ def test_data(self) -> TestData | None:
+ test_data = self.config.test_data
+ if test_data is None:
+ ...
+ # logger.error('No Test Data Available')
+ return test_data
+
+ @test_data.setter
+ def test_data(self, value: TestData):
+ self.config.test_data = value
+
+ async def initialize(self, path: str = "", login: int = 0, password: str = "", server: str = "",
+ timeout: int | None = None, portable=False, load_test_data: bool = False,
+ test_data_file: str = '', use_terminal: bool = True) -> bool:
+
+ success = True
+ if self.config.use_terminal_for_backtesting:
+ success = await super().initialize(path=path, login=login, password=password, server=server, timeout=timeout)
+
+ try:
+ if load_test_data:
+ name = f"{self.config.test_data_dir_name}/{test_data_file}"
+ data = GetData.load_data(name=name, compressed=self.config.compress_test_data)
+ if data is not None:
+ self.test_data = TestData(data)
+ success = True
+
+ except Exception as err:
+ logger.error(f'{err}: unable to load test data')
+ success = False
+
+ return success
+
+ async def login(self, login: int, password: str, server: str, timeout: int = 60000) -> bool:
+ return await super().login(login, password, server, timeout) if self.config.use_terminal_for_backtesting else True
+
+ async def shutdown(self) -> None:
+ await super().shutdown() if self.config.use_terminal_for_backtesting else ...
+ self.test_data.save()
+ name = self.test_data.data.name
+
+ if self.config.compress_test_data:
+ name += '.xz'
+ name = self.config.test_data_dir/name
+ GetData.dump_data(data=self.test_data.data, name=name, compress=self.config.compress_test_data)
+
+ @error_handler(msg='test data not available', exe=AttributeError)
+ async def terminal_info(self) -> TerminalInfo:
+ return self.test_data.get_terminal_info()
+
+ @error_handler(msg='test data not available', exe=AttributeError)
+ async def account_info(self) -> AccountInfo:
+ """"""
+ return self.test_data.get_account_info()
+
+ @error_handler(msg='test data not available', exe=AttributeError)
+ async def symbol_select(self, symbol: str, enable: bool = True) -> bool:
+ return symbol in self.test_data.symbols and enable
+
+ @error_handler(msg='test data not available', exe=AttributeError)
+ async def symbols_total(self) -> int:
+ return self.test_data.get_symbols_total()
+
+ @error_handler(msg='test data not available', exe=AttributeError)
+ async def symbols_get(self, group: str = "") -> tuple[SymbolInfo, ...] | None:
+ """"""
+ return self.test_data.get_symbols(group)
+
+ @error_handler(msg='test data not available', exe=AttributeError)
+ async def symbol_info(self, symbol: str) -> SymbolInfo | None:
+ return self.test_data.symbols.get(symbol)
+
+ @error_handler(msg='test data not available', exe=AttributeError)
+ async def symbol_info_tick(self, symbol: str) -> Tick | None:
+ return self.test_data.get_symbol_info_tick(symbol)
+
+ @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:
+ return self.test_data.get_rates_from(symbol, timeframe, date_from, 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:
+ return self.test_data.get_rates_from_pos(symbol, timeframe, start_pos, count)
+
+ @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:
+ return self.test_data.get_rates_range(symbol, timeframe, date_from, date_to)
+
+ @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:
+ return self.test_data.get_ticks_from(symbol, date_from, count, 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:
+ return self.test_data.get_ticks_range(symbol, date_from, date_to, flags)
+
+ @error_handler(msg='test data not available', exe=AttributeError)
+ async def orders_total(self) -> int:
+ return self.test_data.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}
+ return self.test_data.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, use_terminal: bool = True) -> float | None:
+ res = await self.test_data.order_calc_margin(action, symbol, volume, price, use_terminal=use_terminal)
+ return res
+
+ @error_handler(msg='test data not available', exe=AttributeError)
+ async def order_calc_profit(self, action: OrderType, symbol: str, volume: float, price_open: float,
+ price_close: float, use_terminal: bool = True) -> float | None:
+ return await self.test_data.order_calc_profit(action, symbol, volume,
+ price_open, price_close, use_terminal=use_terminal)
+
+ @error_handler(msg='test data not available', exe=AttributeError)
+ async def order_check(self, request: dict, use_terminal: bool = True) -> OrderCheckResult:
+ return await self.test_data.order_check(request, use_terminal=use_terminal)
+
+ async def order_send(self, request: dict, use_terminal: bool = True) -> OrderSendResult:
+ return await self.test_data.order_send(request, use_terminal=use_terminal)
+
+ @error_handler(msg='test data not available', exe=AttributeError)
+ async def positions_total(self) -> int:
+ return self.test_data.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}
+ return self.test_data.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:
+ return self.test_data.get_history_orders_total(date_from, 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:
+ 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)
+ return self.test_data.get_history_orders(*args, **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:
+ return self.test_data.get_history_deals_total(date_from, 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:
+ 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)
+ return self.test_data.get_history_deals(*args, **kwargs)
diff --git a/src/aiomql/contrib/backtester/strategy_tester.py b/src/aiomql/contrib/backtester/strategy_tester.py
new file mode 100644
index 0000000..257cf02
--- /dev/null
+++ b/src/aiomql/contrib/backtester/strategy_tester.py
@@ -0,0 +1,35 @@
+import asyncio
+
+from .event_manager import EventManager
+from .get_data import GetData
+from .test_data import TestData
+from .meta_tester import MetaTester
+
+from ...core import Config
+
+class StrategyTester:
+ def __init__(self, *, strategies: list = None, test_data: TestData = None, test_data_file: str = ''):
+ self.config = Config()
+ self.mt5 = MetaTester()
+ self.strategies = strategies or []
+ self.test_data = test_data or self.get_test_data(name=test_data_file)
+ self.config.test_data = self.test_data
+ self.event_manager = EventManager(num_tasks=len(self.strategies))
+
+ def get_test_data(self, name: str) -> TestData | None:
+ name = f"{self.config.test_data_dir_name}/{name or self.config.test_data_file}"
+ data = GetData.load_data(name=name, compressed=self.config.compress_test_data)
+ return TestData(data) if data is not None else None
+
+ async def start(self):
+ acc = self.config.account_info()
+ await self.mt5.initialize(**acc)
+ await self.mt5.login(**acc)
+
+ async def run(self):
+ await self.start()
+ tasks = [*[asyncio.create_task(strategy.test()) for strategy in self.strategies],
+ asyncio.create_task(self.event_manager.event_monitor())]
+ self.event_manager.add_task(*tasks)
+ await asyncio.gather(*tasks)
+ await self.mt5.shutdown()
diff --git a/src/aiomql/contrib/backtester/test_account.py b/src/aiomql/contrib/backtester/test_account.py
new file mode 100644
index 0000000..1fd7f02
--- /dev/null
+++ b/src/aiomql/contrib/backtester/test_account.py
@@ -0,0 +1,37 @@
+from dataclasses import dataclass, asdict
+from ...core.constants import AccountTradeMode, AccountMarginMode, AccountStopOutMode
+
+
+@dataclass
+class AccountInfo:
+ login: int = 0
+ server: str = ''
+ trade_mode: AccountTradeMode = AccountTradeMode.DEMO
+ balance: float = 0
+ leverage: float = 0
+ profit: float = 0
+ equity: float = 0
+ credit: float = 0
+ margin: float = 0
+ margin_level: float = 0
+ margin_free: float = 0
+ margin_mode: AccountMarginMode = AccountMarginMode.EXCHANGE
+ margin_so_mode: AccountStopOutMode = AccountStopOutMode.PERCENT
+ margin_so_call: float = 0
+ margin_so_so: float = 0
+ margin_initial: float = 0
+ margin_maintenance: float = 0
+ fifo_close: bool = False
+ limit_orders: float = 0
+ currency: str = "USD"
+ trade_allowed: bool = True
+ trade_expert: bool = True
+ currency_digits: int = 2
+ assets: float = 0
+ liabilities: float = 0
+ commission_blocked: float = 0
+ name: str = ''
+ company: str = ''
+
+ def asdict(self):
+ return asdict(self)
diff --git a/src/aiomql/core/backtester/test_data.py b/src/aiomql/contrib/backtester/test_data.py
similarity index 61%
rename from src/aiomql/core/backtester/test_data.py
rename to src/aiomql/contrib/backtester/test_data.py
index a02eea1..afaf937 100644
--- a/src/aiomql/core/backtester/test_data.py
+++ b/src/aiomql/contrib/backtester/test_data.py
@@ -1,20 +1,23 @@
+import asyncio
from collections import namedtuple
from datetime import datetime
from typing import Literal
from itertools import zip_longest
import random
+import pandas as pd
import pytz
import numpy as np
from pandas import DataFrame
from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TradeOrder, TradePosition, TradeDeal,
- ORDER_TYPE_BUY, ORDER_TYPE_SELL, TradeRequest, OrderCheckResult, OrderSendResult,
- ACCOUNT_STOPOUT_MODE_PERCENT)
-from ..meta_trader import MetaTrader
-from ..constants import TimeFrame, CopyTicks, OrderType, TradeAction
+ TradeRequest, OrderCheckResult, OrderSendResult, TerminalInfo)
+
+from ...core.meta_trader import MetaTrader
+from ...core.constants import TimeFrame, CopyTicks, OrderType, TradeAction, AccountStopOutMode
from .get_data import Data
-from ...account import Account
-from ...utils import round_down, round_up
+from .test_account import AccountInfo as Account
+from ...utils import round_down, round_up, error_handler, error_handler_sync
+# from .event_manager import EventManager
tz = pytz.timezone('Etc/UTC')
Cursor = namedtuple('Cursor', ['index', 'time'])
@@ -26,16 +29,17 @@ class TestData:
def __init__(self, data: Data):
self._data = data
- self.account = Account(**data.account)
- self.symbols = {symbol: SymbolInfo(**info) for symbol, info in data.symbols.items()}
- self.prices = data.prices
- self.ticks = data.ticks
- self.rates = data.rates
- self.span = data.span
- self.range = data.range
- self.cursor = Cursor(index=self.range[0], time=self.span[0])
- self.iter = zip_longest(self.range, self.span)
+ self.version: tuple[int, int, str] = data.version
+ self.terminal_info = TerminalInfo(data.terminal)
+ self.account: Account = Account(**data.account)
+ self.symbols: dict[str, SymbolInfo] = {symbol: SymbolInfo(info) for symbol, info in data.symbols.items()}
+ self.prices: dict[str, DataFrame] = data.prices
+ self.ticks: dict[str, DataFrame] = data.ticks
+ self.rates: dict[str, dict[str, DataFrame]] = data.rates
+ self.span: range = data.span
+ self.range: range = data.range
self.orders: dict[str, dict[int, TradeOrder]] = {}
+ self.deals: dict[str, dict[int, TradeDeal]] = {}
self.open_orders: dict[int, TradeOrder] = {}
self.positions: dict[str, dict[int, TradePosition]] = {}
self.open_positions: dict[int, TradePosition] = {}
@@ -43,38 +47,261 @@ class TestData:
self.history_deals = data.history_deals
self.margins: dict[int, float] = {}
self.mt5 = MetaTrader()
+ self.iter = zip_longest(self.range, self.span)
+ self.cursor = next(self)
+ # self.event_manager = EventManager()
- def __next__(self):
+ def __next__(self) -> Cursor:
index, time = next(self.iter)
self.cursor = Cursor(index=index, time=time)
return self.cursor
+
+ def next(self) -> Cursor:
+ return next(self)
+
+ @property
+ def data(self):
+ return self._data
def reset(self):
self.iter = zip_longest(self.range, self.span)
self.cursor = Cursor(index=self.range[0], time=self.span[0])
return self.cursor
+ def go_to(self, index: int, time: int):
+ range_ = range(time, self.range.stop, self.range.step)
+ span = range(index, self.span.stop, self.span.step)
+ self.iter = zip_longest(range_, span)
+ self.cursor = next(self)
+
+ def get_dtype(self, 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) for ticket in self.open_positions]
+ await asyncio.gather(*pos_tasks)
+ order_tasks = [self.check_order(ticket) for ticket in self.open_orders]
+ await asyncio.gather(*order_tasks)
+
+ def save(self):
+ self._data.history_deals = self.history_deals
+ self._data.history_orders = self.history_orders
+ for symbol in self.orders:
+ self.history_orders = pd.concat([DataFrame(self.orders[symbol].values()), self.history_orders])
+ self._data.history_orders = self.history_orders
+ for symbol in self.deals:
+ self.history_deals = pd.concat([DataFrame(self.deals[symbol].values()), self.history_deals])
+ self._data.history_deals = self.history_deals
+
+ @error_handler
+ async def check_order(self, ticket: int):
+ order = self.open_orders[ticket]
+ order_type, symbol = order.type, order.symbol
+ tick = self.prices[symbol].loc[self.cursor.time]
+ tp, sl = order.tp, order.sl
+
+ match order_type:
+ case OrderType.BUY:
+ if tp >= tick.bid or sl <= tick.bid:
+ self.close_position(ticket)
+
+ case OrderType.SELL:
+ if tp <= tick.ask or sl >= tick.ask:
+ self.close_position(ticket)
+ case _:
+ ...
+
+ @error_handler
+ async def check_position(self, ticket: int, use_terminal=True):
+ pos = self.open_positions[ticket]
+ order_type, symbol, volume, price_open, prev_profit = pos.type, pos.symbol, pos.volume, pos.price_open, pos.profit
+ tick = self.prices[symbol].loc[self.cursor.time]
+ price_current = tick.bid if order_type == OrderType.BUY else tick.ask
+ profit = await self.order_calc_profit(order_type, symbol, volume, price_open, price_current, use_terminal)
+ self.update_account(equity=profit - prev_profit)
+ pos = pos._asdict()
+ pos.update(profit=profit, price_current=price_current, time_update=self.cursor.time)
+ pos = TradePosition(pos)
+ self.open_positions[ticket] = pos
+ self.positions[symbol][ticket] = pos
+
+ def close_position(self, ticket: int):
+ position = self.open_positions.pop(ticket)
+ margin = self.margins.pop(position.ticket)
+ order = self.open_orders.pop(ticket)
+ order = order._asdict()
+ order.update(time_done=self.cursor.time)
+ self.orders[order['symbol']][ticket] = TradeOrder(order)
+ self.update_account(profit=position.profit, margin=-margin)
+
+ def modify_stops(self, ticket: int, sl: int = None, tp: int = None):
+ pos = self.open_positions.pop(ticket)
+ order = self.open_orders.pop(ticket)
+ sl = sl or pos.sl
+ tp = tp or pos.tp
+ pos = pos._asdict()
+ pos.update(tp=tp, sl=sl, time_update=self.cursor.time)
+ sl = sl or order.sl
+ tp = tp or order.tp
+ order = order._asdict()
+ order.update(tp=tp, sl=sl)
+ pos = TradePosition(pos)
+ order = TradeOrder(order)
+ self.open_positions[ticket] = pos
+ self.open_orders[ticket] = order
+ self.positions[pos.symbol][ticket] = pos
+ self.orders[order.symbol][ticket] = order
+
+ def update_account(self, *, profit: float = 0, margin: float = 0, equity: float = 0):
+ self.account.balance += profit
+ self.account.equity += equity
+ self.account.margin += margin
+ self.account.margin_free = self.account.equity - self.account.margin
+ self.account.margin_level = (self.account.equity / (self.account.margin or 1)) * 100 \
+ if self.account.margin_mode == AccountStopOutMode.PERCENT else self.account.margin_free
+
+ @error_handler
+ async def order_send(self, request: dict, use_terminal: bool = True) -> OrderSendResult:
+ osr = {'retcode': 10009, 'comment': 'Request completed', 'request': TradeRequest(request)}
+
+ if (position := request.get('position')) in self.open_positions:
+ pos = self.open_positions[position]
+ order_type = OrderType(request['type'])
+ pos_type = OrderType(pos.type)
+ if order_type.opposite == pos_type: # ToDo: is there another way to check if the order is a close order?
+ # close position
+ self.close_position(pos.ticket)
+ return OrderSendResult(osr) # ToDo: Create a deal object here
+ action = request['action']
+ if action == TradeAction.SLTP:
+ self.modify_stops(position, request['sl'], request['tp'])
+ return OrderSendResult(osr)
+
+ if (action := request.get('action')) == TradeAction.DEAL:
+ ocr = await self.order_check(request, use_terminal=use_terminal)
+ if ocr.retcode != 0:
+ osr.update({'comment': ocr.comment, 'retcode': ocr.retcode})
+ return OrderSendResult(osr)
+
+ ticket = random.randint(100_000_000, 999_999_999)
+ deal_ticket = random.randint(100_000_000, 999_999_999)
+ tick = self.get_symbol_info_tick(request['symbol'])
+ order_type = request['type']
+ price = tick.ask if request['type'] == OrderType.BUY else tick.bid
+ volume = request['volume']
+ sl, tp = request.get('sl', 0), request.get('tp', 0)
+ symbol = request['symbol']
+
+ pos = {'comment': 'open position', 'ticket': ticket, 'symbol': symbol, 'volume': volume,
+ 'price_open': price, 'price_current': price, 'type': order_type, 'profit': 0,
+ 'sl': sl, 'tp': tp, 'time': tick.time,
+ 'time_msc': tick.time_msc}
+
+ order = {'ticket': ticket, 'symbol': symbol, 'volume': volume, 'price': price, 'price_current': price,
+ 'price_open': price, 'type': order_type, 'time_setup': tick.time,
+ 'time_setup_msc': tick.time_msc, 'volume_current': volume, 'sl': sl, 'tp': tp, }
+
+ pos = TradePosition(pos)
+ order = TradeOrder(order)
+ # ToDo: Create a deal object here
+ self.open_positions[pos.ticket] = pos
+ self.open_orders[order.ticket] = order
+ self.orders.setdefault(order.symbol, {})[order.ticket] = order
+ self.positions.setdefault(pos.symbol, {})[pos.ticket] = pos
+ osr.update({'order': ticket, 'price': price, 'volume': volume, 'bid': tick.bid,
+ 'ask': tick.ask, 'deal': deal_ticket})
+ margin = await self.order_calc_margin(action, symbol, volume, price, use_terminal=use_terminal)
+ self.margins[ticket] = margin
+ self.update_account(margin=margin)
+ return OrderSendResult(osr)
+
+ @error_handler
+ async def order_check(self, request: dict, use_terminal=True) -> OrderCheckResult:
+ action, symbol, volume = request.get('action'), request.get('symbol'), request.get('volume')
+ price = request.get('price')
+ ocr = {'retcode': 0, 'balance': 0, 'profit': 0, 'margin': 0, 'equity': 0, 'margin_free': 0,
+ 'margin_level': 0, 'comment': 'Done', request: TradeRequest(request)}
+
+ margin = 0
+ if all([action, symbol, volume, price]):
+ margin = await self.order_calc_margin(action, symbol, volume, price, use_terminal=use_terminal)
+
+ acc = self.get_account_info()
+ equity = acc.equity
+ used_margin = acc.margin + margin
+ free_margin = acc.margin_free - margin
+ margin_level = (equity / used_margin) * 100 if (
+ acc.margin_mode == AccountStopOutMode.PERCENT and used_margin > 0) else free_margin
+
+ if use_terminal and self.mt5.config.use_terminal_for_backtesting:
+ ocr_t = await self.mt5.order_check(request)
+ # return order check result if invalid stops level are detected or bad request
+ if ocr_t.retcode in (10016, 10013, 10014):
+ return ocr_t
+
+ sym = self.symbols[symbol]
+ tsl = sym.trade_stops_level
+ sl, tp = request.get('sl', 0), request.get('tp', 0)
+
+ # check if the stops level is valid
+ if tp or sl:
+ min_sl = min(sl, tp)
+ dsl = abs(price - min_sl) / sym.point
+ if dsl < tsl:
+ ocr['retcode'] = 10016
+ ocr['comment'] = 'Invalid stops'
+ return OrderCheckResult(ocr)
+
+ # check if the account has enough money
+ if margin_level < acc.margin_so_call:
+ ocr['retcode'] = 10019
+ ocr['comment'] = 'No money'
+
+ # check volume
+ if volume < sym.volume_min or volume > sym.volume_max:
+ ocr['retcode'] = 10014
+ ocr['comment'] = 'Invalid volume'
+
+ ocr.update({'balance': acc.balance, 'profit': acc.profit, 'margin': used_margin, 'equity': equity,
+ 'margin_free': free_margin, 'margin_level': margin_level})
+
+ return OrderCheckResult(ocr)
+
+ @error_handler_sync
+ def get_terminal_info(self) -> TerminalInfo:
+ return self.terminal_info
+
+ @error_handler_sync
+ def get_version(self) -> tuple[int, int, str]:
+ return self.version
+
+ @error_handler_sync
def get_symbols_total(self) -> int:
return len(self.symbols)
- def get_symbols(self) -> list:
- return list(self.symbols.keys())
+ @error_handler_sync
+ def get_symbols(self, group: str = '') -> tuple[SymbolInfo, ...]:
+ return tuple(list(self.symbols.values()))
+ @error_handler_sync
def get_account_info(self) -> AccountInfo:
- return AccountInfo(**self.account._asdict())
+ return AccountInfo(self.account.asdict())
+ @error_handler_sync
def get_symbol_info_tick(self, symbol: str) -> Tick:
tick = self.prices[symbol].iloc[self.cursor.index]
- return Tick(**tick)
-
+ return Tick(tick)
+
+ @error_handler_sync
def get_symbol_info(self, symbol: str) -> SymbolInfo:
info = self.symbols[symbol]
tick = self.get_symbol_info_tick(symbol)
info = info._asdict()
info |= {'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)
+ return SymbolInfo(info)
+ @error_handler_sync
def get_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int) -> np.ndarray:
rates = self.rates[symbol][timeframe.name]
start = int(datetime.timestamp(date_from)) if isinstance(date_from, datetime) else int(date_from)
@@ -82,14 +309,16 @@ class TestData:
start = rates[rates.index <= start].iloc[-1].name
start = rates.index.get_loc(start)
end = start + count
- return rates.iloc[start:end].to_numpy()
+ return np.fromiter((tuple(i) for i in rates.iloc[start:end].iloc), dtype=self.get_dtype(rates))
+ @error_handler_sync
def get_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int) -> np.ndarray:
rates = self.rates[symbol][timeframe.name]
end = -start_pos + count
end = end or None
- return rates.iloc[-start_pos:end].to_numpy()
+ return np.fromiter((tuple(i) for i in rates.iloc[-start_pos:end].iloc), dtype=self.get_dtype(rates))
+ @error_handler_sync
def get_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, date_to: datetime | float) -> np.ndarray:
rates = self.rates[symbol][timeframe.name]
start = int(datetime.timestamp(date_from)) if isinstance(date_from, datetime) else int(date_from)
@@ -98,180 +327,50 @@ class TestData:
end = int(datetime.timestamp(date_to)) if isinstance(date_to, datetime) else int(date_to)
end = round_up(end, timeframe.time)
end = rates[rates.index >= end].iloc[-1].name
- return rates.loc[start:end].to_numpy()
+ return np.fromiter((tuple(i) for i in rates.loc[start:end].iloc), dtype=self.get_dtype(rates))
+ @error_handler_sync
def get_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks) -> np.ndarray:
ticks = self.ticks[symbol]
start = int(datetime.timestamp(date_from)) if isinstance(date_from, datetime) else int(date_from)
start = ticks[ticks.index <= start].iloc[-1].name
start = ticks.index.get_loc(start)
end = start + count
- return ticks.iloc[start:end]
-
+ return np.fromiter((tuple(i) for i in ticks.iloc[start:end].iloc), dtype=self.get_dtype(ticks))
+
+ @error_handler_sync
def get_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, flags) -> np.ndarray:
ticks = self.ticks[symbol]
start = int(datetime.timestamp(date_from)) if isinstance(date_from, datetime) else int(date_from)
start = ticks[ticks.index <= start].iloc[-1].index
end = int(datetime.timestamp(date_to)) if isinstance(date_to, datetime) else int(date_to)
end = ticks[ticks.index >= end].iloc[-1].index
- return ticks.loc[start:end].to_numpy()
+ return np.fromiter((tuple(i) for i in ticks.loc[start:end].iloc), dtype=self.get_dtype(ticks))
+ @error_handler
async def order_calc_margin(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float,
price: float, use_terminal=False):
if use_terminal and self.mt5.config.use_terminal_for_backtesting:
- return await self.mt5.order_calc_margin(OrderType(action), symbol, volume, price)
+ 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 margin
+ 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=True):
if use_terminal and self.mt5.config.use_terminal_for_backtesting:
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)
- return profit
+ 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)
- def check_order(self, ticket: int) -> bool:
- order = self.open_orders[ticket]
- order_type, symbol = order.type, order.symbol
- tick = self.prices[symbol].loc[self.cursor.time]
- tp, sl = order.tp, order.sl
-
- match order_type:
- case self.mt5._ORDER_TYPE_BUY:
- if tp >= tick.bid or sl <= tick.bid:
- self.close_position(ticket)
-
- case self.mt5.ORDER_TYPE_SELL:
- if tp <= tick.ask or sl >= tick.ask:
- self.close_position(ticket)
-
- case _:
- ...
-
- def check_position(self, ticket: int) -> bool:
- ...
-
- def close_position(self, ticket: int):
- position = self.open_positions.pop(ticket)
- margin = self.margins.pop(position.ticket)
- profit = position.profit
- self.update_account(profit, margin=margin)
-
- async def modify_stops(self, ticket: int, sl: int = None, tp: int = None):
- pos = self.open_positions.pop(ticket)
- sl = sl or pos.sl
- tp = tp or pos.tp
- order_type, symbol, volume, price_open = pos.order_type, pos.symbol, pos.volume
- pos = pos._asdict()
- pos.update(tp=tp, sl=sl, time_update=self.cursor.time)
- profit = await self.mt5.order_calc_profit(order_type, symbol, volume, price_open, sl)
- self.open_positions[ticket] = TradePosition(**pos)
-
- def update_account(self, profit: float, margin: float = 0):
- self.account.balance += profit
- self.account.equity += profit
- self.account.margin -= margin
- self.account.margin_free = self.account.equity - self.account.margin
- self.account.margin_level = (self.account.equity / self.account.margin) * 100 if self.account.margin_mode
-
- async def order_send(self, request: dict, use_terminal: bool = True) -> OrderSendResult:
- osr = {'retcode': 10009, 'comment': 'Request completed', 'request': TradeRequest(**request)}
-
- if (position := request.get('position')) in self.open_positions:
- pos = self.open_positions[position]
- order_type = OrderType(request['type'])
- pos_type = OrderType(pos.type)
- if order_type.opposite == pos_type: # ToDo: is there another way to check if the order is a close order?
- # close position
- self.close_position(pos)
- return OrderSendResult(**osr) # ToDo: Create a deal object here
- action = request['action']
- if action == TradeAction.SLTP:
- self.modify_stops(position, request['sl'], request['tp'])
- return OrderSendResult(**osr)
-
- if (action := request.get('action')) == TradeAction.DEAL:
- ocr = await self.order_check(request, use_terminal=use_terminal)
- if ocr.retcode != 0:
- osr.update({'comment': ocr.comment, 'retcode': ocr.retcode})
- return OrderSendResult(**osr)
-
- ticket = random.randint(100_000_000, 999_999_999)
- deal_ticket = random.randint(100_000_000, 999_999_999)
- tick = self.get_symbol_info_tick(request['symbol'])
- order_type = request['type']
- price = tick.ask if request['type'] == ORDER_TYPE_BUY else tick.bid
- volume = request['volume']
- sl, tp = request.get('sl', 0), request.get('tp', 0)
- symbol = request['symbol']
- pos = {'comment': 'open position', 'ticket': ticket, 'symbol': symbol, 'volume': volume,
- 'price_open': price, 'price_current': price, 'type': order_type, 'profit': 0,
- 'sl': sl, 'tp': tp, 'time': tick.time,
- 'time_msc': tick.time_msc}
- order = {'ticket': ticket, 'symbol': symbol, 'volume': volume, 'price': price, 'price_current': price,
- 'price_open': price, 'type': order_type, 'time_setup': tick.time,
- 'time_setup_msc': tick.time_msc, 'volume_current': volume, 'sl': sl, 'tp': tp,}
- pos = TradePosition(**pos)
- order = TradeOrder(**order)
- # ToDo: Create a deal object here
- self.open_positions[pos.ticket] = pos
- self.open_orders[order.ticket] = order
- self.orders.setdefault(order.symbol, {})[order.ticket] = order
- self.positions.setdefault(pos.symbol, {})[pos.ticket] = pos
- osr.update({'order': ticket, 'price': price, 'volume': volume, 'bid': tick.bid,
- 'ask': tick.ask, 'deal': deal_ticket})
- margin = await self.order_calc_margin(action, symbol, volume, price)
- self.margins[ticket] = margin
- return OrderSendResult(**osr)
-
- async def order_check(self, request: dict, use_terminal=True) -> OrderCheckResult:
- action, symbol, volume = request.get('action'), request.get('symbol'), request.get('volume')
- price = request.get('price')
- ocr = {'retcode': 0, 'balance': 0, 'profit': 0, 'margin': 0, 'equity': 0, 'margin_free': 0,
- 'margin_level': 0, 'comment': 'Done', request: TradeRequest(**request)}
-
- margin = 0
- if all([action, symbol, volume, price]):
- margin = await self.order_calc_margin(action, symbol, volume, price)
-
- acc = self.get_account_info()
- equity = acc.equity
- used_margin = acc.margin + margin
- free_margin = acc.margin_free - margin
- margin_level = (equity / used_margin) * 100 if (acc.margin_mode == ACCOUNT_STOPOUT_MODE_PERCENT and used_margin > 0) else free_margin
-
- if use_terminal and self.mt5.config.use_terminal_for_backtesting:
- ocr_t = await self.mt5.order_check(request)
- # return order check result if invalid stops level are detected or bad request
- if ocr_t.retcode in (10016, 10013, 10014):
- return ocr_t
- else:
- sym = self.symbols[symbol]
- tsl = sym.trade_stops_level
- sl, tp = request.get('sl', 0), request.get('tp', 0)
-
- if tp or sl:
- min_sl = min(sl, tp)
- dsl = abs(price - min_sl) / sym.point
- if dsl < tsl:
- ocr['retcode'] = 10016
- ocr['comment'] = 'Invalid stops'
- return OrderCheckResult(**ocr)
-
- if margin_level < acc.margin_so_call:
- ocr['retcode'] = 10019
- ocr['comment'] = 'No money'
-
- ocr.update({'balance': acc.balance, 'profit': acc.profit, 'margin': used_margin, 'equity': equity,
- 'margin_free': free_margin, 'margin_level': margin_level})
-
- return OrderCheckResult(**ocr)
-
+ @error_handler_sync
def get_orders_total(self) -> int:
return len(self.open_orders)
-
+
+ @error_handler_sync
def get_orders(self, symbol: str = '', group: str = '', ticket: int = None) -> tuple[TradeOrder, ...]:
if ticket:
order = self.open_orders.get(ticket)
@@ -286,9 +385,11 @@ class TestData:
else:
return tuple(order for order in self.open_orders.values())
- def get_positions_total(self):
+ @error_handler_sync
+ def get_positions_total(self) -> int:
return len(self.open_positions)
+ @error_handler_sync
def get_positions(self, symbol: str = '', group: str = '', ticket: int = None) -> tuple[TradePosition, ...]:
if ticket:
position = self.open_positions.get(ticket)
@@ -302,15 +403,17 @@ class TestData:
else:
return tuple(position for position in self.open_positions.values())
-
- def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
+
+ @error_handler_sync
+ def get_history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
start = int(date_from.timestamp()) if isinstance(date_from, datetime) else int(date_from)
end = int(date_to.timestamp()) if isinstance(date_to, datetime) else int(date_to)
start = self.history_orders[self.history_orders.index >= start].iloc[0].name
end = self.history_orders[self.history_orders.index <= end].iloc[-1].name
return self.history_orders.loc[start:end].shape[0]
- def history_orders_get(self, date_from: datetime | float, date_to: datetime | float, group: str = '',
+ @error_handler_sync
+ def get_history_orders(self, date_from: datetime | float, date_to: datetime | float, group: str = '',
ticket: int = None, position: int = None) -> tuple[TradeOrder, ...]:
start = int(date_from.timestamp()) if isinstance(date_from, datetime) else int(date_from)
end = int(date_to.timestamp()) if isinstance(date_to, datetime) else int(date_to)
@@ -328,8 +431,9 @@ class TestData:
...
orders.drop(columns=['symbol'], inplace=True)
- return tuple(TradeOrder(**order) for order in orders.to_dict(orient='records'))
+ return tuple(TradeOrder(order) for order in orders.iloc)
+ @error_handler_sync
def get_history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
start = int(date_from.timestamp()) if isinstance(date_from, datetime) else int(date_from)
end = int(date_to.timestamp()) if isinstance(date_to, datetime) else int(date_to)
@@ -337,6 +441,7 @@ class TestData:
end = self.history_deals[self.history_deals.index <= end].iloc[-1].name
return self.history_deals.loc[start:end].shape[0]
+ @error_handler_sync
def get_history_deals(self, date_from: datetime | float, date_to: datetime | float, group: str = '',
position: int = None, ticket: int = None) -> tuple[TradeDeal, ...]:
start = int(date_from.timestamp()) if isinstance(date_from, datetime) else int(date_from)
@@ -354,5 +459,4 @@ class TestData:
elif group:
...
- deals.drop(columns=['symbol'], inplace=True)
- return tuple(TradeDeal(**deal) for deal in deals.to_dict(orient='records'))
+ return tuple(TradeDeal(deal) for deal in deals.iloc)
diff --git a/src/aiomql/contrib/backtester/test_executor.py b/src/aiomql/contrib/backtester/test_executor.py
new file mode 100644
index 0000000..0ae1218
--- /dev/null
+++ b/src/aiomql/contrib/backtester/test_executor.py
@@ -0,0 +1,2 @@
+class FingerTrapTest:
+ ...
diff --git a/src/aiomql/contrib/backtester/test_strategy.py b/src/aiomql/contrib/backtester/test_strategy.py
new file mode 100644
index 0000000..f5aedb4
--- /dev/null
+++ b/src/aiomql/contrib/backtester/test_strategy.py
@@ -0,0 +1,20 @@
+from .event_manager import EventManager
+
+from ...core.config import Config
+
+
+class TestStrategy:
+ event_manager: EventManager
+ config: Config
+
+ def set_up(self):
+ self.config = Config()
+ self.event_manager = EventManager()
+
+ async def sleep(self, secs: float):
+ time = self.config.test_data.cursor.time
+ mod = time % secs
+ secs = secs - mod if mod != 0 else mod
+ time = self.config.test_data.cursor.time + secs
+ while time > self.config.test_data.cursor.time:
+ await self.event_manager.wait()
diff --git a/src/aiomql/core/backtester/__init__.py b/src/aiomql/core/backtester/__init__.py
deleted file mode 100644
index cc065ef..0000000
--- a/src/aiomql/core/backtester/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .meta_tester import MetaTester
-from .test_data import TestData
-from .get_data import GetData
diff --git a/src/aiomql/core/backtester/check.py b/src/aiomql/core/backtester/check.py
deleted file mode 100644
index 868e5a3..0000000
--- a/src/aiomql/core/backtester/check.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import socket
-
-
-class socketserver:
- def __init__(self, address = '192.168.1.15', port = 9090):
- self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.address = address
- self.port = port
- self.sock.bind((self.address, self.port))
- self.cummdata = ''
-
-
- def recvmsg(self):
- g=self.sock.listen(1)
- print(g)
- self.conn, self.addr = self.sock.accept()
- print('connected to', self.addr)
- data = self.conn.recv(10)
- self.cummdata += data.decode("utf-8")
-
-
-so = socketserver()
-so.recvmsg()
diff --git a/src/aiomql/core/backtester/meta_data.py b/src/aiomql/core/backtester/meta_data.py
deleted file mode 100644
index 8b13789..0000000
--- a/src/aiomql/core/backtester/meta_data.py
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/aiomql/core/backtester/meta_tester.py b/src/aiomql/core/backtester/meta_tester.py
deleted file mode 100644
index 31a6d6a..0000000
--- a/src/aiomql/core/backtester/meta_tester.py
+++ /dev/null
@@ -1,207 +0,0 @@
-import pickle
-from datetime import datetime
-from logging import getLogger
-import asyncio
-import re
-
-import pytz
-from MetaTrader5 import Tick, SymbolInfo
-import pandas as pd
-
-from ... import TestData
-from ...core.meta_trader import MetaTrader
-from ...core.config import Config
-from ...core.errors import Error
-from ...core.constants import TimeFrame, CopyTicks, OrderType
-from ...core.models import (AccountInfo, SymbolInfo, BookInfo, TradeOrder, OrderCheckResult, OrderSendResult,
- TradePosition, TradeDeal)
-
-from ...utils import backoff_decorator
-from .test_data import TestData
-from .get_data import GetData
-
-logger = getLogger(__name__)
-
-
-class MetaTester(MetaTrader):
- """A class for testing trading strategies in the MetaTrader 5 terminal. A subclass of MetaTrader."""
- data: TestData
-
- def __init__(self, data: TestData = None):
- super().__init__()
- self.data = data
-
- async def initialize(self, path: str = "", login: int = 0, password: str = "", server: str = "",
- timeout: int | None = None, portable=False, compressed: bool = False) -> bool:
- self.data = await GetData.load_data(name=path, compressed=compressed)
- return True
-
- async def account_info(self) -> AccountInfo:
- """"""
- res = self.data.account
- return res
-
- async def symbols_total(self) -> int:
- return len(self.data.symbols)
-
- async def symbols_get(self, group: str = "") -> tuple[SymbolInfo]:
- """"""
- symbols = self.data.symbols.values()
- return tuple(symbols)
-
- async def symbol_info(self, symbol: str) -> SymbolInfo | None:
- return self.data.symbols.get(symbol)
-
- async def symbol_info_tick(self, symbol: str) -> Tick | None:
- res = await asyncio.to_thread(self._symbol_info_tick, symbol)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in obtaining tick for {symbol}.{self.error.description}')
- return res
- return res
-
- async def symbol_select(self, symbol: str, enable: bool) -> bool:
- return await asyncio.to_thread(self._symbol_select, symbol, enable)
-
- async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int):
- res = await asyncio.to_thread(self._copy_rates_from, symbol, timeframe, date_from, count)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
- return res
- return res
-
- async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int):
- res = await asyncio.to_thread(self._copy_rates_from_pos, symbol, timeframe, start_pos, count)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
- return res
- return res
-
- async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float,
- date_to: datetime | float):
- res = await asyncio.to_thread(self._copy_rates_range, symbol, timeframe, date_from, date_to)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
- return res
- return res
-
- async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks):
- res = await asyncio.to_thread(self._copy_ticks_from, symbol, date_from, count, flags)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in obtaining ticks for {symbol}.{self.error.description}')
- return res
- return res
-
- async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float,
- flags: CopyTicks):
- res = await asyncio.to_thread(self._copy_ticks_range, symbol, date_from, date_to, flags)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in obtaining ticks for {symbol}.{self.error.description}')
- return res
- return res
-
- async def orders_total(self) -> int:
- return await asyncio.to_thread(self._orders_total)
-
- async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder] | None:
- """Get active orders with the ability to filter by symbol or ticket. There are three call options.
- Call without parameters. Return active orders on all symbols
-
- Keyword Args:
- symbol (str): Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored.
-
- group (str): The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function
- returns only active orders meeting a specified criteria for a symbol name.
-
- ticket (int): Order ticket (ORDER_TICKET). Optional named parameter.
-
- Returns:
- tuple[TradeOrder]: A list of active trade orders as TradeOrder objects
- """
- kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
- res = await asyncio.to_thread(self._orders_get, **kwargs)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in obtaining orders.{self.error.description}')
- return res
- return res
-
- async def order_calc_margin(self, action: OrderType, symbol: str, volume: float, price: float) -> float | None:
- res = await asyncio.to_thread(self._order_calc_margin, action, symbol, volume, price)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in calculating margin.{self.error.description}')
- return res
- return res
-
- async def order_calc_profit(self, action: OrderType, symbol: str, volume: float, price_open: float,
- price_close: float) -> float | None:
- res = await asyncio.to_thread(self._order_calc_profit, action, symbol, volume, price_open, price_close)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in calculating profit.{self.error.description}')
- return res
- return res
-
- async def order_check(self, request: dict) -> OrderCheckResult:
- return await asyncio.to_thread(self._order_check, request)
-
- async def order_send(self, request: dict) -> OrderSendResult:
- return await asyncio.to_thread(self._order_send, request)
-
- async def positions_total(self) -> int:
- return await asyncio.to_thread(self._positions_total)
-
- 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}
- res = await asyncio.to_thread(self._positions_get, **kwargs)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in obtaining open positions.{self.error.description}')
- return res
- return res
-
- async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
- return await asyncio.to_thread(self._history_orders_total, date_from, date_to)
-
- 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)
- res = await asyncio.to_thread(self._history_orders_get, *args, **kwargs)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in getting orders.{self.error.description}')
- return res
- return res
-
- async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
- return await asyncio.to_thread(self._history_deals_total, date_from, date_to)
-
- 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)
- res = await asyncio.to_thread(self._history_deals_get, *args, **kwargs)
- if res is None:
- err = await self.last_error()
- self.error = Error(*err)
- logger.warning(f'Error in getting deals.{self.error}')
- return res
- return res
diff --git a/src/aiomql/core/base.py b/src/aiomql/core/base.py
index 333fc39..bfa8b90 100644
--- a/src/aiomql/core/base.py
+++ b/src/aiomql/core/base.py
@@ -4,7 +4,7 @@ from logging import getLogger
from .config import Config
from .meta_trader import MetaTrader
-
+from ..contrib.backtester import MetaTester
logger = getLogger(__name__)
@@ -22,7 +22,7 @@ class Base:
**kwargs: Set instance attributes with keyword arguments. Only if they are annotated on the class body.
"""
self.config = Config()
- self.mt5 = MetaTrader()
+ self.mt5 = MetaTrader() if self.config.mode == 'live' else MetaTester()
self.exclude = {'mt5', "config", 'exclude', 'include', 'annotations', 'class_vars', 'dict'}
self.include = set()
self.set_attributes(**kwargs)
diff --git a/src/aiomql/core/config.py b/src/aiomql/core/config.py
index 5e4d2dd..4983cfd 100644
--- a/src/aiomql/core/config.py
+++ b/src/aiomql/core/config.py
@@ -8,6 +8,7 @@ from .task_queue import TaskQueue
logger = getLogger(__name__)
Bot = TypeVar("Bot")
+TestData = TypeVar("TestData")
class Config:
@@ -44,16 +45,20 @@ class Config:
record_trades: bool
records_dir: Path
records_dir_name: str
+ compress_test_data: bool
test_data_dir: Path
test_data_dir_name: str
task_queue: TaskQueue
+ _test_data: TestData
bot: Bot
_instance: 'Config'
mode: Literal['backtest', 'live']
use_terminal_for_backtesting: bool
+ test_data_file: str
_defaults = {"timeout": 60000, "record_trades": True, "trade_record_mode": "csv", "mode": "live",
'filename': "aiomql.json", "records_dir_name": "trade_records", "test_data_dir_name": "test_data",
- "use_terminal_for_backtesting": True, 'path': '', 'login': 0, 'password': '', 'server': ''}
+ "use_terminal_for_backtesting": True, 'path': '', 'login': 0, 'password': '', 'server': '',
+ "compress_test_data": False, 'test_data_file': ''}
def __new__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
@@ -61,12 +66,21 @@ class Config:
cls._instance.state = {}
cls._instance.task_queue = TaskQueue()
cls._instance.set_attributes(**cls._defaults)
+ cls._instance._test_data = None
cls._instance.load_config(**kwargs)
return cls._instance
def __init__(self, **kwargs):
self.set_attributes(**kwargs)
+ @property
+ def test_data(self):
+ return self._test_data
+
+ @test_data.setter
+ def test_data(self, value: TestData):
+ self._test_data = value
+
def set_attributes(self, **kwargs):
"""Set keyword arguments as object attributes
diff --git a/src/aiomql/core/errors.py b/src/aiomql/core/errors.py
index dd17ecd..d6564b7 100644
--- a/src/aiomql/core/errors.py
+++ b/src/aiomql/core/errors.py
@@ -24,7 +24,7 @@ class Error:
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/models.py b/src/aiomql/core/models.py
index 13226e7..6ac1172 100644
--- a/src/aiomql/core/models.py
+++ b/src/aiomql/core/models.py
@@ -334,9 +334,9 @@ class SymbolInfo(Base):
path: str
def __init__(self, **kwargs):
- if (name := kwargs.pop('name', None)) is None:
+ if (name := kwargs.pop('name', '')) == '':
raise AttributeError('Symbol Object Must be initialized with a name')
- self.name = name # type: str
+ self.name = name
super().__init__(**kwargs)
def __repr__(self):
@@ -351,28 +351,6 @@ class SymbolInfo(Base):
def __hash__(self):
return hash(self.name)
-class TickInfo(Base):
- """Price Tick of a Financial Instrument.
-
- Attributes:
- time (int): Time of the last prices update for the symbol
- bid (float): Current Bid price
- ask (float): Current Ask price
- last (float): Price of the last deal (Last)
- volume (float): Volume for the current Last price
- time_msc (int): Time of the last prices update for the symbol in milliseconds
- flags (TickFlag): Tick flags
- 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
- last: float
- volume: float
- time_msc: float
- flags: TickFlag
- volume_real: float
class BookInfo(Base):
"""Book Information Class.
diff --git a/src/aiomql/lib/__init__.py b/src/aiomql/lib/__init__.py
index 130b456..34162fa 100644
--- a/src/aiomql/lib/__init__.py
+++ b/src/aiomql/lib/__init__.py
@@ -1,3 +1,4 @@
from .strategies import *
from .traders import *
from .symbols import *
+from .candle_patterns import *
diff --git a/src/aiomql/lib/candle_patterns/__init__.py b/src/aiomql/lib/candle_patterns/__init__.py
new file mode 100644
index 0000000..a0e421e
--- /dev/null
+++ b/src/aiomql/lib/candle_patterns/__init__.py
@@ -0,0 +1 @@
+from .fractals import *
diff --git a/src/aiomql/lib/candle_patterns/fractals.py b/src/aiomql/lib/candle_patterns/fractals.py
new file mode 100644
index 0000000..fbd001a
--- /dev/null
+++ b/src/aiomql/lib/candle_patterns/fractals.py
@@ -0,0 +1,13 @@
+from ...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):
+ 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):
+ return candles[i]
diff --git a/src/aiomql/lib/strategies/__init__.py b/src/aiomql/lib/strategies/__init__.py
index 625296d..952d071 100644
--- a/src/aiomql/lib/strategies/__init__.py
+++ b/src/aiomql/lib/strategies/__init__.py
@@ -1,2 +1,3 @@
from .finger_trap import FingerTrap
from .tracker import Tracker
+from .finger_trap_back_test import FingerTrapTest
diff --git a/src/aiomql/lib/strategies/finger_trap.py b/src/aiomql/lib/strategies/finger_trap.py
index 0445628..38f43ef 100644
--- a/src/aiomql/lib/strategies/finger_trap.py
+++ b/src/aiomql/lib/strategies/finger_trap.py
@@ -9,7 +9,7 @@ from ...candle import Candles
from ...strategy import Strategy
from ...core import TimeFrame, OrderType
from ...sessions import Sessions
-from ...utils import find_bearish_fractal, find_bullish_fractal
+from ..candle_patterns import find_bearish_fractal, find_bullish_fractal
logger = logging.getLogger(__name__)
@@ -36,6 +36,7 @@ class FingerTrap(Strategy):
async def check_trend(self):
try:
+
candles: Candles = await self.symbol.copy_rates_from_pos(timeframe=self.ttf, count=self.tcc)
if not ((current := candles[-1].time) >= self.tracker.trend_time):
self.tracker.update(new=False, order_type=None)
@@ -106,6 +107,7 @@ class FingerTrap(Strategy):
await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters,
sl=self.tracker.sl)
await self.sleep(self.tracker.snooze)
+
except Exception as err:
logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade")
await self.sleep(self.ttf.time)
diff --git a/src/aiomql/lib/strategies/finger_trap_back_test.py b/src/aiomql/lib/strategies/finger_trap_back_test.py
new file mode 100644
index 0000000..16ea87b
--- /dev/null
+++ b/src/aiomql/lib/strategies/finger_trap_back_test.py
@@ -0,0 +1,30 @@
+from .finger_trap import FingerTrap
+from ...contrib.backtester.test_strategy import TestStrategy
+
+
+class FingerTrapTest(TestStrategy, FingerTrap):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.set_up()
+
+ async def test(self):
+ print(f"Backtesting {self.symbol}")
+ while True:
+ await self.event_manager.acquire()
+ try:
+ await self.event_manager.wait()
+ await self.watch_market()
+
+ if not self.tracker.new:
+ continue
+
+ if self.tracker.order_type is not None:
+ await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters,
+ sl=self.tracker.sl)
+ await self.sleep(self.tracker.snooze)
+ except Exception as err:
+ print(f"{err} For {self.symbol} in {self.__class__.__name__}.trade")
+ await self.sleep(self.ttf.time)
+
+ finally:
+ self.event_manager.release()
diff --git a/src/aiomql/utils.py b/src/aiomql/utils.py
index f1d2201..5a61931 100644
--- a/src/aiomql/utils.py
+++ b/src/aiomql/utils.py
@@ -1,11 +1,11 @@
"""Utility functions for aiomql."""
-
import decimal
import random
from functools import wraps, partial
import asyncio
+from logging import getLogger
-from .candle import Candles, Candle
+logger = getLogger(__name__)
def dict_to_string(data: dict, multi=False) -> str:
@@ -22,25 +22,6 @@ def dict_to_string(data: dict, multi=False) -> str:
return f"{sep}".join(f"{key}: {value}" for key, value in data.items())
-def round_off(value: float, step: float, round_down: bool = False) -> float:
- """Round off a number to the nearest step."""
- with decimal.localcontext() as ctx:
- ctx.rounding = decimal.ROUND_DOWN if round_down else decimal.ROUND_UP
- return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step))))
-
-
-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):
- 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):
- return candles[i]
-
-
def backoff_decorator(func=None, *, max_retries: int = 3, retries: int = 0, delay: int = 1, error=None) -> callable:
if func is None:
return partial(backoff_decorator, max_retries=max_retries, retries=retries, delay=delay, error=error)
@@ -64,8 +45,44 @@ def backoff_decorator(func=None, *, max_retries: int = 3, retries: int = 0, dela
return wrapper
+def error_handler(func=None, *, msg='', exe = Exception):
+ if func is None:
+ return partial(error_handler, msg=msg, exe=exe)
+
+ @wraps(func)
+ async def wrapper(*args, **kwargs):
+ try:
+ res = await func(*args, **kwargs)
+ return res
+ except exe as err:
+ logger.error(f'Error in {func.__name__}: {msg or err}')
+
+ return wrapper
+
+def error_handler_sync(func=None, *, msg='', exe=Exception):
+ if func is None:
+ return partial(error_handler, msg=msg, exe=exe)
+
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ try:
+ res = func(*args, **kwargs)
+ return res
+ except exe as err:
+ logger.error(f'Error in {func.__name__}: {msg or err}')
+
+ return wrapper
+
def round_down(value: int, base: int) -> int:
return value if value % base == 0 else value - (value % base)
+
def round_up(value: int, base: int) -> int:
return value if value % base == 0 else value + base - (value % base)
+
+
+def round_off(value: float, step: float, round_down: bool = False) -> float:
+ """Round off a number to the nearest step."""
+ with decimal.localcontext() as ctx:
+ ctx.rounding = decimal.ROUND_DOWN if round_down else decimal.ROUND_UP
+ return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step))))