mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-19 14:58:07 +00:00
minor bug fix
This commit is contained in:
@@ -11,6 +11,7 @@ from .core.constants import TimeFrame
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Candle:
|
||||
"""A class representing bars from the MetaTrader 5 terminal as a customized class analogous to Japanese Candlesticks.
|
||||
You can subclass this class for added customization.
|
||||
@@ -45,6 +46,7 @@ class Candle:
|
||||
self.time = kwargs.pop('time', 0)
|
||||
self.Index = kwargs.pop('Index', 0)
|
||||
self.set_attributes(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
keys = reprlib.repr(', '.join('%s=%s' % (i, j) for i, j in self.__dict__.items()))[1:-1]
|
||||
return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': keys}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .strategies import *
|
||||
from .traders import *
|
||||
from .symbols import *
|
||||
@@ -0,0 +1 @@
|
||||
from .finger_trap import FingerTrap
|
||||
@@ -0,0 +1,210 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Literal
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ...symbol import Symbol
|
||||
from ...trader import Trader
|
||||
from ...candle import Candles
|
||||
from ...strategy import Strategy
|
||||
from ...core import TimeFrame, OrderType
|
||||
from ...sessions import Sessions
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Entry:
|
||||
"""
|
||||
Entry class for FingerTrap strategy. Will be used to store entry conditions and other entry related data.
|
||||
|
||||
Attributes:
|
||||
bearish (bool): True if the market is bearish
|
||||
bullish (bool): True if the market is bullish
|
||||
ranging (bool): True if the market is ranging
|
||||
snooze (float): Time to wait before checking for entry conditions
|
||||
trend (str): The current trend of the market
|
||||
new (bool): True if the last candle is new
|
||||
order_type (OrderType): The type of order to place
|
||||
"""
|
||||
|
||||
bearish: bool = False
|
||||
bullish: bool = False
|
||||
ranging: bool = True
|
||||
trending: bool = False
|
||||
trend: Literal["ranging", "bullish", "bearish"] = "ranging"
|
||||
snooze: float = 0
|
||||
last_trend_time: float = 0
|
||||
last_entry_time: float = 0
|
||||
new: bool = True
|
||||
order_type: OrderType | None = None
|
||||
|
||||
def update(self, **kwargs):
|
||||
fields = self.__dict__
|
||||
for key in kwargs:
|
||||
if key in fields:
|
||||
setattr(self, key, kwargs[key])
|
||||
match self.trend:
|
||||
case "ranging":
|
||||
self.ranging = True
|
||||
self.trending = self.bullish = self.bearish = False
|
||||
case "bullish":
|
||||
self.ranging = self.bearish = False
|
||||
self.bullish = self.trending = True
|
||||
case "bearish":
|
||||
self.ranging = self.bullish = False
|
||||
self.bearish = self.trending = True
|
||||
|
||||
|
||||
class FingerTrap(Strategy):
|
||||
trend_time_frame: TimeFrame
|
||||
entry_time_frame: TimeFrame
|
||||
trend: int
|
||||
fast_period: int
|
||||
slow_period: int
|
||||
entry_period: int
|
||||
parameters: dict
|
||||
prices: Candles
|
||||
name = "FingerTrap"
|
||||
interval: TimeFrame
|
||||
entry_candles_count: int
|
||||
trend_candles_count: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
symbol: Symbol,
|
||||
params: dict | None = None,
|
||||
trader: Trader = None,
|
||||
sessions: Sessions = None,
|
||||
):
|
||||
super().__init__(symbol=symbol, params=params, sessions=sessions)
|
||||
self.trend = self.parameters.get("trend", 3)
|
||||
self.fast_period = self.parameters.setdefault("fast_period", 8)
|
||||
self.slow_period = self.parameters.setdefault("slow_period", 34)
|
||||
self.entry_time_frame = self.parameters.setdefault(
|
||||
"entry_time_frame", TimeFrame.M5
|
||||
)
|
||||
self.trend_time_frame = self.parameters.setdefault(
|
||||
"trend_time_frame", TimeFrame.H1
|
||||
)
|
||||
self.trader = trader or Trader(symbol=self.symbol)
|
||||
self.entry: Entry = Entry(snooze=self.trend_time_frame.time)
|
||||
self.entry_period = self.parameters.setdefault("entry_period", 8)
|
||||
|
||||
self.trend_candles_count = self.parameters.setdefault(
|
||||
"trend_candles_count", 86400 // self.trend_time_frame.time
|
||||
)
|
||||
self.trend_candles_count = max(self.trend_candles_count, self.slow_period)
|
||||
self.entry_candles_count = self.trend_candles_count * (
|
||||
self.trend_time_frame.time // self.entry_time_frame.time
|
||||
)
|
||||
self.entry_candles_count = max(self.entry_candles_count, self.entry_period)
|
||||
|
||||
async def check_trend(self):
|
||||
try:
|
||||
candles = await self.symbol.copy_rates_from_pos(
|
||||
timeframe=self.trend_time_frame, count=self.trend_candles_count
|
||||
)
|
||||
current = candles[-1]
|
||||
if current.time > self.entry.last_trend_time:
|
||||
self.entry.update(new=True, last_trend_time=current.time)
|
||||
else:
|
||||
self.entry.update(new=False)
|
||||
return
|
||||
|
||||
candles.ta.ema(length=self.slow_period, append=True, fillna=0)
|
||||
candles.ta.ema(length=self.fast_period, append=True, fillna=0)
|
||||
candles.rename(
|
||||
inplace=True,
|
||||
**{
|
||||
f"EMA_{self.fast_period}": "fast",
|
||||
f"EMA_{self.slow_period}": "slow",
|
||||
},
|
||||
)
|
||||
|
||||
# Compute
|
||||
candles["fast_A_slow"] = candles.ta_lib.above(candles.fast, candles.slow)
|
||||
candles["fast_B_slow"] = candles.ta_lib.below(candles.fast, candles.slow)
|
||||
candles["close_A_fast"] = candles.ta_lib.above(candles.close, candles.fast)
|
||||
candles["close_B_fast"] = candles.ta_lib.below(candles.close, candles.fast)
|
||||
|
||||
trend = candles[-self.trend : -1]
|
||||
if all(
|
||||
(c.is_bullish() and c.fast_A_slow and c.close_A_fast) for c in trend
|
||||
):
|
||||
self.entry.update(trend="bullish")
|
||||
|
||||
elif all(
|
||||
c.is_bearish() and c.fast_B_slow and c.close_B_fast for c in trend
|
||||
):
|
||||
self.entry.update(trend="bearish")
|
||||
|
||||
else:
|
||||
self.entry.update(trend="ranging", snooze=self.trend_time_frame.time)
|
||||
except Exception as exe:
|
||||
logger.error(f"{exe}. Error in {self.__class__.__name__}.check_trend")
|
||||
|
||||
async def confirm_trend(self):
|
||||
try:
|
||||
candles = await self.symbol.copy_rates_from_pos(
|
||||
timeframe=self.entry_time_frame, count=self.entry_candles_count
|
||||
)
|
||||
current = candles[-1]
|
||||
if current.time > self.entry.last_entry_time:
|
||||
self.entry.update(new=True, last_entry_time=current.time)
|
||||
else:
|
||||
self.entry.update(new=False)
|
||||
return
|
||||
|
||||
candles.ta.ema(length=self.entry_period, append=True, fillna=0)
|
||||
candles.rename(**{f"EMA_{self.entry_period}": "ema"})
|
||||
candles["close_A_ema"] = candles.ta_lib.above(candles.close, candles.ema)
|
||||
candles["close_B_ema"] = candles.ta_lib.below(candles.close, candles.ema)
|
||||
candles["close_XA_ema"] = candles.ta_lib.cross(candles.close, candles.ema)
|
||||
candles["close_XB_ema"] = candles.ta_lib.cross(
|
||||
candles.close, candles.ema, above=False
|
||||
)
|
||||
if self.entry.bullish and current.close_XA_ema:
|
||||
self.entry.update(
|
||||
snooze=self.entry_time_frame.time, order_type=OrderType.BUY
|
||||
)
|
||||
elif self.entry.bearish and current.close_XB_ema:
|
||||
self.entry.update(
|
||||
snooze=self.entry_time_frame.time, order_type=OrderType.SELL
|
||||
)
|
||||
else:
|
||||
self.entry.update(snooze=self.entry_time_frame.time, order_type=None)
|
||||
except Exception as exe:
|
||||
logger.error(f"{exe} Error in {self.__class__.__name__}.confirm_trend")
|
||||
|
||||
async def watch_market(self):
|
||||
await self.check_trend()
|
||||
if not self.entry.ranging:
|
||||
await self.confirm_trend()
|
||||
|
||||
async def trade(self):
|
||||
logger.info(f"Trading {self.symbol}")
|
||||
async with self.sessions as sess:
|
||||
while True:
|
||||
await sess.check()
|
||||
try:
|
||||
await self.watch_market()
|
||||
if not self.entry.new:
|
||||
await asyncio.sleep(2)
|
||||
continue
|
||||
if self.entry.order_type is None:
|
||||
await self.sleep(self.entry.snooze)
|
||||
continue
|
||||
|
||||
await self.trader.place_trade(
|
||||
order_type=self.entry.order_type, params=self.parameters
|
||||
)
|
||||
await self.sleep(self.entry.snooze)
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"Error: {err}\t Symbol: {self.symbol} in {self.__class__.__name__}.trade"
|
||||
)
|
||||
await self.sleep(self.trend_time_frame.time)
|
||||
continue
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .forex_symbol import ForexSymbol
|
||||
@@ -0,0 +1,30 @@
|
||||
from ...symbol import Symbol
|
||||
from ...core.exceptions import VolumeError
|
||||
|
||||
|
||||
class CryptoSymbol(Symbol):
|
||||
"""Subclass of Symbol for Crypto/Fiat Symbols. Handles the computation of volume based on the amount to risk."""
|
||||
|
||||
async def compute_volume(self, *, amount: float, points, use_limits=False) -> float:
|
||||
"""Compute volume given an amount to risk and target pips. Round the computed volume to the nearest step.
|
||||
|
||||
Args:
|
||||
amount (float): Amount to risk. Given in terms of the account currency.
|
||||
points (float): Target pips.
|
||||
use_limits (bool): If True, the computed volume checked against the maximum and minimum volume.
|
||||
|
||||
Returns:
|
||||
float: volume
|
||||
|
||||
Raises:
|
||||
VolumeError: If the computed volume is less than the minimum volume or greater than the maximum volume.
|
||||
"""
|
||||
if self.currency_profit != self.account.currency:
|
||||
amount = await self.convert_currency(amount=amount, base=self.currency_profit, quote=self.account.currency)
|
||||
volume = amount / (self.point * points * self.trade_contract_size)
|
||||
volume = self.round_off_volume(volume)
|
||||
if self.check_volume(volume)[0]:
|
||||
return volume
|
||||
if use_limits:
|
||||
return self.check_volume(volume)[1]
|
||||
raise VolumeError(f'Incorrect Volume. Computed Volume outside the range of permitted volumes')
|
||||
@@ -0,0 +1,32 @@
|
||||
from ...symbol import Symbol
|
||||
from ...core.exceptions import VolumeError
|
||||
|
||||
|
||||
class ForexSymbol(Symbol):
|
||||
"""Subclass of Symbol for Forex Symbols. Handles the conversion of currency and the computation of stop loss,
|
||||
take profit and volume.
|
||||
"""
|
||||
|
||||
async def compute_volume(self, *, amount: float, pips, use_limits=False) -> float:
|
||||
"""Compute volume given an amount to risk and target pips. Round the computed volume to the nearest step.
|
||||
|
||||
Args:
|
||||
amount (float): Amount to risk. Given in terms of the account currency.
|
||||
pips (float): Target pips.
|
||||
use_limits (bool): If True, the computed volume checked against the maximum and minimum volume.
|
||||
|
||||
Returns:
|
||||
float: volume
|
||||
|
||||
Raises:
|
||||
VolumeError: If the computed volume is less than the minimum volume or greater than the maximum volume.
|
||||
"""
|
||||
if self.currency_profit != self.account.currency:
|
||||
amount = await self.convert_currency(amount=amount, base=self.currency_profit, quote=self.account.currency)
|
||||
volume = amount / (self.pip * pips * self.trade_contract_size)
|
||||
volume = self.round_off_volume(volume)
|
||||
if self.check_volume(volume)[0]:
|
||||
return volume
|
||||
if use_limits:
|
||||
return self.check_volume(volume)[1]
|
||||
raise VolumeError(f'Incorrect Volume. Computed Volume outside the range of permitted volumes')
|
||||
+3
-25
@@ -1,6 +1,5 @@
|
||||
"""Risk Assessment and Management"""
|
||||
from .account import Account
|
||||
from .symbol import Symbol
|
||||
|
||||
|
||||
class RAM:
|
||||
@@ -8,24 +7,20 @@ class RAM:
|
||||
risk_to_reward: float
|
||||
risk: float
|
||||
amount: float
|
||||
pips: float
|
||||
volume: float
|
||||
|
||||
def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, amount: float = 0, pips: float = 0, volume=0):
|
||||
def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, amount: float = 0, **kwargs):
|
||||
"""Initialize Risk Assessment and Management with the provided keyword arguments.
|
||||
|
||||
Keyword Args:
|
||||
risk_to_reward (float): Risk to reward ratio. Defaults to 1
|
||||
risk (float): Percentage of account balance to risk per trade 0.01 # 1%
|
||||
amount (float): Amount to risk per trade in terms of account currency 0
|
||||
pips (float): Target pips to risk
|
||||
volume (float): Volume to trade 0
|
||||
kwargs: extra keyword arguments are set as object attributes
|
||||
"""
|
||||
self.risk_to_reward = risk_to_reward
|
||||
self.risk = risk
|
||||
self.amount = amount
|
||||
self.pips = pips
|
||||
self.volume = volume
|
||||
[setattr(self, key, value) for key, value in kwargs.items()]
|
||||
|
||||
async def get_amount(self, risk: float = 0) -> float:
|
||||
"""Calculate the amount to risk per trade as a percentage of equity.
|
||||
@@ -39,20 +34,3 @@ class RAM:
|
||||
await self.account.refresh()
|
||||
risk = risk or self.risk
|
||||
return self.account.equity * risk
|
||||
|
||||
async def get_volume(self, *, symbol: Symbol, pips: float = 0, amount: float = 0) -> float:
|
||||
"""Calculate the volume to trade. if pips is not provided, the pips attribute is used.
|
||||
If the amount attribute or amount argument is zero, the amount is calculated using the get_amount method based
|
||||
on the risk.
|
||||
|
||||
Keyword Args:
|
||||
symbol (Symbol): Financial instrument
|
||||
pips (float): Target pips. Defaults to zero.
|
||||
amount (float): Amount to risk per trade. Defaults to zero.
|
||||
|
||||
Returns:
|
||||
float: Volume to trade
|
||||
"""
|
||||
pips = pips or self.pips
|
||||
amount = amount or self.amount or await self.get_amount()
|
||||
return await symbol.compute_volume(amount=amount, pips=pips)
|
||||
|
||||
@@ -19,12 +19,6 @@ def delta(obj: time):
|
||||
return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond)
|
||||
|
||||
|
||||
def seconds(start: time, end: time) -> set[int]:
|
||||
if start > end:
|
||||
return set(range(delta(start).seconds, 86400)) | set(range(0, delta(end).seconds))
|
||||
return set(range(delta(start).seconds, delta(end).seconds))
|
||||
|
||||
|
||||
class Session:
|
||||
"""A session is a time period between two datetime.time objects specified in utc.
|
||||
|
||||
@@ -36,7 +30,6 @@ class Session:
|
||||
custom_start (Callable): A custom function to call when the session starts. Default is None.
|
||||
custom_end (Callable): A custom function to call when the session ends. Default is None.
|
||||
name (str): A name for the session. Default is a combination of start and end.
|
||||
seconds (set[int]): A set of seconds between start and end.
|
||||
|
||||
Methods:
|
||||
begin: Call the action specified in on_start or custom_start.
|
||||
@@ -69,10 +62,13 @@ class Session:
|
||||
self.custom_start = custom_start
|
||||
self.custom_end = custom_end
|
||||
self.name = name or f'{self.start} - {self.end}'
|
||||
self.seconds = seconds(self.start, self.end)
|
||||
|
||||
def __contains__(self, item: time):
|
||||
return delta(item).seconds in self.seconds
|
||||
if self.start > self.end:
|
||||
m1 = time(hour=23, minute=59, second=59, microsecond=9999)
|
||||
m2 = time(hour=0)
|
||||
return self.start <= item <= m1 or m2 <= item < self.end
|
||||
return self.start <= item < self.end
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.start}-->{self.name}-->{self.end}' if self.name else f'{self.start}-->{self.end}'
|
||||
|
||||
+41
-19
@@ -79,7 +79,7 @@ class Symbol(SymbolInfo):
|
||||
Raises:
|
||||
ValueError: If request was unsuccessful and None was returned
|
||||
"""
|
||||
|
||||
|
||||
info = await self.mt5.symbol_info(self.name)
|
||||
if info:
|
||||
self.set_attributes(**info._asdict())
|
||||
@@ -96,6 +96,7 @@ class Symbol(SymbolInfo):
|
||||
if await self.symbol_select():
|
||||
await self.book_add()
|
||||
await self.info()
|
||||
await self.info_tick()
|
||||
return True
|
||||
logger.warning(f'Unable to initialized symbol {self}')
|
||||
return False
|
||||
@@ -137,6 +138,15 @@ class Symbol(SymbolInfo):
|
||||
return await self.mt5.market_book_release(self.name)
|
||||
|
||||
def check_volume(self, volume) -> tuple[bool, float]:
|
||||
"""Check if the volume is within the limits of the symbol. If not, return the nearest limit.
|
||||
|
||||
Args:
|
||||
volume (float): Volume to check
|
||||
|
||||
Returns: tuple[bool, float]: Returns a tuple of a boolean and a float. The boolean indicates if the volume is
|
||||
within the limits of the symbol. The float is the volume to use if the volume is not within the limits of the
|
||||
symbol.
|
||||
"""
|
||||
check = self.volume_min <= volume <= self.volume_max
|
||||
if check:
|
||||
return check, volume
|
||||
@@ -145,25 +155,36 @@ class Symbol(SymbolInfo):
|
||||
else:
|
||||
return check, self.volume_max
|
||||
|
||||
def round_off_volume(self, volume):
|
||||
def round_off_volume(self, volume) -> float:
|
||||
"""Round off the volume to the nearest volume step.
|
||||
|
||||
Args:
|
||||
volume (float): Volume to round off
|
||||
|
||||
Returns:
|
||||
float: Rounded off volume
|
||||
"""
|
||||
step = ceil(abs(log10(self.volume_step)))
|
||||
return round(volume, step)
|
||||
|
||||
async def compute_volume(self, *, amount: float, pips: float, use_limits: bool = False) -> float:
|
||||
"""Computes the volume of a trade based on the amount and the number of pips to target.
|
||||
async def compute_volume(self, *args, **kwargs) -> float:
|
||||
"""Computes the volume required for a trade usually based on the amount and any other keyword arguments.
|
||||
This is a dummy method that returns the minimum volume of the symbol. It is meant to be overridden by a subclass
|
||||
that implements the computation of volume.
|
||||
|
||||
Args:
|
||||
amount (float): Amount to risk in the trade
|
||||
pips (float): Number of pips to target
|
||||
use_limits (bool): If True, the computed volume is rounded to the nearest step and checked against
|
||||
Keyword Args:
|
||||
use_limits (bool): round up or round down the computed volume to the nearest volume limit i.e volume_min
|
||||
or volume_max
|
||||
|
||||
Returns:
|
||||
float: Returns the volume of the trade
|
||||
"""
|
||||
return self.volume_min
|
||||
|
||||
async def convert_currency(self, *, amount: float, base: str, quote: str) -> float:
|
||||
"""Convert from one currency to the other. Alias for currency_conversion"""
|
||||
return await self.currency_conversion(amount=amount, base=base, quote=quote)
|
||||
|
||||
async def currency_conversion(self, *, amount: float, base: str, quote: str) -> float:
|
||||
"""Convert from one currency to the other.
|
||||
|
||||
@@ -173,7 +194,7 @@ class Symbol(SymbolInfo):
|
||||
quote: The quote currency of the pair
|
||||
|
||||
Returns:
|
||||
float: Amount in terms of the base currency or None if it failed to convert
|
||||
float: Amount in terms of the base currency
|
||||
|
||||
Raises:
|
||||
ValueError: If conversion is impossible
|
||||
@@ -189,8 +210,7 @@ class Symbol(SymbolInfo):
|
||||
if self.account.has_symbol(pair):
|
||||
tick = await self.info_tick(name=pair)
|
||||
if tick is not None:
|
||||
amount = amount * tick.bid
|
||||
return amount
|
||||
return amount * tick.bid
|
||||
except Exception as err:
|
||||
logger.warning(f'Currency conversion failed: Unable to convert {amount} in {quote} to {base}')
|
||||
raise ValueError(f'Currency Conversion Failed: {err}')
|
||||
@@ -201,11 +221,11 @@ class Symbol(SymbolInfo):
|
||||
"""
|
||||
Get bars from the MetaTrader 5 terminal starting from the specified date.
|
||||
|
||||
Args:
|
||||
timeframe (TimeFrame): Timeframe the bars are requested for. Set by a value from the TimeFrame enumeration. Required unnamed parameter.
|
||||
Args: timeframe (TimeFrame): Timeframe the bars are requested for. Set by a value from the TimeFrame
|
||||
enumeration. Required unnamed parameter.
|
||||
|
||||
date_from (datetime | int): Date of opening of the first bar from the requested sample. Set by the 'datetime' object or as a number
|
||||
of seconds elapsed since 1970.01.01. Required unnamed parameter.
|
||||
date_from (datetime | int): Date of opening of the first bar from the requested sample. Set by the
|
||||
'datetime' object or as a number of seconds elapsed since 1970.01.01. Required unnamed parameter.
|
||||
|
||||
count (int): Number of bars to receive. Required unnamed parameter.
|
||||
|
||||
@@ -220,7 +240,7 @@ class Symbol(SymbolInfo):
|
||||
return Candles(data=rates)
|
||||
raise ValueError(f'Could not get rates for {self.name}')
|
||||
|
||||
async def copy_rates_from_pos(self, *,timeframe: TimeFrame, count: int = 500, start_position: int = 0) -> Candles:
|
||||
async def copy_rates_from_pos(self, *, timeframe: TimeFrame, count: int = 500, start_position: int = 0) -> Candles:
|
||||
"""Get bars from the MetaTrader 5 terminal starting from the specified index.
|
||||
|
||||
Args:
|
||||
@@ -267,7 +287,8 @@ class Symbol(SymbolInfo):
|
||||
return Candles(data=rates)
|
||||
raise ValueError(f'Could not get rates for {self.name}')
|
||||
|
||||
async def copy_ticks_from(self, *, date_from: datetime | int, count: int = 100, flags: CopyTicks = CopyTicks.ALL) -> Ticks:
|
||||
async def copy_ticks_from(self, *, date_from: datetime | int, count: int = 100,
|
||||
flags: CopyTicks = CopyTicks.ALL) -> Ticks:
|
||||
"""
|
||||
Get ticks from the MetaTrader 5 terminal starting from the specified date.
|
||||
|
||||
@@ -289,7 +310,8 @@ class Symbol(SymbolInfo):
|
||||
return Ticks(data=ticks)
|
||||
raise ValueError(f'Could not get ticks for {self.name}')
|
||||
|
||||
async def copy_ticks_range(self, *, date_from: datetime | int, date_to: datetime | int, flags: CopyTicks = CopyTicks.ALL) -> Ticks:
|
||||
async def copy_ticks_range(self, *, date_from: datetime | int, date_to: datetime | int,
|
||||
flags: CopyTicks = CopyTicks.ALL) -> Ticks:
|
||||
"""Get ticks for the specified date range from the MetaTrader 5 terminal.
|
||||
|
||||
Args:
|
||||
@@ -298,7 +320,7 @@ class Symbol(SymbolInfo):
|
||||
|
||||
date_to: Date, up to which the bars are requested. Set by the 'datetime' object or as a number of seconds elapsed since 1970.01.01. Bars
|
||||
with the open time <= date_to are returned. Required unnamed parameter.
|
||||
|
||||
|
||||
flags (CopyTicks):
|
||||
|
||||
Returns:
|
||||
|
||||
+63
-40
@@ -8,7 +8,7 @@ from zoneinfo import ZoneInfo
|
||||
from .order import Order
|
||||
from .symbol import Symbol as _Symbol
|
||||
from .ram import RAM
|
||||
from .core.models import OrderType
|
||||
from .core.models import OrderType, OrderSendResult
|
||||
from .core.config import Config
|
||||
from .utils import dict_to_string
|
||||
from .result import Result
|
||||
@@ -43,34 +43,43 @@ class Trader:
|
||||
self.symbol = symbol
|
||||
self.order = Order(symbol=symbol.name)
|
||||
self.ram = ram or RAM()
|
||||
self.params = {}
|
||||
|
||||
async def create_order(self, *, order_type: OrderType, **kwargs):
|
||||
"""Complete the order object with the required values. Creates a simple order.
|
||||
Uses the ram instance to set the volume.
|
||||
|
||||
Args:
|
||||
order_type (OrderType): Type of order
|
||||
kwargs: keyword arguments as required for the specific trader
|
||||
"""
|
||||
# check if pips is passed in as a keyword argument, if not use the pips attribute of the ram instance
|
||||
pips = kwargs.get('pips', 0) or self.ram.pips
|
||||
self.order.volume = kwargs.get('volume', self.ram.volume) or await self.ram.get_volume(symbol=self.symbol,
|
||||
pips=pips)
|
||||
points = kwargs.get('points', self.symbol.trade_stops_level+self.symbol.spread)
|
||||
self.order.volume = await self.symbol.compute_volume()
|
||||
self.order.type = order_type
|
||||
await self.set_order_limits(pips=pips)
|
||||
await self.set_trade_stop_levels(points=points)
|
||||
|
||||
async def set_order_limits(self, pips: float):
|
||||
"""Sets the stop loss and take profit for the order.
|
||||
This method uses pips as defined for forex instruments.
|
||||
"""Sets the stop loss and take profit for the order. This method uses pips as defined for forex instruments.
|
||||
|
||||
Args:
|
||||
pips: Target pips
|
||||
"""
|
||||
# use passed in pips and the pip value of the symbol to calculate the stop loss and take profit.
|
||||
# this is sure to work for forex instruments.
|
||||
pips = pips * self.symbol.pip
|
||||
sl, tp = pips, pips * self.ram.risk_to_reward
|
||||
tick = await self.symbol.info_tick()
|
||||
if self.order.type == OrderType.BUY:
|
||||
self.order.sl, self.order.tp = tick.ask - sl, tick.ask + tp
|
||||
self.order.price = tick.ask
|
||||
elif self.order.type == OrderType.SELL:
|
||||
self.order.sl, self.order.tp = tick.bid + sl, tick.bid - tp
|
||||
self.order.price = tick.bid
|
||||
else:
|
||||
raise ValueError(f"Invalid order type: {self.order.type}")
|
||||
|
||||
async def set_trade_stop_levels(self, *, points):
|
||||
"""Set the stop loss and take profit levels of the order based on the points."""
|
||||
points = points * self.symbol.point
|
||||
sl, tp = points, points * self.ram.risk_to_reward
|
||||
tick = await self.symbol.info_tick()
|
||||
if self.order.type == OrderType.BUY:
|
||||
self.order.sl, self.order.tp = tick.ask - sl, tick.ask + tp
|
||||
self.order.price = tick.ask
|
||||
@@ -78,6 +87,46 @@ class Trader:
|
||||
self.order.sl, self.order.tp = tick.bid + sl, tick.bid - tp
|
||||
self.order.price = tick.bid
|
||||
|
||||
async def check_order(self) -> bool:
|
||||
"""Check order before sending it to the broker.
|
||||
|
||||
Returns:
|
||||
bool: True if order can go through else false
|
||||
"""
|
||||
check = await self.order.check()
|
||||
if check.retcode != 0:
|
||||
logger.warning(
|
||||
f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(check.get_dict(include={'comment', 'retcode'}), multi=True)}")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def send_order(self):
|
||||
result = await self.order.send()
|
||||
if result.retcode != 10009:
|
||||
logger.warning(
|
||||
f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(result.get_dict(include={'comment', 'retcode'}), multi=True)}")
|
||||
return
|
||||
logger.info(f"Symbol: {self.order.symbol}\nOrder: {dict_to_string(result.dict, multi=True)}\n")
|
||||
await self.record_trade(result)
|
||||
|
||||
async def record_trade(self, result: OrderSendResult):
|
||||
"""
|
||||
Record the trade in a csv file.
|
||||
Args:
|
||||
result (OrderSendResult): Result of the order send
|
||||
"""
|
||||
if result.retcode != 10009 or not self.config.record_trades:
|
||||
return
|
||||
profit = await self.order.calc_profit()
|
||||
params = self.params
|
||||
params['expected_profit'] = profit
|
||||
date = datetime.utcnow()
|
||||
date = date.replace(tzinfo=ZoneInfo('UTC'))
|
||||
params['date'] = date
|
||||
params['time'] = date.timestamp()
|
||||
res = Result(result=result, parameters=params)
|
||||
await res.save_csv()
|
||||
|
||||
async def place_trade(self, order_type: OrderType, params: dict = None, **kwargs):
|
||||
"""Places a trade based on the order_type.
|
||||
|
||||
@@ -88,35 +137,9 @@ class Trader:
|
||||
"""
|
||||
try:
|
||||
await self.create_order(order_type=order_type, **kwargs)
|
||||
|
||||
# Check the order before placing it
|
||||
check = await self.order.check()
|
||||
if check.retcode != 0:
|
||||
logger.warning(
|
||||
f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(check.get_dict(include={'comment', 'retcode'}), multi=True)}")
|
||||
if not await self.check_order():
|
||||
return
|
||||
|
||||
# check expected profit
|
||||
profit = await self.order.calc_profit()
|
||||
|
||||
# Send the order.
|
||||
result = await self.order.send()
|
||||
if result.retcode != 10009:
|
||||
logger.warning(
|
||||
f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(result.get_dict(include={'comment', 'retcode'}), multi=True)}")
|
||||
return
|
||||
|
||||
logger.info(f"Symbol: {self.order.symbol}\nOrder: {dict_to_string(result.dict, multi=True)}\n")
|
||||
|
||||
# save trade result and passed in parameters
|
||||
if result.retcode == 10009 and self.config.record_trades:
|
||||
params = params or {}
|
||||
params['expected_profit'] = profit
|
||||
date = datetime.utcnow()
|
||||
date = date.replace(tzinfo=ZoneInfo('UTC'))
|
||||
params['date'] = date
|
||||
params['time'] = date.timestamp()
|
||||
res = Result(result=result, parameters=params)
|
||||
await res.save_csv()
|
||||
self.params |= params or {}
|
||||
await self.send_order()
|
||||
except Exception as err:
|
||||
logger.error(f"{err}. Symbol: {self.order.symbol}\n {self.__class__.__name__}.place_trade")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Utility functions for aiomql."""
|
||||
|
||||
def dict_to_string(data: dict, multi=False) -> str:
|
||||
"""Convert a dict to a string. Use for logging.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user