This commit is contained in:
Ichinga Samuel
2024-09-02 05:59:00 +01:00
parent 63fccf8e06
commit f9f40be1da
6 changed files with 420 additions and 216 deletions
+161 -74
View File
@@ -2,16 +2,11 @@
"cells": [ "cells": [
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 1, "execution_count": 2,
"id": "f4500c8d-0e58-4d3f-8dd3-06a4896f397f", "id": "f4500c8d-0e58-4d3f-8dd3-06a4896f397f",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# import shelve\n",
"# import pickle\n",
"# import zlib\n",
"# import lzma\n",
"# import pytz\n",
"from datetime import datetime, timedelta\n", "from datetime import datetime, timedelta\n",
"from aiomql import MetaTrader, TimeFrame, AccountInfo, TimeFrame, CopyTicks, Account, Symbol\n", "from aiomql import MetaTrader, TimeFrame, AccountInfo, TimeFrame, CopyTicks, Account, Symbol\n",
"from MetaTrader5 import SymbolInfo\n", "from MetaTrader5 import SymbolInfo\n",
@@ -22,7 +17,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 2, "execution_count": 3,
"id": "f2ef126c-6edc-4651-8a81-06bb97eed6f9", "id": "f2ef126c-6edc-4651-8a81-06bb97eed6f9",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -41,21 +36,108 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 3, "execution_count": 21,
"id": "2c598a85-1e90-49b0-abf1-87329597feae", "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": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"True" "(410, 7710.71, 0.01, 553.9999999999964)"
] ]
}, },
"execution_count": 3, "execution_count": 22,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "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": [ "source": [
"sym = Symbol(name='Volatility 25 Index')\n", "sym = Symbol(name='Volatility 25 Index')\n",
"await sym.init()" "await sym.init()"
@@ -63,42 +145,20 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 13, "execution_count": null,
"id": "0a7d319c-5760-4058-994f-27e8d10df108", "id": "0a7d319c-5760-4058-994f-27e8d10df108",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [],
{
"data": {
"text/plain": [
"0.5"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [ "source": [
"await sym.mt5.order_calc_margin(0, 'Volatility 25 Index', 1, sym.tick.ask)" "await sym.mt5.order_calc_margin(0, 'Volatility 25 Index', 1, sym.tick.ask)"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 17, "execution_count": null,
"id": "4ec498ea-5e4e-4ff6-85da-051acc2eaf28", "id": "4ec498ea-5e4e-4ff6-85da-051acc2eaf28",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [],
{
"data": {
"text/plain": [
"4"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [ "source": [
"sy = await sym.mt5.symbol_info('Volatility 25 Index')\n", "sy = await sym.mt5.symbol_info('Volatility 25 Index')\n",
"await sym.mt5.symbol_select('Volatility 25 Index', enable=True)\n", "await sym.mt5.symbol_select('Volatility 25 Index', enable=True)\n",
@@ -107,21 +167,10 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 12, "execution_count": null,
"id": "9307e3df-a5f3-42cf-9d3d-e65e5254c0e5", "id": "9307e3df-a5f3-42cf-9d3d-e65e5254c0e5",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [],
{
"data": {
"text/plain": [
"0.50097375"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [ "source": [
"# margin\n", "# margin\n",
"# worked for forex.\n", "# worked for forex.\n",
@@ -432,26 +481,75 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 70,
"id": "e47bd638-42fa-40d9-b573-474a39884dbc", "id": "e47bd638-42fa-40d9-b573-474a39884dbc",
"metadata": {}, "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": [ "source": [
"n = datetime.now()\n", "await symb.copy_rates_from_pos(timeframe=TimeFrame.M1)\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)"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 72,
"id": "a4bc7b28-6dac-4863-b257-64d2c5c62575", "id": "a4bc7b28-6dac-4863-b257-64d2c5c62575",
"metadata": {}, "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": [ "source": [
"res = pd.DataFrame(res)\n", "tz = pytz.timezone('Etc/UTC')\n",
"len(res.index)" "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", "cell_type": "code",
"execution_count": 2, "execution_count": null,
"id": "fe60fc41-8844-4e00-b254-bb95d28528f2", "id": "fe60fc41-8844-4e00-b254-bb95d28528f2",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [],
{
"data": {
"text/plain": [
"1.2"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [ "source": [
"6 / (0 or 5)" "6 / (0 or 5)"
] ]
@@ -635,7 +722,7 @@
"name": "python", "name": "python",
"nbconvert_exporter": "python", "nbconvert_exporter": "python",
"pygments_lexer": "ipython3", "pygments_lexer": "ipython3",
"version": "3.11.4" "version": "3.11.6"
} }
}, },
"nbformat": 4, "nbformat": 4,
+2 -4
View File
@@ -29,10 +29,8 @@ class Account(AccountInfo):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
acc = self.config.account_info() self.exclude = self.exclude | {'_instance', 'symbols'}
acc_details = {k: v for k, v in self.get_dict(include={'login', 'server', 'password'}).items() if v} acc = {k: (self.dict[k] or v) for k, v in self.config.account_info().items()}
acc |= acc_details
self.config.set_attributes(**acc)
self.set_attributes(**acc) self.set_attributes(**acc)
async def refresh(self): async def refresh(self):
+21 -19
View File
@@ -1,21 +1,23 @@
from collections import namedtuple import socket
class ITR:
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']) class socketserver:
gen = Gender(man='Manny', woman='Babe') def __init__(self, address = '192.168.1.15', port = 9090):
gend = gen._asdict() self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
genz = Gender(gend) self.address = address
print(gen, genz) self.port = port
# b = ITR() self.sock.bind((self.address, self.port))
# print(next(b)) self.cummdata = ''
# print(next(b))
# print(next(b))
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()
+23 -17
View File
@@ -1,4 +1,4 @@
from typing import TypedDict from dataclasses import dataclass
import pickle import pickle
import lzma import lzma
from datetime import datetime from datetime import datetime
@@ -16,24 +16,36 @@ from ...core.constants import TimeFrame, CopyTicks
from ...utils import backoff_decorator from ...utils import backoff_decorator
logger = getLogger(__name__) 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 account: dict
symbols: dict[str, dict] symbols: dict[str, dict]
prices: dict[str, DataFrame] prices: dict[str, DataFrame]
ticks: dict[str, DataFrame] ticks: dict[str, DataFrame]
rates: dict[str, 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: class GetData:
config: Config = Config()
def __init__(self, *, start: datetime, end: datetime, timeframes: set[TimeFrame], symbols: set[str], def __init__(self, *, start: datetime, end: datetime, timeframes: set[TimeFrame], symbols: set[str],
name: str = '', tz: str = 'Etc/UTC'): name: str = '', tz: str = 'Etc/UTC'):
"""""" """"""
super().__init__() self.config = Config()
self.tz = pytz.timezone(tz) self.tz = pytz.timezone(tz)
self.start = start.replace(tzinfo=self.tz) self.start = start.replace(tzinfo=self.tz)
self.end = end.replace(tzinfo=self.tz) self.end = end.replace(tzinfo=self.tz)
@@ -41,24 +53,18 @@ class GetData:
self.timeframes = timeframes self.timeframes = timeframes
self.name = name or f"{start:%d-%m-%y}_{end:%d-%m-%y}" self.name = name or f"{start:%d-%m-%y}_{end:%d-%m-%y}"
diff = int((self.end - self.start).total_seconds()) 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() self.mt5 = MetaTrader()
async def get_data(self) -> Data: async def get_data(self) -> Data:
"""""" """"""
data = {}
rates, ticks, prices, symbols, account = await asyncio.gather(self.get_symbols_rates(), self.get_symbols_ticks(), 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_symbols_prices(), self.get_symbols_info(),
self.get_account_info()) self.get_account_info())
return Data(account=account, symbols=symbols, prices=prices, ticks=ticks, rates=rates,
data['rates'] = rates span=self.span, range=self.range)
data['ticks'] = ticks
data['prices'] = prices
data['symbols'] = symbols
data['account'] = account
data['range'] = self.interval
return Data(**data)
async def pickle_data(self) -> None: async def pickle_data(self) -> None:
"""""" """"""
@@ -145,7 +151,7 @@ class GetData:
res = pd.DataFrame(res) res = pd.DataFrame(res)
res.drop_duplicates(subset=['time'], keep='last', inplace=True) res.drop_duplicates(subset=['time'], keep='last', inplace=True)
res.set_index('time', inplace=True, drop=False) 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 return symbol, res
@backoff_decorator(max_retries=5) @backoff_decorator(max_retries=5)
+146 -26
View File
@@ -1,19 +1,22 @@
from datetime import datetime, tzinfo from collections import namedtuple
from datetime import datetime
from typing import Literal from typing import Literal
from itertools import zip_longest
import random
import pytz import pytz
import numpy as np import numpy as np
import pandas as pd
from pandas import DataFrame from pandas import DataFrame
from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TradeOrder, TradePosition, TradeDeal, from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TradeOrder, TradePosition, TradeDeal,
ORDER_TYPE_BUY, ORDER_TYPE_SELL, TradeRequest) ORDER_TYPE_BUY, ORDER_TYPE_SELL, TradeRequest, OrderCheckResult, OrderSendResult,
import MetaTrader5 as mt5 ACCOUNT_STOPOUT_MODE_PERCENT)
from ..meta_trader import MetaTrader from ..meta_trader import MetaTrader
from ..constants import TimeFrame, CopyTicks, OrderType from ..constants import TimeFrame, CopyTicks, OrderType, TradeAction
from .get_data import Data, GetData from .get_data import Data
from ...utils import round_down, round_up from ...utils import round_down, round_up
tz = pytz.timezone('Etc/UTC') tz = pytz.timezone('Etc/UTC')
Cursor = namedtuple('Cursor', ['index', 'time'])
class TestData: class TestData:
@@ -22,27 +25,33 @@ class TestData:
def __init__(self, data: Data): def __init__(self, data: Data):
self._data = data self._data = data
self.account = AccountInfo(**data['account']) self.account = AccountInfo(**data.account)
self.symbols = {symbol: SymbolInfo(**info) for symbol, info in data['symbols'].items()} self.symbols = {symbol: SymbolInfo(**info) for symbol, info in data.symbols.items()}
self.prices = data['prices'] self.prices = data.prices
self.ticks = data['ticks'] self.ticks = data.ticks
self.rates = data['rates'] self.rates = data.rates
self.interval = data['interval'] self.span = data.span
self.cursor = 0 self.range = data.range
self.iter = iter(self.interval) 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.orders: dict[str, dict[int, TradeOrder]] = {}
self.open_orders: dict[int, TradeOrder] = {} self.open_orders: dict[int, TradeOrder] = {}
self.positions: dict[str, dict[int, TradePosition]] = {} self.positions: dict[str, dict[int, TradePosition]] = {}
self.open_positions: 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): def __next__(self):
self.cursor = next(self.iter) index, time = next(self.iter)
self.cursor = Cursor(index=index, time=time)
return self.cursor return self.cursor
def reset(self): def reset(self):
self.iter = iter(self.interval) self.iter = zip_longest(self.range, self.span)
return self.iter self.cursor = Cursor(index=self.range[0], time=self.span[0])
return self.cursor
def get_symbols_total(self) -> int: def get_symbols_total(self) -> int:
return len(self.symbols) return len(self.symbols)
@@ -54,7 +63,7 @@ class TestData:
return AccountInfo(**self.account._asdict()) return AccountInfo(**self.account._asdict())
def get_symbol_info_tick(self, symbol: str) -> Tick: 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) return Tick(**tick)
def get_symbol_info(self, symbol: str) -> SymbolInfo: def get_symbol_info(self, symbol: str) -> SymbolInfo:
@@ -108,26 +117,137 @@ class TestData:
async def order_calc_margin(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, async def order_calc_margin(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float,
price: float, use_terminal=False): price: float, use_terminal=False):
if use_terminal or self.mt.config.use_terminal: if use_terminal and self.mt5.config.use_terminal_for_backtesting:
return await self.mt.order_calc_margin(OrderType(action), symbol, volume, price) return await self.mt5.order_calc_margin(OrderType(action), symbol, volume, price)
sym = self.symbols[symbol] sym = self.symbols[symbol]
margin = (volume * sym.trade_contract_size * price) / (self.account.leverage / (sym.margin_initial or 1)) margin = (volume * sym.trade_contract_size * price) / (self.account.leverage / (sym.margin_initial or 1))
return margin return margin
async def order_calc_profit(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, 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): price_open: float, price_close: float, use_terminal=True):
if use_terminal or self.mt.config.use_terminal: if use_terminal and self.mt5.config.use_terminal_for_backtesting:
return await self.mt.order_calc_profit(action, symbol, volume, price_open, price_close) return await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close)
sym = self.symbols[symbol] sym = self.symbols[symbol]
profit = volume * sym.trade_contract_size * (price_close - price_open) profit = volume * sym.trade_contract_size * (price_close - price_open)
return profit 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: def get_orders_total(self) -> int:
return len(self.open_orders) return len(self.open_orders)
+67 -76
View File
@@ -23,54 +23,49 @@ class Config:
server (str): Broker server server (str): Broker server
path (str): Path to terminal file path (str): Path to terminal file
timeout (int): Timeout for terminal connection timeout (int): Timeout for terminal connection
_initialize (bool): First time initialization flag
state (dict): A global state dictionary for storing data across the framework 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: Notes:
By default, the config class looks for a file named aiomql.json. 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 You can change this by passing the filename and/or the config_dir keyword argument(s) to the constructor
or the load_config method. or the load_config method.
By passing reload=True to the load_config method, you can reload and search again for the config file. By passing reload=True to the load_config method, you can reload and search again for the config file.
""" """
login: int = 0 login: int
trade_record_mode: Literal['csv', 'json'] = 'csv' trade_record_mode: Literal['csv', 'json']
password: str = "" password: str
server: str = "" server: str
path: str | Path = "" path: str | Path
timeout: int = 60000 timeout: int
record_trades: bool = True filename: str
filename: str = "aiomql.json" state: dict
_initialize = True
state: dict = {}
root: Path root: Path
root_dir: Path record_trades: bool
records_dir: Path records_dir: Path
config_dir: str = '' records_dir_name: str
task_queue: TaskQueue = TaskQueue() test_data_dir: Path
bot: Bot = None test_data_dir_name: str
task_queue: TaskQueue
bot: Bot
_instance: 'Config' _instance: 'Config'
mode: Literal['backtest', 'live'] = 'live' mode: Literal['backtest', 'live']
test_data_dir: str = 'test_data' use_terminal_for_backtesting: bool
use_terminal: bool = False _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): def __new__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"): if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls) 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 return cls._instance
def __init__(self, **kwargs): def __init__(self, **kwargs):
reload = kwargs.pop('reload', False) self.set_attributes(**kwargs)
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)
def set_attributes(self, **kwargs): def set_attributes(self, **kwargs):
"""Set keyword arguments as object attributes """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)) parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
last_dir, current_dir = current_dir, parent_dir last_dir, current_dir = current_dir, parent_dir
def find_config(self): def find_config_file(self):
try: try:
path = self.root_dir / self.config_dir for dirname in self.walk_to_root(self.root):
for dirname in self.walk_to_root(path):
check_path = os.path.join(dirname, self.filename) check_path = os.path.join(dirname, self.filename)
if os.path.isfile(check_path): if os.path.isfile(check_path):
return check_path return check_path
@@ -106,54 +100,51 @@ class Config:
except Exception as _: except Exception as _:
return return
def create_records_dir(self, *, records_dir: str | Path = 'records'): def load_config(self, *, file: str | Path = None, filename: str = None, root: str | Path = None, **kwargs):
"""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):
"""Load configuration settings from a file. """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") logger.warning("No Config File Found")
file_config = {}
else: else:
fh = open(file, mode="r") fh = open(file, mode="r")
data = json.load(fh) file_config = json.load(fh)
fh.close() fh.close()
data |= kwargs
data = file_config | kwargs
self.set_attributes(**data) 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]: def account_info(self) -> dict[str, int | str]:
"""Returns Account login details as found in the config object if available """Returns Account login details as found in the config object if available
@@ -161,4 +152,4 @@ class Config:
Returns: Returns:
dict: A dictionary of login details 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}