mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-22 16:28:08 +00:00
4.0.15dev
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "aiomql"
|
name = "aiomql"
|
||||||
version = "4.0.15b"
|
version = "4.0.15dev"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
|
|||||||
@@ -3,5 +3,4 @@ from .candle_patterns import *
|
|||||||
from .symbols import *
|
from .symbols import *
|
||||||
from .utils import *
|
from .utils import *
|
||||||
from .traders import *
|
from .traders import *
|
||||||
from .quants import *
|
|
||||||
from .trackers import *
|
from .trackers import *
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from aiomql import Candle, Candles
|
from aiomql import Candle, Candles
|
||||||
|
|
||||||
from ..quants.change import percentage_difference
|
from ...utils.change import percentage_difference
|
||||||
|
|
||||||
|
|
||||||
def is_bullish_fractal(candles: Candles) -> tuple[bool, Candle|None]:
|
def is_bullish_fractal(candles: Candles) -> tuple[bool, Candle|None]:
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
from .change import *
|
|
||||||
@@ -6,7 +6,7 @@ from ...lib import Symbol, Positions, Order
|
|||||||
from ...core.models import TradePosition, TradeAction, OrderSendResult
|
from ...core.models import TradePosition, TradeAction, OrderSendResult
|
||||||
from ...core.constants import OrderType
|
from ...core.constants import OrderType
|
||||||
from ...core.config import Config
|
from ...core.config import Config
|
||||||
from ..quants import percentage_increase, percentage_decrease
|
from ...utils.change import percentage_increase, percentage_decrease
|
||||||
from .position_tracker import PositionTracker
|
from .position_tracker import PositionTracker
|
||||||
|
|
||||||
logger = getLogger()
|
logger = getLogger()
|
||||||
@@ -54,26 +54,38 @@ class OpenPosition:
|
|||||||
await self.update_position()
|
await self.update_position()
|
||||||
if not self.is_open:
|
if not self.is_open:
|
||||||
self.remove_from_state()
|
self.remove_from_state()
|
||||||
|
await self.close_pending_order()
|
||||||
except Exception as exe:
|
except Exception as exe:
|
||||||
logger.error("%s: Unable to remove closed position from state", exe)
|
logger.error("%s: Unable to remove closed position from state", exe)
|
||||||
|
|
||||||
|
async def close_pending_order(self) -> tuple[bool, OrderSendResult | None]:
|
||||||
|
try:
|
||||||
|
if self.pending_hedge:
|
||||||
|
res = await Order.cancel_order(order=self.pending_hedge.order, symbol=self.symbol.name)
|
||||||
|
if res.retcode != 10009:
|
||||||
|
logger.critical("%s: Unable to cancel pending order", res.comment)
|
||||||
|
return False, res
|
||||||
|
self.pending_hedge = None
|
||||||
|
return True, res
|
||||||
|
except Exception as exe:
|
||||||
|
logger.error("%s: Unable to cancel pending order", exe)
|
||||||
|
return False, None
|
||||||
|
|
||||||
async def update_position(self) -> bool:
|
async def update_position(self) -> bool:
|
||||||
# ToDo: remove pending order
|
|
||||||
pos = await self.positions.get_position_by_ticket(ticket=self.ticket)
|
pos = await self.positions.get_position_by_ticket(ticket=self.ticket)
|
||||||
if pos is not None:
|
if pos is not None:
|
||||||
self.position = pos
|
self.position = pos
|
||||||
self.is_open = True
|
self.is_open = True
|
||||||
else:
|
else:
|
||||||
self.is_open = False
|
self.is_open = False
|
||||||
|
await self.close_pending_order()
|
||||||
return self.is_open
|
return self.is_open
|
||||||
|
|
||||||
async def modify_stops(self, *, sl: float = None, tp: float = None,
|
async def modify_stops(self, *, sl: float = None, tp: float = None,
|
||||||
use_stop_levels=False) -> tuple[bool, OrderSendResult | None]:
|
use_stop_levels=False) -> tuple[bool, OrderSendResult | None]:
|
||||||
try:
|
try:
|
||||||
# todo: add stops
|
|
||||||
tick = await self.symbol.info_tick()
|
tick = await self.symbol.info_tick()
|
||||||
|
|
||||||
# modify stop_loss
|
|
||||||
if sl is not None and use_stop_levels is True:
|
if sl is not None and use_stop_levels is True:
|
||||||
min_stops_value = (self.symbol.trade_stops_level + self.symbol.spread) * self.symbol.point
|
min_stops_value = (self.symbol.trade_stops_level + self.symbol.spread) * self.symbol.point
|
||||||
if self.position.type == OrderType.BUY:
|
if self.position.type == OrderType.BUY:
|
||||||
@@ -149,8 +161,8 @@ class OpenPosition:
|
|||||||
logger.error("%s: Error occurred in track method of Open Position for %d:%s",
|
logger.error("%s: Error occurred in track method of Open Position for %d:%s",
|
||||||
exe, self.symbol.name, self.ticket)
|
exe, self.symbol.name, self.ticket)
|
||||||
|
|
||||||
async def profit_to_price(self, profit):
|
async def profit_to_price(self, *, profit):
|
||||||
action = OrderType.BUY if self.position.type == 0 else OrderType.SELL
|
action = self.position.type
|
||||||
volume = self.position.volume
|
volume = self.position.volume
|
||||||
price_open = self.position.price_open
|
price_open = self.position.price_open
|
||||||
price_close = percentage_increase(price_open, 50) if action == 0 else percentage_decrease(price_open, 50)
|
price_close = percentage_increase(price_open, 50) if action == 0 else percentage_decrease(price_open, 50)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from logging import getLogger
|
|||||||
|
|
||||||
|
|
||||||
from .open_position import OpenPosition
|
from .open_position import OpenPosition
|
||||||
from ..quants import extend_interval_by_percentage, get_percentage_position, percentage_position
|
from ...utils.change import extend_interval_by_percentage, get_percentage_position, percentage_position
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ async def exit_at_checkpoint(pos: OpenPosition, /, start: float = 80, trail: flo
|
|||||||
if is_open is False:
|
if is_open is False:
|
||||||
return
|
return
|
||||||
position = pos.position
|
position = pos.position
|
||||||
if position.profit > 0 and percentage_position(position.price_open, position.tp, position.price_current) >= start:
|
if percentage_position(position.price_open, position.tp, position.price_current) >= start:
|
||||||
new_checkpoint = get_percentage_position(position.price_open, position.price_current, 100-trail)
|
new_checkpoint = get_percentage_position(position.price_open, position.price_current, 100-trail)
|
||||||
change_checkpoint = False
|
change_checkpoint = False
|
||||||
if position.type.long and new_checkpoint > (pos.checkpoint or position.price_open):
|
if position.type.long and new_checkpoint > (pos.checkpoint or position.price_open):
|
||||||
@@ -66,6 +66,7 @@ async def exit_at_checkpoint(pos: OpenPosition, /, start: float = 80, trail: flo
|
|||||||
if change_checkpoint:
|
if change_checkpoint:
|
||||||
pos.checkpoint = new_checkpoint
|
pos.checkpoint = new_checkpoint
|
||||||
pos.use_checkpoint = True
|
pos.use_checkpoint = True
|
||||||
|
await pos.modify_stops(sl=new_checkpoint, use_stop_levels=True)
|
||||||
logger.info("New checkpoint created for %s:%d at %f:%f",
|
logger.info("New checkpoint created for %s:%d at %f:%f",
|
||||||
position.symbol, position.ticket, new_checkpoint, position.profit)
|
position.symbol, position.ticket, new_checkpoint, position.profit)
|
||||||
close = False
|
close = False
|
||||||
|
|||||||
@@ -119,15 +119,13 @@ class Base:
|
|||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary of instance and class attributes
|
dict: A dictionary of instance and class attributes
|
||||||
"""
|
"""
|
||||||
try:
|
_filter = self.exclude.difference(self.include)
|
||||||
_filter = self.exclude.difference(self.include)
|
return {
|
||||||
return {
|
key: value
|
||||||
key: value
|
for key, value in (self.class_vars | self.__dict__).items()
|
||||||
for key, value in (self.class_vars | self.__dict__).items()
|
if key not in _filter and value is not None
|
||||||
if key not in _filter and value is not None
|
}
|
||||||
}
|
|
||||||
except Exception as err:
|
|
||||||
logger.warning(err)
|
|
||||||
|
|
||||||
|
|
||||||
class _Base(Base):
|
class _Base(Base):
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class Bot:
|
|||||||
logger.info("Login Successful")
|
logger.info("Login Successful")
|
||||||
await self.init_strategies()
|
await self.init_strategies()
|
||||||
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
||||||
self.add_coroutine(coroutine=self.executor.exit)
|
self.add_function(function=self.executor.exit)
|
||||||
|
|
||||||
if len(self.executor.strategy_runners) == 0:
|
if len(self.executor.strategy_runners) == 0:
|
||||||
logger.warning("No strategies were added to the bot. Exiting in one second")
|
logger.warning("No strategies were added to the bot. Exiting in one second")
|
||||||
@@ -112,7 +112,7 @@ class Bot:
|
|||||||
logger.info("Login Successful")
|
logger.info("Login Successful")
|
||||||
self.init_strategies_sync()
|
self.init_strategies_sync()
|
||||||
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
||||||
self.add_coroutine(coroutine=self.executor.exit)
|
self.add_function(function=self.executor.exit)
|
||||||
|
|
||||||
if len(self.executor.strategy_runners) == 0:
|
if len(self.executor.strategy_runners) == 0:
|
||||||
logger.warning("No strategies were added to the bot. Exiting in one second")
|
logger.warning("No strategies were added to the bot. Exiting in one second")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from signal import signal, SIGINT
|
from signal import signal, SIGINT
|
||||||
from typing import Coroutine, Callable
|
from typing import Coroutine, Callable
|
||||||
@@ -93,16 +94,16 @@ class Executor:
|
|||||||
def sigint_handle(self, signum, frame):
|
def sigint_handle(self, signum, frame):
|
||||||
self.config.shutdown = True
|
self.config.shutdown = True
|
||||||
|
|
||||||
async def exit(self):
|
def exit(self):
|
||||||
"""Shutdown the executor"""
|
"""Shutdown the executor"""
|
||||||
start = asyncio.get_event_loop().time()
|
start = time.time()
|
||||||
try:
|
try:
|
||||||
while self.config.shutdown is False and self.config.force_shutdown is False:
|
while self.config.shutdown is False and self.config.force_shutdown is False:
|
||||||
if self.timeout is not None and self.timeout < (asyncio.get_event_loop().time() - start):
|
if self.timeout is not None and self.timeout < (time.time() - start):
|
||||||
self.config.shutdown = True
|
self.config.shutdown = True
|
||||||
break
|
break
|
||||||
timeout = self.timeout or 1
|
timeout = self.timeout or 1
|
||||||
await asyncio.sleep(timeout)
|
time.sleep(timeout)
|
||||||
for strategy in self.strategy_runners:
|
for strategy in self.strategy_runners:
|
||||||
strategy.running = False
|
strategy.running = False
|
||||||
self.config.task_queue.cancel()
|
self.config.task_queue.cancel()
|
||||||
|
|||||||
@@ -32,12 +32,11 @@ class History:
|
|||||||
group: str
|
group: str
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
instance = super().__new__(cls)
|
if not hasattr(cls, 'config'):
|
||||||
if not hasattr(instance.__class__, 'config'):
|
cls.config = Config()
|
||||||
instance.__class__.config = Config()
|
if not hasattr(cls, 'mt5'):
|
||||||
if not hasattr(instance.__class__, 'mt5'):
|
cls.mt5 = MetaTrader() if cls.config.mode != "backtest" else MetaBackTester()
|
||||||
instance.__class__.mt5 = MetaTrader() if instance.config.mode != "backtest" else MetaBackTester()
|
return super().__new__(cls)
|
||||||
return instance
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, *, date_from: datetime | float, date_to: datetime | float, group: str = "", use_utc: bool = float
|
self, *, date_from: datetime | float, date_to: datetime | float, group: str = "", use_utc: bool = float
|
||||||
|
|||||||
+11
-2
@@ -1,10 +1,10 @@
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from ..core.models import TradeRequest, TradeOrder, OrderCheckResult, OrderSendResult
|
from ..core.models import TradeRequest, TradeOrder, OrderCheckResult, OrderSendResult
|
||||||
from ..core.constants import TradeAction, OrderTime, OrderFilling
|
from ..core.constants import TradeAction, OrderTime, OrderFilling, OrderType
|
||||||
from ..core.exceptions import OrderError
|
from ..core.exceptions import OrderError
|
||||||
from ..core.base import _Base
|
from ..core.base import _Base
|
||||||
from ..utils import error_handler
|
from ..utils import error_handler, percentage_decrease, percentage_increase
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
@@ -152,3 +152,12 @@ class Order(_Base, TradeRequest):
|
|||||||
def request(self) -> dict:
|
def request(self) -> dict:
|
||||||
"""Return the order request as a dictionary."""
|
"""Return the order request as a dictionary."""
|
||||||
return {key: value for key, value in self.dict.items() if key in self.mt5.TradeRequest.__match_args__}
|
return {key: value for key, value in self.dict.items() if key in self.mt5.TradeRequest.__match_args__}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def profit_to_price(cls, *, profit: float, order_type: OrderType, volume: float, symbol: str, price_open: float):
|
||||||
|
price_close = percentage_increase(price_open, 50) if order_type == 0 else percentage_decrease(price_open, 50)
|
||||||
|
half_profit = await cls.mt5.order_calc_profit(symbol=symbol, action=order_type, volume=volume,
|
||||||
|
price_open=price_open, price_close=price_close)
|
||||||
|
rate = profit / half_profit * 50
|
||||||
|
rate = percentage_increase(price_open, rate) if order_type == 0 else percentage_decrease(price_open, rate)
|
||||||
|
return rate
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class Positions:
|
|||||||
kwargs["group"] = group
|
kwargs["group"] = group
|
||||||
positions = await cls.mt5.positions_get(**kwargs)
|
positions = await cls.mt5.positions_get(**kwargs)
|
||||||
if positions is not None:
|
if positions is not None:
|
||||||
return cls.positions
|
return tuple(TradePosition(**pos._asdict()) for pos in positions)
|
||||||
logger.warning("Failed to get open positions")
|
logger.warning("Failed to get open positions")
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
|
|||||||
+36
-3
@@ -41,6 +41,20 @@ class RAM:
|
|||||||
"""
|
"""
|
||||||
[setattr(self, key, value) for key, value in kwargs.items()]
|
[setattr(self, key, value) for key, value in kwargs.items()]
|
||||||
|
|
||||||
|
def get_amount_sync(self) -> float:
|
||||||
|
"""Calculate the amount to risk per trade as a percentage of margin_free.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Amount to risk per trade
|
||||||
|
"""
|
||||||
|
if self.fixed_amount:
|
||||||
|
return self.fixed_amount
|
||||||
|
self.account.refresh_sync()
|
||||||
|
amount = self.account.margin_free * (self.risk / 100)
|
||||||
|
if self.min_amount and self.max_amount:
|
||||||
|
return max(self.min_amount, min(self.max_amount, amount))
|
||||||
|
return amount
|
||||||
|
|
||||||
async def get_amount(self) -> float:
|
async def get_amount(self) -> float:
|
||||||
"""Calculate the amount to risk per trade as a percentage of margin_free.
|
"""Calculate the amount to risk per trade as a percentage of margin_free.
|
||||||
|
|
||||||
@@ -61,8 +75,8 @@ class RAM:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True if the number of losing positions is less than or equal the loss limit
|
bool: True if the number of losing positions is less than or equal the loss limit
|
||||||
"""
|
"""
|
||||||
positions = await self.positions.get_positions()
|
positions = await self.account.mt5.positions_get()
|
||||||
loosing = [position for position in positions if position.profit < 0]
|
loosing = [position for position in positions if position.profit <= 0]
|
||||||
return len(loosing) <= self.loss_limit
|
return len(loosing) <= self.loss_limit
|
||||||
|
|
||||||
async def check_open_positions(self) -> bool:
|
async def check_open_positions(self) -> bool:
|
||||||
@@ -71,5 +85,24 @@ class RAM:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True if the number of open positions is less than the open limit
|
bool: True if the number of open positions is less than the open limit
|
||||||
"""
|
"""
|
||||||
positions = await self.positions.get_positions()
|
positions = await self.account.mt5.positions_get()
|
||||||
|
return len(positions) <= self.open_limit
|
||||||
|
|
||||||
|
def check_losing_positions_sync(self) -> bool:
|
||||||
|
"""Check if the number of losing positions is less than the loss limit
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the number of losing positions is less than or equal the loss limit
|
||||||
|
"""
|
||||||
|
positions = self.account.mt5._positions_get()
|
||||||
|
loosing = [position for position in positions if position.profit <= 0]
|
||||||
|
return len(loosing) <= self.loss_limit
|
||||||
|
|
||||||
|
def check_open_positions_sync(self) -> bool:
|
||||||
|
"""Check if the number of open positions is less than or equal the loss limit.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the number of open positions is less than the open limit
|
||||||
|
"""
|
||||||
|
positions = self.account.mt5._positions_get()
|
||||||
return len(positions) <= self.open_limit
|
return len(positions) <= self.open_limit
|
||||||
|
|||||||
@@ -61,15 +61,32 @@ class Result:
|
|||||||
with self.lock:
|
with self.lock:
|
||||||
trade_record_mode = trade_record_mode or self.config.trade_record_mode
|
trade_record_mode = trade_record_mode or self.config.trade_record_mode
|
||||||
if trade_record_mode == "csv":
|
if trade_record_mode == "csv":
|
||||||
await self.to_csv()
|
self.to_csv()
|
||||||
elif trade_record_mode == "json":
|
elif trade_record_mode == "json":
|
||||||
await self.to_json()
|
self.to_json()
|
||||||
elif trade_record_mode == "sql":
|
elif trade_record_mode == "sql":
|
||||||
self.to_sql()
|
self.to_sql()
|
||||||
else:
|
else:
|
||||||
logger.error(f"Invalid trade record mode: {trade_record_mode}")
|
logger.error(f"Invalid trade record mode: {trade_record_mode}")
|
||||||
|
|
||||||
async def to_csv(self):
|
def save_sync(self, *, trade_record_mode: Literal["csv", "json"] = None):
|
||||||
|
"""Record trade results as a csv or json file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
trade_record_mode (Literal['csv'|'json']): Mode of saving trade records
|
||||||
|
"""
|
||||||
|
with self.lock:
|
||||||
|
trade_record_mode = trade_record_mode or self.config.trade_record_mode
|
||||||
|
if trade_record_mode == "csv":
|
||||||
|
self.to_csv()
|
||||||
|
elif trade_record_mode == "json":
|
||||||
|
self.to_json()
|
||||||
|
elif trade_record_mode == "sql":
|
||||||
|
self.to_sql()
|
||||||
|
else:
|
||||||
|
logger.error(f"Invalid trade record mode: {trade_record_mode}")
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
"""Record trade results and associated parameters as a csv file"""
|
"""Record trade results and associated parameters as a csv file"""
|
||||||
try:
|
try:
|
||||||
data = self.get_data()
|
data = self.get_data()
|
||||||
@@ -99,7 +116,7 @@ class Result:
|
|||||||
logger.error("%s: Unable to serialize value", err)
|
logger.error("%s: Unable to serialize value", err)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def to_json(self):
|
def to_json(self):
|
||||||
"""Save trades and strategy parameters in a json file"""
|
"""Save trades and strategy parameters in a json file"""
|
||||||
try:
|
try:
|
||||||
file = self.config.records_dir / f"{self.name}.json"
|
file = self.config.records_dir / f"{self.name}.json"
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
from typing import ClassVar
|
||||||
|
from datetime import datetime, UTC
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from ...core.config import Config
|
||||||
|
from ...core.sync.meta_trader import MetaTrader
|
||||||
|
from ...core.models import TradeDeal, TradeOrder
|
||||||
|
from ...core.meta_backtester import MetaBackTester
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class History:
|
||||||
|
"""The history class handles completed trade deals and trade orders in the trading history of an account.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
deals (list[TradeDeal]): Iterable of trade deals
|
||||||
|
orders (list[TradeOrder]): Iterable of trade orders
|
||||||
|
total_deals: Total number of deals
|
||||||
|
total_orders (int): Total number orders
|
||||||
|
group (str): Filter for selecting history by symbols.
|
||||||
|
mt5 (MetaTrader): MetaTrader instance
|
||||||
|
config (Config): Config instance
|
||||||
|
"""
|
||||||
|
mt5: ClassVar[MetaTrader | MetaBackTester]
|
||||||
|
config: ClassVar[Config]
|
||||||
|
deals: tuple[TradeDeal, ...]
|
||||||
|
orders: tuple[TradeOrder, ...]
|
||||||
|
total_deals: int
|
||||||
|
total_orders: int
|
||||||
|
group: str
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if not hasattr(cls, 'config'):
|
||||||
|
cls.config = Config()
|
||||||
|
if not hasattr(cls, 'mt5'):
|
||||||
|
cls.mt5 = MetaTrader() if cls.config.mode != "backtest" else MetaBackTester()
|
||||||
|
return super().__new__(cls)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, *, date_from: datetime | float, date_to: datetime | float, group: str = "", use_utc: bool = float
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
date_from (datetime, float): Date the orders are requested from. Set by the 'datetime' object or as a
|
||||||
|
number of seconds elapsed since 1970.01.01.
|
||||||
|
|
||||||
|
date_to (datetime, float): Date up to which the orders are requested. Set by the 'datetime' object or as a
|
||||||
|
number of seconds elapsed since 1970.01.01.
|
||||||
|
|
||||||
|
use_utc (bool): Convert date_from and date_to to UTC. Default is False.
|
||||||
|
|
||||||
|
group (str): Filter for selecting history by symbols. This defaults to an empty string
|
||||||
|
"""
|
||||||
|
date_from = date_from if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from)
|
||||||
|
date_to = date_to if isinstance(date_to, datetime) else datetime.fromtimestamp(date_to)
|
||||||
|
self.date_from = date_from.astimezone(UTC) if use_utc else date_from
|
||||||
|
self.date_to = date_to.astimezone(UTC) if use_utc else date_to
|
||||||
|
self.group = group
|
||||||
|
self.deals: tuple[TradeDeal, ...] = ()
|
||||||
|
self.orders: tuple[TradeOrder, ...] = ()
|
||||||
|
self.total_deals: int = 0
|
||||||
|
self.total_orders: int = 0
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
"""Get history deals and orders"""
|
||||||
|
deals, orders = [self.get_deals(), self.get_orders()]
|
||||||
|
self.deals = deals if isinstance(deals, tuple) else ()
|
||||||
|
self.orders = orders if isinstance(orders, tuple) else ()
|
||||||
|
self.total_deals = len(self.deals)
|
||||||
|
self.total_orders = len(self.orders)
|
||||||
|
|
||||||
|
def get_deals(self) -> tuple[TradeDeal, ...]:
|
||||||
|
"""Get deals from trading history using the parameters set in the constructor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[TradeDeal, ...]: A list of trade deals
|
||||||
|
"""
|
||||||
|
deals = self.mt5.history_deals_get(date_from=self.date_from, date_to=self.date_to, group=self.group)
|
||||||
|
if deals is not None:
|
||||||
|
return tuple(TradeDeal(**deal._asdict()) for deal in deals)
|
||||||
|
logger.warning(f"Failed to get deals")
|
||||||
|
return tuple()
|
||||||
|
|
||||||
|
def get_deals_by_ticket(self, *, ticket: int) -> tuple[TradeDeal, ...]:
|
||||||
|
"""Call specifying the order ticket. Return all deals having the specified order ticket in the DEAL_ORDER
|
||||||
|
property.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ticket (int): The order ticket
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[TradeDeal]: A tuple of all deals with the order ticket
|
||||||
|
"""
|
||||||
|
return tuple(sorted((deal for deal in self.deals if deal.order == ticket), key=lambda x: x.time_msc))
|
||||||
|
|
||||||
|
def get_deals_by_position(self, *, position: int = None) -> tuple[TradeDeal, ...]:
|
||||||
|
"""
|
||||||
|
Get all deals with the specified position ticket in the DEAL_POSITION_ID property
|
||||||
|
Args:
|
||||||
|
position (int): The position ticket
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[TradeDeal]: A tuple of all deals with the position ticket
|
||||||
|
"""
|
||||||
|
return tuple(sorted((deal for deal in self.deals if deal.position_id == position), key=lambda x: x.time_msc))
|
||||||
|
|
||||||
|
def get_orders(self) -> tuple[TradeOrder, ...]:
|
||||||
|
"""Get orders from trading history using the parameters set in the constructor or the method arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[TradeOrder]: A list of trade orders
|
||||||
|
"""
|
||||||
|
orders = self.mt5.history_orders_get(date_from=self.date_from, date_to=self.date_to, group=self.group)
|
||||||
|
|
||||||
|
if orders is not None:
|
||||||
|
return tuple(TradeOrder(**order._asdict()) for order in orders)
|
||||||
|
|
||||||
|
logger.warning(f"Failed to get orders")
|
||||||
|
return tuple()
|
||||||
|
|
||||||
|
def get_orders_by_ticket(self, *, ticket: int) -> tuple[TradeOrder, ...]:
|
||||||
|
"""filter orders by ticket"""
|
||||||
|
return tuple(sorted((order for order in self.orders if order.ticket == ticket), key=lambda x: x.time_done_msc))
|
||||||
|
|
||||||
|
def get_orders_by_position(self, *, position: int) -> tuple[TradeOrder, ...]:
|
||||||
|
"""filter orders by position"""
|
||||||
|
return tuple(
|
||||||
|
sorted((order for order in self.orders if order.position_id == position), key=lambda x: x.time_done_msc)
|
||||||
|
)
|
||||||
@@ -48,6 +48,12 @@ class Order(_Base, TradeRequest):
|
|||||||
"""
|
"""
|
||||||
return cls.mt5.orders_total()
|
return cls.mt5.orders_total()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def cancel_order(cls, *, order: int, symbol: str) -> OrderSendResult:
|
||||||
|
"""Cancel an active pending order by ticket number."""
|
||||||
|
res = cls.mt5.order_send({"symbol": symbol, "order": order, "action": TradeAction.REMOVE})
|
||||||
|
return res
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_pending_order(cls, *, ticket: int) -> TradeOrder | None:
|
def get_pending_order(cls, *, ticket: int) -> TradeOrder | None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class Positions:
|
|||||||
kwargs["group"] = group
|
kwargs["group"] = group
|
||||||
positions = cls.mt5.positions_get(**kwargs)
|
positions = cls.mt5.positions_get(**kwargs)
|
||||||
if positions is not None:
|
if positions is not None:
|
||||||
return cls.positions
|
return tuple(TradePosition(**pos._asdict()) for pos in positions)
|
||||||
logger.warning("Failed to get open positions")
|
logger.warning("Failed to get open positions")
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import csv
|
|||||||
import logging
|
import logging
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from MetaTrader5 import TradePosition
|
|
||||||
|
|
||||||
from ..core.config import Config
|
from ..core.config import Config
|
||||||
from ..core.meta_trader import MetaTrader
|
from ..core.meta_trader import MetaTrader
|
||||||
from ..core.meta_backtester import MetaBackTester
|
from ..core.meta_backtester import MetaBackTester
|
||||||
|
from ..core.models import TradePosition
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from datetime import datetime, UTC
|
|||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
|
|
||||||
from ..core.models import OrderType, OrderSendResult, OrderCheckResult
|
from ..core.models import OrderType, OrderSendResult, OrderCheckResult
|
||||||
from ..core.config import Config
|
from ..core.config import Config
|
||||||
from ..core.task_queue import QueueItem
|
from ..core.task_queue import QueueItem
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
from .utils import *
|
from .utils import *
|
||||||
from .process_pool import *
|
from .process_pool import *
|
||||||
|
from .change import *
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# from importlib.metadata import version
|
|
||||||
# version = version("pandas-ta")
|
|
||||||
|
|
||||||
from pandas_ta.maps import EXCHANGE_TZ, RATE, Category, Imports
|
from pandas_ta.maps import EXCHANGE_TZ, RATE, Category, Imports
|
||||||
from pandas_ta.utils import *
|
from pandas_ta.utils import *
|
||||||
from pandas_ta.utils import __all__ as utils_all
|
from pandas_ta.utils import __all__ as utils_all
|
||||||
@@ -42,7 +38,6 @@ __all__ = [
|
|||||||
"RATE",
|
"RATE",
|
||||||
"Category",
|
"Category",
|
||||||
"Imports",
|
"Imports",
|
||||||
# "version",
|
|
||||||
"ma",
|
"ma",
|
||||||
"create_dir",
|
"create_dir",
|
||||||
"import_dir",
|
"import_dir",
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# #-*- coding: utf-8 -*-
|
|
||||||
# from pandas_ta import version
|
|
||||||
#
|
|
||||||
# SUPPORT="http://www.pandas-ta.dev/support"
|
|
||||||
#
|
|
||||||
# if __name__ == "__main__":
|
|
||||||
# print(f"Pandas TA: {version}\nSupport: {SUPPORT}")
|
|
||||||
+28
-42
@@ -1,17 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import (
|
from typing import Any, Iterable, Sequence, TypeVar, Dict, List, Tuple, TextIO, Union, Optional
|
||||||
Any,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Sequence,
|
|
||||||
TextIO,
|
|
||||||
Tuple,
|
|
||||||
TypeVar,
|
|
||||||
Union
|
|
||||||
)
|
|
||||||
|
|
||||||
from numpy import ndarray, recarray, void
|
from numpy import ndarray, recarray, void
|
||||||
from numpy import bool_ as np_bool_
|
from numpy import bool_ as np_bool_
|
||||||
@@ -21,40 +9,38 @@ from numpy import integer as np_integer
|
|||||||
from numpy import number as np_number
|
from numpy import number as np_number
|
||||||
from pandas import DataFrame, Series
|
from pandas import DataFrame, Series
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Generic types
|
# Generic types
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
# Scalars
|
# Scalars
|
||||||
Scalar = Union[str, float, int, complex, bool, object, np_generic]
|
Scalar = str | float | int | complex | bool | object | np_generic
|
||||||
Number = Union[int, float, complex, np_number, np_bool_]
|
Number = int | float | complex | np_number | np_bool_
|
||||||
Int = Union[int, np_integer]
|
Int = int | np_integer
|
||||||
Float = Union[float, np_floating]
|
Float = float | np_floating
|
||||||
IntFloat = Union[Int, Float]
|
IntFloat = Int | Float
|
||||||
|
|
||||||
# Basic sequences
|
# Basic sequences
|
||||||
MaybeTuple = Union[T, Tuple[T, ...]]
|
MaybeTuple = T | tuple[T, ...]
|
||||||
MaybeList = Union[T, List[T]]
|
MaybeList = T | list[T]
|
||||||
TupleList = Union[List[T], Tuple[T, ...]]
|
TupleList = list[T] | tuple[T, ...]
|
||||||
MaybeTupleList = Union[T, List[T], Tuple[T, ...]]
|
MaybeTupleList = T | list[T] | tuple[T, ...]
|
||||||
MaybeIterable = Union[T, Iterable[T]]
|
MaybeIterable = T | Iterable[T]
|
||||||
MaybeSequence = Union[T, Sequence[T]]
|
MaybeSequence = T | Sequence[T]
|
||||||
ListStr = List[str]
|
ListStr = list[str]
|
||||||
|
|
||||||
DictLike = Union[None, dict]
|
DictLike = None | dict
|
||||||
DictLikeSequence = MaybeSequence[DictLike]
|
DictLikeSequence = MaybeSequence[DictLike]
|
||||||
Args = Tuple[Any, ...]
|
Args = tuple[Any, ...]
|
||||||
ArgsLike = Union[None, Args]
|
ArgsLike = None | Args
|
||||||
Kwargs = Dict[str, Any]
|
Kwargs = dict[str, Any]
|
||||||
KwargsLike = Union[None, Kwargs]
|
KwargsLike = None | Kwargs
|
||||||
KwargsLikeSequence = MaybeSequence[KwargsLike]
|
KwargsLikeSequence = MaybeSequence[KwargsLike]
|
||||||
FileName = Union[str, Path]
|
FileName = str | Path
|
||||||
|
|
||||||
DTypeLike = Any
|
DTypeLike = Any
|
||||||
PandasDTypeLike = Any
|
PandasDTypeLike = Any
|
||||||
Shape = Tuple[int, ...]
|
Shape = tuple[int, ...]
|
||||||
RelaxedShape = Union[int, Shape]
|
RelaxedShape = int | Shape
|
||||||
Array = ndarray
|
Array = ndarray
|
||||||
Array1d = ndarray
|
Array1d = ndarray
|
||||||
Array2d = ndarray
|
Array2d = ndarray
|
||||||
@@ -62,10 +48,10 @@ Array3d = ndarray
|
|||||||
Record = void
|
Record = void
|
||||||
RecordArray = ndarray
|
RecordArray = ndarray
|
||||||
RecArray = recarray
|
RecArray = recarray
|
||||||
MaybeArray = Union[T, Array]
|
MaybeArray = T | Array
|
||||||
SeriesFrame = Union[Series, DataFrame]
|
SeriesFrame = Series | DataFrame
|
||||||
MaybeSeries = Union[T, Series]
|
MaybeSeries = T | Series
|
||||||
MaybeSeriesFrame = Union[T, Series, DataFrame]
|
MaybeSeriesFrame = T | Series | DataFrame
|
||||||
AnyArray = Union[Array, Series, DataFrame]
|
AnyArray = Array | Series | DataFrame
|
||||||
AnyArray1d = Union[Array1d, Series]
|
AnyArray1d = Array1d | Series
|
||||||
AnyArray2d = Union[Array2d, DataFrame]
|
AnyArray2d = Array2d | DataFrame
|
||||||
|
|||||||
Reference in New Issue
Block a user