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": [
{
"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,
+2 -4
View File
@@ -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):
+21 -19
View File
@@ -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()
+23 -17
View File
@@ -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)
+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 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
View File
@@ -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}