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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
012symbols
0101020A
1201122A
2301224A
3401326A
4501428A
\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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
012symbols
0101020A
1201122A
2301224A
3401326A
4501428A
5601530D
6701632D
7801734D
8901836D
91001938D
\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))))