mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-07-27 20:27:43 +00:00
testdata
This commit is contained in:
+161
-74
@@ -2,16 +2,11 @@
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": 2,
|
||||
"id": "f4500c8d-0e58-4d3f-8dd3-06a4896f397f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# import shelve\n",
|
||||
"# import pickle\n",
|
||||
"# import zlib\n",
|
||||
"# import lzma\n",
|
||||
"# import pytz\n",
|
||||
"from datetime import datetime, timedelta\n",
|
||||
"from aiomql import MetaTrader, TimeFrame, AccountInfo, TimeFrame, CopyTicks, Account, Symbol\n",
|
||||
"from MetaTrader5 import SymbolInfo\n",
|
||||
@@ -22,7 +17,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": 3,
|
||||
"id": "f2ef126c-6edc-4651-8a81-06bb97eed6f9",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -41,21 +36,108 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "2c598a85-1e90-49b0-abf1-87329597feae",
|
||||
"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": [
|
||||
"True"
|
||||
"(410, 7710.71, 0.01, 553.9999999999964)"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"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()"
|
||||
@@ -63,42 +145,20 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"execution_count": null,
|
||||
"id": "0a7d319c-5760-4058-994f-27e8d10df108",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"0.5"
|
||||
]
|
||||
},
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"await sym.mt5.order_calc_margin(0, 'Volatility 25 Index', 1, sym.tick.ask)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"execution_count": null,
|
||||
"id": "4ec498ea-5e4e-4ff6-85da-051acc2eaf28",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"4"
|
||||
]
|
||||
},
|
||||
"execution_count": 17,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"sy = await sym.mt5.symbol_info('Volatility 25 Index')\n",
|
||||
"await sym.mt5.symbol_select('Volatility 25 Index', enable=True)\n",
|
||||
@@ -107,21 +167,10 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"execution_count": null,
|
||||
"id": "9307e3df-a5f3-42cf-9d3d-e65e5254c0e5",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"0.50097375"
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# margin\n",
|
||||
"# worked for forex.\n",
|
||||
@@ -432,26 +481,75 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 70,
|
||||
"id": "e47bd638-42fa-40d9-b573-474a39884dbc",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"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": [
|
||||
"n = datetime.now()\n",
|
||||
"start = n.replace(hour=0, day=1, year=2020, month=1)\n",
|
||||
"end = n.replace(hour=15)\n",
|
||||
"res = await mt.copy_rates_range('Volatility 25 Index', TimeFrame.M1, start, end)"
|
||||
"await symb.copy_rates_from_pos(timeframe=TimeFrame.M1)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 72,
|
||||
"id": "a4bc7b28-6dac-4863-b257-64d2c5c62575",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"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": [
|
||||
"res = pd.DataFrame(res)\n",
|
||||
"len(res.index)"
|
||||
"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)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -591,21 +689,10 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "fe60fc41-8844-4e00-b254-bb95d28528f2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1.2"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"6 / (0 or 5)"
|
||||
]
|
||||
@@ -635,7 +722,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
"version": "3.11.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -29,10 +29,8 @@ class Account(AccountInfo):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
acc = self.config.account_info()
|
||||
acc_details = {k: v for k, v in self.get_dict(include={'login', 'server', 'password'}).items() if v}
|
||||
acc |= acc_details
|
||||
self.config.set_attributes(**acc)
|
||||
self.exclude = self.exclude | {'_instance', 'symbols'}
|
||||
acc = {k: (self.dict[k] or v) for k, v in self.config.account_info().items()}
|
||||
self.set_attributes(**acc)
|
||||
|
||||
async def refresh(self):
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
from collections import namedtuple
|
||||
class ITR:
|
||||
import socket
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.span = iter(range(0, 10))
|
||||
self.start = 0
|
||||
|
||||
def __next__(self):
|
||||
self.start = next(self.span)
|
||||
return self.start
|
||||
|
||||
|
||||
Gender = namedtuple('Gender', ['man', 'woman'])
|
||||
gen = Gender(man='Manny', woman='Babe')
|
||||
gend = gen._asdict()
|
||||
genz = Gender(gend)
|
||||
print(gen, genz)
|
||||
# b = ITR()
|
||||
# print(next(b))
|
||||
# print(next(b))
|
||||
# print(next(b))
|
||||
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()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import TypedDict
|
||||
from dataclasses import dataclass
|
||||
import pickle
|
||||
import lzma
|
||||
from datetime import datetime
|
||||
@@ -16,24 +16,36 @@ from ...core.constants import TimeFrame, CopyTicks
|
||||
from ...utils import backoff_decorator
|
||||
|
||||
logger = getLogger(__name__)
|
||||
from MetaTrader5 import TradePosition, TradeOrder, TradeDeal
|
||||
|
||||
tof = list(TradeOrder._fields)
|
||||
tof.append('symbol')
|
||||
tpf = list(TradePosition._fields)
|
||||
tpf.append('symbol')
|
||||
tdf = list(TradeDeal._fields)
|
||||
tdf.append('symbol')
|
||||
|
||||
|
||||
class Data(TypedDict):
|
||||
@dataclass
|
||||
class Data:
|
||||
account: dict
|
||||
symbols: dict[str, dict]
|
||||
prices: dict[str, DataFrame]
|
||||
ticks: dict[str, DataFrame]
|
||||
rates: dict[str, dict[str, DataFrame]]
|
||||
interval: range
|
||||
span: range
|
||||
range: range
|
||||
history_orders: DataFrame = DataFrame([], columns=tof)
|
||||
history_deals: DataFrame = DataFrame([], columns=tdf)
|
||||
positions: DataFrame = DataFrame([], columns=tpf)
|
||||
|
||||
|
||||
class GetData:
|
||||
config: Config = Config()
|
||||
|
||||
def __init__(self, *, start: datetime, end: datetime, timeframes: set[TimeFrame], symbols: set[str],
|
||||
name: str = '', tz: str = 'Etc/UTC'):
|
||||
""""""
|
||||
super().__init__()
|
||||
self.config = Config()
|
||||
self.tz = pytz.timezone(tz)
|
||||
self.start = start.replace(tzinfo=self.tz)
|
||||
self.end = end.replace(tzinfo=self.tz)
|
||||
@@ -41,24 +53,18 @@ class GetData:
|
||||
self.timeframes = timeframes
|
||||
self.name = name or f"{start:%d-%m-%y}_{end:%d-%m-%y}"
|
||||
diff = int((self.end - self.start).total_seconds())
|
||||
self.interval = range(start := int(self.start.timestamp()), diff + start)
|
||||
self.range = range(diff)
|
||||
self.span = range(start := int(self.start.timestamp()), diff + start)
|
||||
self.mt5 = MetaTrader()
|
||||
|
||||
async def get_data(self) -> Data:
|
||||
""""""
|
||||
data = {}
|
||||
|
||||
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())
|
||||
|
||||
data['rates'] = rates
|
||||
data['ticks'] = ticks
|
||||
data['prices'] = prices
|
||||
data['symbols'] = symbols
|
||||
data['account'] = account
|
||||
data['range'] = self.interval
|
||||
|
||||
return Data(**data)
|
||||
return Data(account=account, symbols=symbols, prices=prices, ticks=ticks, rates=rates,
|
||||
span=self.span, range=self.range)
|
||||
|
||||
async def pickle_data(self) -> None:
|
||||
""""""
|
||||
@@ -145,7 +151,7 @@ class GetData:
|
||||
res = pd.DataFrame(res)
|
||||
res.drop_duplicates(subset=['time'], keep='last', inplace=True)
|
||||
res.set_index('time', inplace=True, drop=False)
|
||||
res = res.reindex(self.interval, method='nearest')
|
||||
res = res.reindex(self.span, method='nearest')
|
||||
return symbol, res
|
||||
|
||||
@backoff_decorator(max_retries=5)
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
from datetime import datetime, tzinfo
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from itertools import zip_longest
|
||||
import random
|
||||
|
||||
import pytz
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TradeOrder, TradePosition, TradeDeal,
|
||||
ORDER_TYPE_BUY, ORDER_TYPE_SELL, TradeRequest)
|
||||
import MetaTrader5 as mt5
|
||||
ORDER_TYPE_BUY, ORDER_TYPE_SELL, TradeRequest, OrderCheckResult, OrderSendResult,
|
||||
ACCOUNT_STOPOUT_MODE_PERCENT)
|
||||
from ..meta_trader import MetaTrader
|
||||
from ..constants import TimeFrame, CopyTicks, OrderType
|
||||
from .get_data import Data, GetData
|
||||
from ..constants import TimeFrame, CopyTicks, OrderType, TradeAction
|
||||
from .get_data import Data
|
||||
from ...utils import round_down, round_up
|
||||
|
||||
tz = pytz.timezone('Etc/UTC')
|
||||
Cursor = namedtuple('Cursor', ['index', 'time'])
|
||||
|
||||
|
||||
class TestData:
|
||||
@@ -22,27 +25,33 @@ class TestData:
|
||||
|
||||
def __init__(self, data: Data):
|
||||
self._data = data
|
||||
self.account = AccountInfo(**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.interval = data['interval']
|
||||
self.cursor = 0
|
||||
self.iter = iter(self.interval)
|
||||
self.account = AccountInfo(**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.orders: dict[str, dict[int, TradeOrder]] = {}
|
||||
self.open_orders: dict[int, TradeOrder] = {}
|
||||
self.positions: dict[str, dict[int, TradePosition]] = {}
|
||||
self.open_positions: dict[int, TradePosition] = {}
|
||||
self.mt = MetaTrader()
|
||||
self.history_orders = data.history_orders
|
||||
self.history_deals = data.history_deals
|
||||
self.margins: dict[int, float] = {}
|
||||
self.mt5 = MetaTrader()
|
||||
|
||||
def __next__(self):
|
||||
self.cursor = next(self.iter)
|
||||
index, time = next(self.iter)
|
||||
self.cursor = Cursor(index=index, time=time)
|
||||
return self.cursor
|
||||
|
||||
def reset(self):
|
||||
self.iter = iter(self.interval)
|
||||
return self.iter
|
||||
self.iter = zip_longest(self.range, self.span)
|
||||
self.cursor = Cursor(index=self.range[0], time=self.span[0])
|
||||
return self.cursor
|
||||
|
||||
def get_symbols_total(self) -> int:
|
||||
return len(self.symbols)
|
||||
@@ -54,7 +63,7 @@ class TestData:
|
||||
return AccountInfo(**self.account._asdict())
|
||||
|
||||
def get_symbol_info_tick(self, symbol: str) -> Tick:
|
||||
tick = self.prices[symbol].iloc[self.cursor]
|
||||
tick = self.prices[symbol].iloc[self.cursor.index]
|
||||
return Tick(**tick)
|
||||
|
||||
def get_symbol_info(self, symbol: str) -> SymbolInfo:
|
||||
@@ -108,25 +117,136 @@ class TestData:
|
||||
|
||||
async def order_calc_margin(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float,
|
||||
price: float, use_terminal=False):
|
||||
if use_terminal or self.mt.config.use_terminal:
|
||||
return await self.mt.order_calc_margin(OrderType(action), symbol, volume, price)
|
||||
if use_terminal and self.mt5.config.use_terminal_for_backtesting:
|
||||
return await self.mt5.order_calc_margin(OrderType(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
|
||||
|
||||
async def order_calc_profit(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float,
|
||||
price_open: float, price_close: float, use_terminal=False):
|
||||
if use_terminal or self.mt.config.use_terminal:
|
||||
return await self.mt.order_calc_profit(action, symbol, volume, price_open, price_close)
|
||||
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
|
||||
|
||||
def order_send(self, request: dict) -> dict:
|
||||
def check_order(self, order: TradeOrder) -> bool:
|
||||
...
|
||||
|
||||
def order_check(self, request: dict) -> dict:
|
||||
def check_position(self, position: TradePosition) -> bool:
|
||||
...
|
||||
|
||||
def close_position(self, position: TradePosition):
|
||||
profit = position.profit
|
||||
self.open_orders.pop(position.ticket)
|
||||
self.open_positions.pop(position.ticket)
|
||||
margin = self.margins.pop(position.ticket)
|
||||
self.update_account(profit, margin=margin)
|
||||
|
||||
def modify_stops(self, ticket: int, sl: int = None, tp: int = None):
|
||||
...
|
||||
|
||||
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
|
||||
|
||||
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 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)
|
||||
|
||||
level = margin_level if acc.margin_mode == ACCOUNT_STOPOUT_MODE_PERCENT else free_margin
|
||||
if level < acc.margin_so_call or free_margin <= 0:
|
||||
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)
|
||||
|
||||
def get_orders_total(self) -> int:
|
||||
return len(self.open_orders)
|
||||
|
||||
+67
-76
@@ -23,54 +23,49 @@ class Config:
|
||||
server (str): Broker server
|
||||
path (str): Path to terminal file
|
||||
timeout (int): Timeout for terminal connection
|
||||
_initialize (bool): First time initialization flag
|
||||
state (dict): A global state dictionary for storing data across the framework
|
||||
root_dir (str): The root directory of the project
|
||||
root (str): Root directory of the project
|
||||
|
||||
Notes:
|
||||
By default, the config class looks for a file named aiomql.json.
|
||||
You can change this by passing the filename and/or the config_dir keyword argument(s) to the constructor
|
||||
or the load_config method.
|
||||
By passing reload=True to the load_config method, you can reload and search again for the config file.
|
||||
"""
|
||||
login: int = 0
|
||||
trade_record_mode: Literal['csv', 'json'] = 'csv'
|
||||
password: str = ""
|
||||
server: str = ""
|
||||
path: str | Path = ""
|
||||
timeout: int = 60000
|
||||
record_trades: bool = True
|
||||
filename: str = "aiomql.json"
|
||||
_initialize = True
|
||||
state: dict = {}
|
||||
login: int
|
||||
trade_record_mode: Literal['csv', 'json']
|
||||
password: str
|
||||
server: str
|
||||
path: str | Path
|
||||
timeout: int
|
||||
filename: str
|
||||
state: dict
|
||||
root: Path
|
||||
root_dir: Path
|
||||
record_trades: bool
|
||||
records_dir: Path
|
||||
config_dir: str = ''
|
||||
task_queue: TaskQueue = TaskQueue()
|
||||
bot: Bot = None
|
||||
records_dir_name: str
|
||||
test_data_dir: Path
|
||||
test_data_dir_name: str
|
||||
task_queue: TaskQueue
|
||||
bot: Bot
|
||||
_instance: 'Config'
|
||||
mode: Literal['backtest', 'live'] = 'live'
|
||||
test_data_dir: str = 'test_data'
|
||||
use_terminal: bool = False
|
||||
mode: Literal['backtest', 'live']
|
||||
use_terminal_for_backtesting: bool
|
||||
_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': ''}
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not hasattr(cls, "_instance"):
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance.state = {}
|
||||
cls._instance.task_queue = TaskQueue()
|
||||
cls._instance.set_attributes(**cls._defaults)
|
||||
cls._instance.load_config(**kwargs)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
reload = kwargs.pop('reload', False)
|
||||
self.load_config(reload=reload, **kwargs)
|
||||
|
||||
def set_root(self, *, root: str | Path):
|
||||
root = Path(root) if str else root
|
||||
self.root = root.absolute().resolve()
|
||||
self.root_dir = self.root
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if key == 'path':
|
||||
value = str(self.root_dir / Path(value).absolute().resolve())
|
||||
super().__setattr__(key, value)
|
||||
self.set_attributes(**kwargs)
|
||||
|
||||
def set_attributes(self, **kwargs):
|
||||
"""Set keyword arguments as object attributes
|
||||
@@ -95,10 +90,9 @@ class Config:
|
||||
parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
|
||||
last_dir, current_dir = current_dir, parent_dir
|
||||
|
||||
def find_config(self):
|
||||
def find_config_file(self):
|
||||
try:
|
||||
path = self.root_dir / self.config_dir
|
||||
for dirname in self.walk_to_root(path):
|
||||
for dirname in self.walk_to_root(self.root):
|
||||
check_path = os.path.join(dirname, self.filename)
|
||||
if os.path.isfile(check_path):
|
||||
return check_path
|
||||
@@ -106,54 +100,51 @@ class Config:
|
||||
except Exception as _:
|
||||
return
|
||||
|
||||
def create_records_dir(self, *, records_dir: str | Path = 'records'):
|
||||
"""Create records directory if it does not exist. By default, it is relative to the root directory of the
|
||||
project unless an absolute path is provided.
|
||||
|
||||
Keyword Args:
|
||||
records_dir (str|Path): The directory to save trade records. Default is 'trade_records'
|
||||
"""
|
||||
try:
|
||||
if isinstance(records_dir, str):
|
||||
records_dir = self.root_dir / records_dir
|
||||
elif isinstance(records_dir, Path):
|
||||
records_dir = records_dir.absolute().resolve()
|
||||
records_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.records_dir = records_dir
|
||||
except Exception as err:
|
||||
logger.warning(f"{err}: Unable to create records directory")
|
||||
|
||||
def load_config(self, *, file: str = None, reload: bool = True, filename: str = None,
|
||||
config_dir: str = '', **kwargs):
|
||||
def load_config(self, *, file: str | Path = None, filename: str = None, root: str | Path = None, **kwargs):
|
||||
"""Load configuration settings from a file.
|
||||
Keyword Args:
|
||||
file (str): The path to the file to load. If not provided, the file is searched for
|
||||
reload (bool): Whether to reload the config object. Default is True
|
||||
filename (str): The name of the file to load. If not provided, the default filename is used
|
||||
config_dir (str): The name of the directory to search for the file. Default is the root directory
|
||||
root_dir (str): The root directory of the project
|
||||
kwargs: Additional keyword arguments
|
||||
"""
|
||||
if not (self._initialize or reload):
|
||||
return
|
||||
data = {}
|
||||
self.filename = filename or self.filename
|
||||
self.config_dir = config_dir or self.config_dir
|
||||
root_dir = kwargs.pop('root_dir', None)
|
||||
records_dir = kwargs.pop('records_dir', 'records')
|
||||
if self._initialize or (root_dir is not None):
|
||||
self.set_root(root=(root_dir or '.'))
|
||||
self.create_records_dir(records_dir=records_dir)
|
||||
|
||||
if (file := (file or self.find_config())) is None:
|
||||
Keyword Args:
|
||||
file (str | Path): The absolute path to the config file.
|
||||
filename (str): The name of the file to load if file path is not specified. If not provided aiomql.json is used
|
||||
root (str): The root directory of the project.
|
||||
kwargs: Additional keyword arguments to set as object attributes.
|
||||
"""
|
||||
if root is not None:
|
||||
root = Path(root).resolve()
|
||||
root.mkdir(parents=True, exist_ok=True) if not root.exists() else ...
|
||||
self.root = root
|
||||
else:
|
||||
self.root = self.root if hasattr(self, 'root') else Path.cwd()
|
||||
|
||||
if file is not None:
|
||||
file = Path(file).resolve()
|
||||
if not file.exists():
|
||||
self.filename = filename or self.filename
|
||||
file = self.find_config_file()
|
||||
else:
|
||||
self.filename = file.name
|
||||
else:
|
||||
self.filename = filename or self.filename
|
||||
file = self.find_config_file()
|
||||
|
||||
if file is None:
|
||||
logger.warning("No Config File Found")
|
||||
file_config = {}
|
||||
else:
|
||||
fh = open(file, mode="r")
|
||||
data = json.load(fh)
|
||||
file_config = json.load(fh)
|
||||
fh.close()
|
||||
data |= kwargs
|
||||
|
||||
data = file_config | kwargs
|
||||
self.set_attributes(**data)
|
||||
self._initialize = False
|
||||
|
||||
if self.record_trades and not hasattr(self, "records_dir"):
|
||||
self.records_dir = self.root / self.records_dir_name
|
||||
self.records_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if self.mode == "backtest" and not hasattr(self, "test_data_dir"):
|
||||
self.test_data_dir = self.root / self.test_data_dir_name
|
||||
self.test_data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def account_info(self) -> dict[str, int | str]:
|
||||
"""Returns Account login details as found in the config object if available
|
||||
@@ -161,4 +152,4 @@ class Config:
|
||||
Returns:
|
||||
dict: A dictionary of login details
|
||||
"""
|
||||
return {"login": self.login, "password": self.password, "server": self.server}
|
||||
return {'login': self.login, 'password': self.password, 'server': self.server}
|
||||
|
||||
Reference in New Issue
Block a user