Trading

pdmt5.trading

Trading operations module with advanced MetaTrader5 functionality.

Mt5TradingClient

Bases: Mt5DataClient

MetaTrader5 trading client with advanced trading operations.

This class extends Mt5DataClient to provide specialized trading functionality including position management, order analysis, and trading performance metrics.

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

mt5_failed_trade_retcodes cached property

mt5_failed_trade_retcodes: set[int]

Set of failed trade return codes.

Returns:

Type Description
set[int]

Set of failed trade return codes.

mt5_successful_trade_retcodes cached property

mt5_successful_trade_retcodes: set[int]

Set of successful trade return codes.

Returns:

Type Description
set[int]

Set of successful trade return codes.

calculate_minimum_order_margin

calculate_minimum_order_margin(
    symbol: str, order_side: Literal["BUY", "SELL"]
) -> dict[str, float]

Calculate the minimum order margins for a given symbol.

Parameters:

Name Type Description Default
symbol str

Symbol for which to calculate the minimum order margins.

required
order_side Literal['BUY', 'SELL']

Optional side of the order, either "BUY" or "SELL".

required

Returns:

Type Description
dict[str, float]

Dictionary with minimum volume and margin for the specified order side.

Source code in pdmt5/trading.py
def calculate_minimum_order_margin(
    self,
    symbol: str,
    order_side: Literal["BUY", "SELL"],
) -> dict[str, float]:
    """Calculate the minimum order margins for a given symbol.

    Args:
        symbol: Symbol for which to calculate the minimum order margins.
        order_side: Optional side of the order, either "BUY" or "SELL".

    Returns:
        Dictionary with minimum volume and margin for the specified order side.
    """
    symbol_info = self.symbol_info_as_dict(symbol=symbol)
    symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
    margin = self.order_calc_margin(
        action=getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
        symbol=symbol,
        volume=symbol_info["volume_min"],
        price=(
            symbol_info_tick["bid"]
            if order_side == "SELL"
            else symbol_info_tick["ask"]
        ),
    )
    result = {"volume": symbol_info["volume_min"], "margin": margin}
    if margin:
        self.logger.info(
            "Calculated minimum %s order margin for %s: %s",
            order_side,
            symbol,
            result,
        )
    else:
        self.logger.warning(
            "Calculated minimum order margin to %s %s: %s",
            order_side,
            symbol,
            result,
        )
    return result

calculate_new_position_margin_ratio

calculate_new_position_margin_ratio(
    symbol: str,
    new_position_side: Literal["BUY", "SELL"] | None = None,
    new_position_volume: float = 0,
) -> float

Calculate the margin ratio for a new position.

Parameters:

Name Type Description Default
symbol str

Symbol for which to calculate the margin ratio.

required
new_position_side Literal['BUY', 'SELL'] | None

Side of the new position, either "BUY" or "SELL".

None
new_position_volume float

Volume of the new position.

0

Returns:

Name Type Description
float float

Margin ratio for the new position as a fraction of account equity.

Source code in pdmt5/trading.py
def calculate_new_position_margin_ratio(
    self,
    symbol: str,
    new_position_side: Literal["BUY", "SELL"] | None = None,
    new_position_volume: float = 0,
) -> float:
    """Calculate the margin ratio for a new position.

    Args:
        symbol: Symbol for which to calculate the margin ratio.
        new_position_side: Side of the new position, either "BUY" or "SELL".
        new_position_volume: Volume of the new position.

    Returns:
        float: Margin ratio for the new position as a fraction of account equity.
    """
    account_info = self.account_info_as_dict()
    if not account_info["equity"]:
        result = 0.0
    else:
        positions_df = self.fetch_positions_with_metrics_as_df(symbol=symbol)
        current_signed_margin = (
            positions_df["signed_margin"].sum() if positions_df.size else 0
        )
        symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
        if not (new_position_side and new_position_volume):
            new_signed_margin = 0
        elif new_position_side.upper() == "BUY":
            new_signed_margin = self.order_calc_margin(
                action=self.mt5.ORDER_TYPE_BUY,
                symbol=symbol,
                volume=new_position_volume,
                price=symbol_info_tick["ask"],
            )
        elif new_position_side.upper() == "SELL":
            new_signed_margin = -self.order_calc_margin(
                action=self.mt5.ORDER_TYPE_SELL,
                symbol=symbol,
                volume=new_position_volume,
                price=symbol_info_tick["bid"],
            )
        else:
            new_signed_margin = 0
        result = abs(
            (new_signed_margin + current_signed_margin) / account_info["equity"]
        )
    self.logger.info(
        "Calculated new position margin ratio for %s: %s",
        symbol,
        result,
    )
    return result

calculate_spread_ratio

calculate_spread_ratio(symbol: str) -> float

Calculate the spread ratio for a given symbol.

Parameters:

Name Type Description Default
symbol str

Symbol for which to calculate the spread ratio.

required

Returns:

Type Description
float

Spread ratio as a float.

Source code in pdmt5/trading.py
def calculate_spread_ratio(
    self,
    symbol: str,
) -> float:
    """Calculate the spread ratio for a given symbol.

    Args:
        symbol: Symbol for which to calculate the spread ratio.

    Returns:
        Spread ratio as a float.
    """
    symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
    result = (
        (symbol_info_tick["ask"] - symbol_info_tick["bid"])
        / (symbol_info_tick["ask"] + symbol_info_tick["bid"])
        * 2
    )
    self.logger.info("Calculated spread ratio for %s: %s", symbol, result)
    return result

calculate_volume_by_margin

calculate_volume_by_margin(
    symbol: str,
    margin: float,
    order_side: Literal["BUY", "SELL"],
) -> float

Calculate volume based on margin for a given symbol and order side.

Parameters:

Name Type Description Default
symbol str

Symbol for which to calculate the volume.

required
margin float

Margin amount to use for the calculation.

required
order_side Literal['BUY', 'SELL']

Side of the order, either "BUY" or "SELL".

required

Returns:

Type Description
float

Calculated volume as a float.

Source code in pdmt5/trading.py
def calculate_volume_by_margin(
    self,
    symbol: str,
    margin: float,
    order_side: Literal["BUY", "SELL"],
) -> float:
    """Calculate volume based on margin for a given symbol and order side.

    Args:
        symbol: Symbol for which to calculate the volume.
        margin: Margin amount to use for the calculation.
        order_side: Side of the order, either "BUY" or "SELL".

    Returns:
        Calculated volume as a float.
    """
    min_order_margin_dict = self.calculate_minimum_order_margin(
        symbol=symbol,
        order_side=order_side,
    )
    if min_order_margin_dict["margin"]:
        result = (
            floor(margin / min_order_margin_dict["margin"])
            * min_order_margin_dict["volume"]
        )
    else:
        result = 0.0
    self.logger.info(
        "Calculated volume by margin to %s %s: %s",
        order_side,
        symbol,
        result,
    )
    return result

close_open_positions

close_open_positions(
    symbols: str
    | list[str]
    | tuple[str, ...]
    | None = None,
    order_filling_mode: Literal[
        "IOC", "FOK", "RETURN"
    ] = "IOC",
    dry_run: bool = False,
    **kwargs: Any,
) -> dict[str, list[dict[str, Any]]]

Close all open positions for specified symbols.

Parameters:

Name Type Description Default
symbols str | list[str] | tuple[str, ...] | None

Optional symbol or list of symbols to filter positions. If None, all symbols will be considered.

None
order_filling_mode Literal['IOC', 'FOK', 'RETURN']

Order filling mode, either "IOC", "FOK", or "RETURN".

'IOC'
dry_run bool

If True, only check the order without sending it.

False
**kwargs Any

Additional keyword arguments for request parameters.

{}

Returns:

Type Description
dict[str, list[dict[str, Any]]]

Dictionary with symbols as keys and lists of dictionaries containing operation results for each closed position as values.

Source code in pdmt5/trading.py
def close_open_positions(
    self,
    symbols: str | list[str] | tuple[str, ...] | None = None,
    order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
    dry_run: bool = False,
    **kwargs: Any,  # noqa: ANN401
) -> dict[str, list[dict[str, Any]]]:
    """Close all open positions for specified symbols.

    Args:
        symbols: Optional symbol or list of symbols to filter positions.
            If None, all symbols will be considered.
        order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
        dry_run: If True, only check the order without sending it.
        **kwargs: Additional keyword arguments for request parameters.

    Returns:
        Dictionary with symbols as keys and lists of dictionaries containing
            operation results for each closed position as values.
    """
    if isinstance(symbols, str):
        symbol_list = [symbols]
    elif isinstance(symbols, (list, tuple)):
        symbol_list = symbols
    else:
        symbol_list = self.symbols_get()
    self.logger.info("Fetching and closing positions for symbols: %s", symbol_list)
    return {
        s: self._fetch_and_close_position(
            symbol=s,
            order_filling_mode=order_filling_mode,
            dry_run=dry_run,
            **kwargs,
        )
        for s in symbol_list
    }

collect_entry_deals_as_df

collect_entry_deals_as_df(
    symbol: str,
    history_seconds: int = 3600,
    index_keys: str | None = "ticket",
) -> DataFrame

Collect entry deals as a DataFrame.

Parameters:

Name Type Description Default
symbol str

Symbol to collect entry deals for.

required
history_seconds int

Time range in seconds to fetch deals around the last tick.

3600
index_keys str | None

Optional index keys for the DataFrame.

'ticket'

Returns:

Type Description
DataFrame

pd.DataFrame: Entry deals with time index.

Source code in pdmt5/trading.py
def collect_entry_deals_as_df(
    self,
    symbol: str,
    history_seconds: int = 3600,
    index_keys: str | None = "ticket",
) -> pd.DataFrame:
    """Collect entry deals as a DataFrame.

    Args:
        symbol: Symbol to collect entry deals for.
        history_seconds: Time range in seconds to fetch deals around the last tick.
        index_keys: Optional index keys for the DataFrame.

    Returns:
        pd.DataFrame: Entry deals with time index.
    """
    last_tick_time = self.symbol_info_tick_as_dict(symbol=symbol)["time"]
    deals_df = self.history_deals_get_as_df(
        date_from=(last_tick_time - timedelta(seconds=history_seconds)),
        date_to=(last_tick_time + timedelta(seconds=history_seconds)),
        symbol=symbol,
        index_keys=index_keys,
    )
    if deals_df.empty:
        result = deals_df
    else:
        result = deals_df.pipe(
            lambda d: d[
                d["entry"]
                & d["type"].isin({self.mt5.DEAL_TYPE_BUY, self.mt5.DEAL_TYPE_SELL})
            ]
        )
    self.logger.info(
        "Collected entry deals for %s: %d rows",
        symbol,
        result.shape[0],
    )
    return result

fetch_latest_rates_as_df

fetch_latest_rates_as_df(
    symbol: str,
    granularity: str = "M1",
    count: int = 1440,
    index_keys: str | None = "time",
) -> DataFrame

Fetch rate (OHLC) data as a DataFrame.

Parameters:

Name Type Description Default
symbol str

Symbol to fetch data for.

required
granularity str

Time granularity as a timeframe suffix (e.g., "M1", "H1").

'M1'
count int

Number of bars to fetch.

1440
index_keys str | None

Optional index keys for the DataFrame.

'time'

Returns:

Type Description
DataFrame

pd.DataFrame: OHLC data with time index.

Raises:

Type Description
Mt5TradingError

If the granularity is not supported by MetaTrader5.

Source code in pdmt5/trading.py
def fetch_latest_rates_as_df(
    self,
    symbol: str,
    granularity: str = "M1",
    count: int = 1440,
    index_keys: str | None = "time",
) -> pd.DataFrame:
    """Fetch rate (OHLC) data as a DataFrame.

    Args:
        symbol: Symbol to fetch data for.
        granularity: Time granularity as a timeframe suffix (e.g., "M1", "H1").
        count: Number of bars to fetch.
        index_keys: Optional index keys for the DataFrame.

    Returns:
        pd.DataFrame: OHLC data with time index.

    Raises:
        Mt5TradingError: If the granularity is not supported by MetaTrader5.
    """
    try:
        timeframe = getattr(self.mt5, f"TIMEFRAME_{granularity.upper()}")
    except AttributeError as e:
        error_message = (
            f"MetaTrader5 does not support the given granularity: {granularity}"
        )
        raise Mt5TradingError(error_message) from e
    else:
        result = self.copy_rates_from_pos_as_df(
            symbol=symbol,
            timeframe=timeframe,
            start_pos=0,
            count=count,
            index_keys=index_keys,
        )
        self.logger.info(
            "Fetched latest %s rates for %s: %d rows",
            granularity,
            symbol,
            result.shape[0],
        )
        return result

fetch_latest_ticks_as_df

fetch_latest_ticks_as_df(
    symbol: str,
    seconds: int = 300,
    index_keys: str | None = "time_msc",
) -> DataFrame

Fetch tick data as a DataFrame.

Parameters:

Name Type Description Default
symbol str

Symbol to fetch tick data for.

required
seconds int

Time range in seconds to fetch ticks around the last tick time.

300
index_keys str | None

Optional index keys for the DataFrame.

'time_msc'

Returns:

Type Description
DataFrame

pd.DataFrame: Tick data with time index.

Source code in pdmt5/trading.py
def fetch_latest_ticks_as_df(
    self,
    symbol: str,
    seconds: int = 300,
    index_keys: str | None = "time_msc",
) -> pd.DataFrame:
    """Fetch tick data as a DataFrame.

    Args:
        symbol: Symbol to fetch tick data for.
        seconds: Time range in seconds to fetch ticks around the last tick time.
        index_keys: Optional index keys for the DataFrame.

    Returns:
        pd.DataFrame: Tick data with time index.
    """
    last_tick_time = self.symbol_info_tick_as_dict(symbol=symbol)["time"]
    result = self.copy_ticks_range_as_df(
        symbol=symbol,
        date_from=(last_tick_time - timedelta(seconds=seconds)),
        date_to=(last_tick_time + timedelta(seconds=seconds)),
        flags=self.mt5.COPY_TICKS_ALL,
        index_keys=index_keys,
    )
    self.logger.info(
        "Fetched latest ticks for %s: %d rows",
        symbol,
        result.shape[0],
    )
    return result

fetch_positions_with_metrics_as_df

fetch_positions_with_metrics_as_df(
    symbol: str,
) -> DataFrame

Fetch open positions as a DataFrame with additional metrics.

Parameters:

Name Type Description Default
symbol str

Symbol to fetch positions for.

required

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame containing open positions with additional metrics.

Source code in pdmt5/trading.py
def fetch_positions_with_metrics_as_df(
    self,
    symbol: str,
) -> pd.DataFrame:
    """Fetch open positions as a DataFrame with additional metrics.

    Args:
        symbol: Symbol to fetch positions for.

    Returns:
        pd.DataFrame: DataFrame containing open positions with additional metrics.
    """
    positions_df = self.positions_get_as_df(symbol=symbol)
    if positions_df.empty:
        result = positions_df
    else:
        symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
        ask_margin = self.order_calc_margin(
            action=self.mt5.ORDER_TYPE_BUY,
            symbol=symbol,
            volume=1,
            price=symbol_info_tick["ask"],
        )
        bid_margin = self.order_calc_margin(
            action=self.mt5.ORDER_TYPE_SELL,
            symbol=symbol,
            volume=1,
            price=symbol_info_tick["bid"],
        )
        result = (
            positions_df.assign(
                elapsed_seconds=lambda d: (
                    symbol_info_tick["time"] - d["time"]
                ).dt.total_seconds(),
                underlier_increase_ratio=lambda d: (
                    d["price_current"] / d["price_open"] - 1
                ),
                buy=lambda d: (d["type"] == self.mt5.POSITION_TYPE_BUY),
                sell=lambda d: (d["type"] == self.mt5.POSITION_TYPE_SELL),
            )
            .assign(
                buy_i=lambda d: d["buy"].astype(int),
                sell_i=lambda d: d["sell"].astype(int),
            )
            .assign(
                sign=lambda d: (d["buy_i"] - d["sell_i"]),
                margin=lambda d: (
                    (d["buy_i"] * ask_margin + d["sell_i"] * bid_margin)
                    * d["volume"]
                ),
            )
            .assign(
                signed_volume=lambda d: (d["volume"] * d["sign"]),
                signed_margin=lambda d: (d["margin"] * d["sign"]),
                underlier_profit_ratio=lambda d: (
                    d["underlier_increase_ratio"] * d["sign"]
                ),
            )
            .drop(columns=["buy_i", "sell_i", "sign", "underlier_increase_ratio"])
        )
    self.logger.info(
        "Fetched positions with metrics for %s: %d rows",
        symbol,
        result.shape[0],
    )
    return result

place_market_order

place_market_order(
    symbol: str,
    volume: float,
    order_side: Literal["BUY", "SELL"],
    order_filling_mode: Literal[
        "IOC", "FOK", "RETURN"
    ] = "IOC",
    order_time_mode: Literal[
        "GTC", "DAY", "SPECIFIED", "SPECIFIED_DAY"
    ] = "GTC",
    raise_on_error: bool = False,
    dry_run: bool = False,
    **kwargs: Any,
) -> dict[str, Any]

Send or check an order request to place a market order.

Parameters:

Name Type Description Default
symbol str

Symbol for the order.

required
volume float

Volume of the order.

required
order_side Literal['BUY', 'SELL']

Side of the order, either "BUY" or "SELL".

required
order_filling_mode Literal['IOC', 'FOK', 'RETURN']

Order filling mode, either "IOC", "FOK", or "RETURN".

'IOC'
order_time_mode Literal['GTC', 'DAY', 'SPECIFIED', 'SPECIFIED_DAY']

Order time mode, either "GTC", "DAY", "SPECIFIED", or "SPECIFIED_DAY".

'GTC'
raise_on_error bool

If True, raise an error on operation failure.

False
dry_run bool

If True, only check the order without sending it.

False
**kwargs Any

Additional keyword arguments for request parameters.

{}

Returns:

Type Description
dict[str, Any]

Dictionary with operation result.

Source code in pdmt5/trading.py
def place_market_order(
    self,
    symbol: str,
    volume: float,
    order_side: Literal["BUY", "SELL"],
    order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
    order_time_mode: Literal["GTC", "DAY", "SPECIFIED", "SPECIFIED_DAY"] = "GTC",
    raise_on_error: bool = False,
    dry_run: bool = False,
    **kwargs: Any,  # noqa: ANN401
) -> dict[str, Any]:
    """Send or check an order request to place a market order.

    Args:
        symbol: Symbol for the order.
        volume: Volume of the order.
        order_side: Side of the order, either "BUY" or "SELL".
        order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
        order_time_mode: Order time mode, either "GTC", "DAY", "SPECIFIED",
            or "SPECIFIED_DAY".
        raise_on_error: If True, raise an error on operation failure.
        dry_run: If True, only check the order without sending it.
        **kwargs: Additional keyword arguments for request parameters.

    Returns:
        Dictionary with operation result.
    """
    self.logger.info("Placing market order: %s %s %s", order_side, volume, symbol)
    return self._send_or_check_order(
        request={
            "action": self.mt5.TRADE_ACTION_DEAL,
            "symbol": symbol,
            "volume": volume,
            "type": getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
            "type_filling": getattr(
                self.mt5, f"ORDER_FILLING_{order_filling_mode.upper()}"
            ),
            "type_time": getattr(self.mt5, f"ORDER_TIME_{order_time_mode.upper()}"),
            **kwargs,
        },
        raise_on_error=raise_on_error,
        dry_run=dry_run,
    )

update_sltp_for_open_positions

update_sltp_for_open_positions(
    symbol: str,
    stop_loss: float | None = None,
    take_profit: float | None = None,
    tickets: list[int] | None = None,
    raise_on_error: bool = False,
    dry_run: bool = False,
    **kwargs: Any,
) -> list[dict[str, Any]]

Change Stop Loss and Take Profit for open positions.

Parameters:

Name Type Description Default
symbol str

Symbol for the position.

required
stop_loss float | None

New Stop Loss price. If None, it will not be changed.

None
take_profit float | None

New Take Profit price. If None, it will not be changed.

None
tickets list[int] | None

List of position tickets to filter positions. If None, all open positions for the symbol will be considered.

None
raise_on_error bool

If True, raise an error on operation failure.

False
dry_run bool

If True, only check the order without sending it.

False
**kwargs Any

Additional keyword arguments for request parameters.

{}

Returns:

Type Description
list[dict[str, Any]]

List of dictionaries with operation results for each updated position.

Source code in pdmt5/trading.py
def update_sltp_for_open_positions(
    self,
    symbol: str,
    stop_loss: float | None = None,
    take_profit: float | None = None,
    tickets: list[int] | None = None,
    raise_on_error: bool = False,
    dry_run: bool = False,
    **kwargs: Any,  # noqa: ANN401
) -> list[dict[str, Any]]:
    """Change Stop Loss and Take Profit for open positions.

    Args:
        symbol: Symbol for the position.
        stop_loss: New Stop Loss price. If None, it will not be changed.
        take_profit: New Take Profit price. If None, it will not be changed.
        tickets: List of position tickets to filter positions. If None, all open
            positions for the symbol will be considered.
        raise_on_error: If True, raise an error on operation failure.
        dry_run: If True, only check the order without sending it.
        **kwargs: Additional keyword arguments for request parameters.

    Returns:
        List of dictionaries with operation results for each updated position.
    """
    positions_df = self.positions_get_as_df(symbol=symbol)
    if positions_df.empty:
        self.logger.warning("No open positions found for symbol: %s", symbol)
        return []
    elif tickets:
        filtered_positions_df = positions_df.pipe(
            lambda d: d[d["ticket"].isin(tickets)]
        )
    else:
        filtered_positions_df = positions_df
    if filtered_positions_df.empty:
        self.logger.warning(
            "No open positions found for symbol: %s with specified tickets: %s",
            symbol,
            tickets,
        )
        return []
    else:
        symbol_info = self.symbol_info_as_dict(symbol=symbol)
        sl = round(stop_loss, symbol_info["digits"]) if stop_loss else None
        tp = round(take_profit, symbol_info["digits"]) if take_profit else None
        order_requests = [
            {
                "action": self.mt5.TRADE_ACTION_SLTP,
                "symbol": p["symbol"],
                "position": p["ticket"],
                "sl": (sl or p["sl"]),
                "tp": (tp or p["tp"]),
                **kwargs,
            }
            for _, p in filtered_positions_df.iterrows()
            if sl != p["sl"] or tp != p["tp"]
        ]
        if order_requests:
            self.logger.info(
                "Updating SL/TP for %d positions for %s: %s/%s",
                len(order_requests),
                symbol,
                sl,
                tp,
            )
            return [
                self._send_or_check_order(
                    request=r, raise_on_error=raise_on_error, dry_run=dry_run
                )
                for r in order_requests
            ]
        else:
            self.logger.info(
                "No positions to update for symbol: %s with SL: %s and TP: %s",
                symbol,
                sl,
                tp,
            )
            return []

Mt5TradingError

Bases: Mt5RuntimeError

MetaTrader5 trading error.

Overview

The trading module extends Mt5DataClient with advanced trading operations including position management, order execution, and dry run support for testing trading strategies without actual execution.

Classes

Mt5TradingClient

pdmt5.trading.Mt5TradingClient

Bases: Mt5DataClient

MetaTrader5 trading client with advanced trading operations.

This class extends Mt5DataClient to provide specialized trading functionality including position management, order analysis, and trading performance metrics.

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

mt5_failed_trade_retcodes cached property

mt5_failed_trade_retcodes: set[int]

Set of failed trade return codes.

Returns:

Type Description
set[int]

Set of failed trade return codes.

mt5_successful_trade_retcodes cached property

mt5_successful_trade_retcodes: set[int]

Set of successful trade return codes.

Returns:

Type Description
set[int]

Set of successful trade return codes.

calculate_minimum_order_margin

calculate_minimum_order_margin(
    symbol: str, order_side: Literal["BUY", "SELL"]
) -> dict[str, float]

Calculate the minimum order margins for a given symbol.

Parameters:

Name Type Description Default
symbol str

Symbol for which to calculate the minimum order margins.

required
order_side Literal['BUY', 'SELL']

Optional side of the order, either "BUY" or "SELL".

required

Returns:

Type Description
dict[str, float]

Dictionary with minimum volume and margin for the specified order side.

Source code in pdmt5/trading.py
def calculate_minimum_order_margin(
    self,
    symbol: str,
    order_side: Literal["BUY", "SELL"],
) -> dict[str, float]:
    """Calculate the minimum order margins for a given symbol.

    Args:
        symbol: Symbol for which to calculate the minimum order margins.
        order_side: Optional side of the order, either "BUY" or "SELL".

    Returns:
        Dictionary with minimum volume and margin for the specified order side.
    """
    symbol_info = self.symbol_info_as_dict(symbol=symbol)
    symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
    margin = self.order_calc_margin(
        action=getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
        symbol=symbol,
        volume=symbol_info["volume_min"],
        price=(
            symbol_info_tick["bid"]
            if order_side == "SELL"
            else symbol_info_tick["ask"]
        ),
    )
    result = {"volume": symbol_info["volume_min"], "margin": margin}
    if margin:
        self.logger.info(
            "Calculated minimum %s order margin for %s: %s",
            order_side,
            symbol,
            result,
        )
    else:
        self.logger.warning(
            "Calculated minimum order margin to %s %s: %s",
            order_side,
            symbol,
            result,
        )
    return result

calculate_new_position_margin_ratio

calculate_new_position_margin_ratio(
    symbol: str,
    new_position_side: Literal["BUY", "SELL"] | None = None,
    new_position_volume: float = 0,
) -> float

Calculate the margin ratio for a new position.

Parameters:

Name Type Description Default
symbol str

Symbol for which to calculate the margin ratio.

required
new_position_side Literal['BUY', 'SELL'] | None

Side of the new position, either "BUY" or "SELL".

None
new_position_volume float

Volume of the new position.

0

Returns:

Name Type Description
float float

Margin ratio for the new position as a fraction of account equity.

Source code in pdmt5/trading.py
def calculate_new_position_margin_ratio(
    self,
    symbol: str,
    new_position_side: Literal["BUY", "SELL"] | None = None,
    new_position_volume: float = 0,
) -> float:
    """Calculate the margin ratio for a new position.

    Args:
        symbol: Symbol for which to calculate the margin ratio.
        new_position_side: Side of the new position, either "BUY" or "SELL".
        new_position_volume: Volume of the new position.

    Returns:
        float: Margin ratio for the new position as a fraction of account equity.
    """
    account_info = self.account_info_as_dict()
    if not account_info["equity"]:
        result = 0.0
    else:
        positions_df = self.fetch_positions_with_metrics_as_df(symbol=symbol)
        current_signed_margin = (
            positions_df["signed_margin"].sum() if positions_df.size else 0
        )
        symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
        if not (new_position_side and new_position_volume):
            new_signed_margin = 0
        elif new_position_side.upper() == "BUY":
            new_signed_margin = self.order_calc_margin(
                action=self.mt5.ORDER_TYPE_BUY,
                symbol=symbol,
                volume=new_position_volume,
                price=symbol_info_tick["ask"],
            )
        elif new_position_side.upper() == "SELL":
            new_signed_margin = -self.order_calc_margin(
                action=self.mt5.ORDER_TYPE_SELL,
                symbol=symbol,
                volume=new_position_volume,
                price=symbol_info_tick["bid"],
            )
        else:
            new_signed_margin = 0
        result = abs(
            (new_signed_margin + current_signed_margin) / account_info["equity"]
        )
    self.logger.info(
        "Calculated new position margin ratio for %s: %s",
        symbol,
        result,
    )
    return result

calculate_spread_ratio

calculate_spread_ratio(symbol: str) -> float

Calculate the spread ratio for a given symbol.

Parameters:

Name Type Description Default
symbol str

Symbol for which to calculate the spread ratio.

required

Returns:

Type Description
float

Spread ratio as a float.

Source code in pdmt5/trading.py
def calculate_spread_ratio(
    self,
    symbol: str,
) -> float:
    """Calculate the spread ratio for a given symbol.

    Args:
        symbol: Symbol for which to calculate the spread ratio.

    Returns:
        Spread ratio as a float.
    """
    symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
    result = (
        (symbol_info_tick["ask"] - symbol_info_tick["bid"])
        / (symbol_info_tick["ask"] + symbol_info_tick["bid"])
        * 2
    )
    self.logger.info("Calculated spread ratio for %s: %s", symbol, result)
    return result

calculate_volume_by_margin

calculate_volume_by_margin(
    symbol: str,
    margin: float,
    order_side: Literal["BUY", "SELL"],
) -> float

Calculate volume based on margin for a given symbol and order side.

Parameters:

Name Type Description Default
symbol str

Symbol for which to calculate the volume.

required
margin float

Margin amount to use for the calculation.

required
order_side Literal['BUY', 'SELL']

Side of the order, either "BUY" or "SELL".

required

Returns:

Type Description
float

Calculated volume as a float.

Source code in pdmt5/trading.py
def calculate_volume_by_margin(
    self,
    symbol: str,
    margin: float,
    order_side: Literal["BUY", "SELL"],
) -> float:
    """Calculate volume based on margin for a given symbol and order side.

    Args:
        symbol: Symbol for which to calculate the volume.
        margin: Margin amount to use for the calculation.
        order_side: Side of the order, either "BUY" or "SELL".

    Returns:
        Calculated volume as a float.
    """
    min_order_margin_dict = self.calculate_minimum_order_margin(
        symbol=symbol,
        order_side=order_side,
    )
    if min_order_margin_dict["margin"]:
        result = (
            floor(margin / min_order_margin_dict["margin"])
            * min_order_margin_dict["volume"]
        )
    else:
        result = 0.0
    self.logger.info(
        "Calculated volume by margin to %s %s: %s",
        order_side,
        symbol,
        result,
    )
    return result

close_open_positions

close_open_positions(
    symbols: str
    | list[str]
    | tuple[str, ...]
    | None = None,
    order_filling_mode: Literal[
        "IOC", "FOK", "RETURN"
    ] = "IOC",
    dry_run: bool = False,
    **kwargs: Any,
) -> dict[str, list[dict[str, Any]]]

Close all open positions for specified symbols.

Parameters:

Name Type Description Default
symbols str | list[str] | tuple[str, ...] | None

Optional symbol or list of symbols to filter positions. If None, all symbols will be considered.

None
order_filling_mode Literal['IOC', 'FOK', 'RETURN']

Order filling mode, either "IOC", "FOK", or "RETURN".

'IOC'
dry_run bool

If True, only check the order without sending it.

False
**kwargs Any

Additional keyword arguments for request parameters.

{}

Returns:

Type Description
dict[str, list[dict[str, Any]]]

Dictionary with symbols as keys and lists of dictionaries containing operation results for each closed position as values.

Source code in pdmt5/trading.py
def close_open_positions(
    self,
    symbols: str | list[str] | tuple[str, ...] | None = None,
    order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
    dry_run: bool = False,
    **kwargs: Any,  # noqa: ANN401
) -> dict[str, list[dict[str, Any]]]:
    """Close all open positions for specified symbols.

    Args:
        symbols: Optional symbol or list of symbols to filter positions.
            If None, all symbols will be considered.
        order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
        dry_run: If True, only check the order without sending it.
        **kwargs: Additional keyword arguments for request parameters.

    Returns:
        Dictionary with symbols as keys and lists of dictionaries containing
            operation results for each closed position as values.
    """
    if isinstance(symbols, str):
        symbol_list = [symbols]
    elif isinstance(symbols, (list, tuple)):
        symbol_list = symbols
    else:
        symbol_list = self.symbols_get()
    self.logger.info("Fetching and closing positions for symbols: %s", symbol_list)
    return {
        s: self._fetch_and_close_position(
            symbol=s,
            order_filling_mode=order_filling_mode,
            dry_run=dry_run,
            **kwargs,
        )
        for s in symbol_list
    }

collect_entry_deals_as_df

collect_entry_deals_as_df(
    symbol: str,
    history_seconds: int = 3600,
    index_keys: str | None = "ticket",
) -> DataFrame

Collect entry deals as a DataFrame.

Parameters:

Name Type Description Default
symbol str

Symbol to collect entry deals for.

required
history_seconds int

Time range in seconds to fetch deals around the last tick.

3600
index_keys str | None

Optional index keys for the DataFrame.

'ticket'

Returns:

Type Description
DataFrame

pd.DataFrame: Entry deals with time index.

Source code in pdmt5/trading.py
def collect_entry_deals_as_df(
    self,
    symbol: str,
    history_seconds: int = 3600,
    index_keys: str | None = "ticket",
) -> pd.DataFrame:
    """Collect entry deals as a DataFrame.

    Args:
        symbol: Symbol to collect entry deals for.
        history_seconds: Time range in seconds to fetch deals around the last tick.
        index_keys: Optional index keys for the DataFrame.

    Returns:
        pd.DataFrame: Entry deals with time index.
    """
    last_tick_time = self.symbol_info_tick_as_dict(symbol=symbol)["time"]
    deals_df = self.history_deals_get_as_df(
        date_from=(last_tick_time - timedelta(seconds=history_seconds)),
        date_to=(last_tick_time + timedelta(seconds=history_seconds)),
        symbol=symbol,
        index_keys=index_keys,
    )
    if deals_df.empty:
        result = deals_df
    else:
        result = deals_df.pipe(
            lambda d: d[
                d["entry"]
                & d["type"].isin({self.mt5.DEAL_TYPE_BUY, self.mt5.DEAL_TYPE_SELL})
            ]
        )
    self.logger.info(
        "Collected entry deals for %s: %d rows",
        symbol,
        result.shape[0],
    )
    return result

fetch_latest_rates_as_df

fetch_latest_rates_as_df(
    symbol: str,
    granularity: str = "M1",
    count: int = 1440,
    index_keys: str | None = "time",
) -> DataFrame

Fetch rate (OHLC) data as a DataFrame.

Parameters:

Name Type Description Default
symbol str

Symbol to fetch data for.

required
granularity str

Time granularity as a timeframe suffix (e.g., "M1", "H1").

'M1'
count int

Number of bars to fetch.

1440
index_keys str | None

Optional index keys for the DataFrame.

'time'

Returns:

Type Description
DataFrame

pd.DataFrame: OHLC data with time index.

Raises:

Type Description
Mt5TradingError

If the granularity is not supported by MetaTrader5.

Source code in pdmt5/trading.py
def fetch_latest_rates_as_df(
    self,
    symbol: str,
    granularity: str = "M1",
    count: int = 1440,
    index_keys: str | None = "time",
) -> pd.DataFrame:
    """Fetch rate (OHLC) data as a DataFrame.

    Args:
        symbol: Symbol to fetch data for.
        granularity: Time granularity as a timeframe suffix (e.g., "M1", "H1").
        count: Number of bars to fetch.
        index_keys: Optional index keys for the DataFrame.

    Returns:
        pd.DataFrame: OHLC data with time index.

    Raises:
        Mt5TradingError: If the granularity is not supported by MetaTrader5.
    """
    try:
        timeframe = getattr(self.mt5, f"TIMEFRAME_{granularity.upper()}")
    except AttributeError as e:
        error_message = (
            f"MetaTrader5 does not support the given granularity: {granularity}"
        )
        raise Mt5TradingError(error_message) from e
    else:
        result = self.copy_rates_from_pos_as_df(
            symbol=symbol,
            timeframe=timeframe,
            start_pos=0,
            count=count,
            index_keys=index_keys,
        )
        self.logger.info(
            "Fetched latest %s rates for %s: %d rows",
            granularity,
            symbol,
            result.shape[0],
        )
        return result

fetch_latest_ticks_as_df

fetch_latest_ticks_as_df(
    symbol: str,
    seconds: int = 300,
    index_keys: str | None = "time_msc",
) -> DataFrame

Fetch tick data as a DataFrame.

Parameters:

Name Type Description Default
symbol str

Symbol to fetch tick data for.

required
seconds int

Time range in seconds to fetch ticks around the last tick time.

300
index_keys str | None

Optional index keys for the DataFrame.

'time_msc'

Returns:

Type Description
DataFrame

pd.DataFrame: Tick data with time index.

Source code in pdmt5/trading.py
def fetch_latest_ticks_as_df(
    self,
    symbol: str,
    seconds: int = 300,
    index_keys: str | None = "time_msc",
) -> pd.DataFrame:
    """Fetch tick data as a DataFrame.

    Args:
        symbol: Symbol to fetch tick data for.
        seconds: Time range in seconds to fetch ticks around the last tick time.
        index_keys: Optional index keys for the DataFrame.

    Returns:
        pd.DataFrame: Tick data with time index.
    """
    last_tick_time = self.symbol_info_tick_as_dict(symbol=symbol)["time"]
    result = self.copy_ticks_range_as_df(
        symbol=symbol,
        date_from=(last_tick_time - timedelta(seconds=seconds)),
        date_to=(last_tick_time + timedelta(seconds=seconds)),
        flags=self.mt5.COPY_TICKS_ALL,
        index_keys=index_keys,
    )
    self.logger.info(
        "Fetched latest ticks for %s: %d rows",
        symbol,
        result.shape[0],
    )
    return result

fetch_positions_with_metrics_as_df

fetch_positions_with_metrics_as_df(
    symbol: str,
) -> DataFrame

Fetch open positions as a DataFrame with additional metrics.

Parameters:

Name Type Description Default
symbol str

Symbol to fetch positions for.

required

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame containing open positions with additional metrics.

Source code in pdmt5/trading.py
def fetch_positions_with_metrics_as_df(
    self,
    symbol: str,
) -> pd.DataFrame:
    """Fetch open positions as a DataFrame with additional metrics.

    Args:
        symbol: Symbol to fetch positions for.

    Returns:
        pd.DataFrame: DataFrame containing open positions with additional metrics.
    """
    positions_df = self.positions_get_as_df(symbol=symbol)
    if positions_df.empty:
        result = positions_df
    else:
        symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
        ask_margin = self.order_calc_margin(
            action=self.mt5.ORDER_TYPE_BUY,
            symbol=symbol,
            volume=1,
            price=symbol_info_tick["ask"],
        )
        bid_margin = self.order_calc_margin(
            action=self.mt5.ORDER_TYPE_SELL,
            symbol=symbol,
            volume=1,
            price=symbol_info_tick["bid"],
        )
        result = (
            positions_df.assign(
                elapsed_seconds=lambda d: (
                    symbol_info_tick["time"] - d["time"]
                ).dt.total_seconds(),
                underlier_increase_ratio=lambda d: (
                    d["price_current"] / d["price_open"] - 1
                ),
                buy=lambda d: (d["type"] == self.mt5.POSITION_TYPE_BUY),
                sell=lambda d: (d["type"] == self.mt5.POSITION_TYPE_SELL),
            )
            .assign(
                buy_i=lambda d: d["buy"].astype(int),
                sell_i=lambda d: d["sell"].astype(int),
            )
            .assign(
                sign=lambda d: (d["buy_i"] - d["sell_i"]),
                margin=lambda d: (
                    (d["buy_i"] * ask_margin + d["sell_i"] * bid_margin)
                    * d["volume"]
                ),
            )
            .assign(
                signed_volume=lambda d: (d["volume"] * d["sign"]),
                signed_margin=lambda d: (d["margin"] * d["sign"]),
                underlier_profit_ratio=lambda d: (
                    d["underlier_increase_ratio"] * d["sign"]
                ),
            )
            .drop(columns=["buy_i", "sell_i", "sign", "underlier_increase_ratio"])
        )
    self.logger.info(
        "Fetched positions with metrics for %s: %d rows",
        symbol,
        result.shape[0],
    )
    return result

place_market_order

place_market_order(
    symbol: str,
    volume: float,
    order_side: Literal["BUY", "SELL"],
    order_filling_mode: Literal[
        "IOC", "FOK", "RETURN"
    ] = "IOC",
    order_time_mode: Literal[
        "GTC", "DAY", "SPECIFIED", "SPECIFIED_DAY"
    ] = "GTC",
    raise_on_error: bool = False,
    dry_run: bool = False,
    **kwargs: Any,
) -> dict[str, Any]

Send or check an order request to place a market order.

Parameters:

Name Type Description Default
symbol str

Symbol for the order.

required
volume float

Volume of the order.

required
order_side Literal['BUY', 'SELL']

Side of the order, either "BUY" or "SELL".

required
order_filling_mode Literal['IOC', 'FOK', 'RETURN']

Order filling mode, either "IOC", "FOK", or "RETURN".

'IOC'
order_time_mode Literal['GTC', 'DAY', 'SPECIFIED', 'SPECIFIED_DAY']

Order time mode, either "GTC", "DAY", "SPECIFIED", or "SPECIFIED_DAY".

'GTC'
raise_on_error bool

If True, raise an error on operation failure.

False
dry_run bool

If True, only check the order without sending it.

False
**kwargs Any

Additional keyword arguments for request parameters.

{}

Returns:

Type Description
dict[str, Any]

Dictionary with operation result.

Source code in pdmt5/trading.py
def place_market_order(
    self,
    symbol: str,
    volume: float,
    order_side: Literal["BUY", "SELL"],
    order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
    order_time_mode: Literal["GTC", "DAY", "SPECIFIED", "SPECIFIED_DAY"] = "GTC",
    raise_on_error: bool = False,
    dry_run: bool = False,
    **kwargs: Any,  # noqa: ANN401
) -> dict[str, Any]:
    """Send or check an order request to place a market order.

    Args:
        symbol: Symbol for the order.
        volume: Volume of the order.
        order_side: Side of the order, either "BUY" or "SELL".
        order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
        order_time_mode: Order time mode, either "GTC", "DAY", "SPECIFIED",
            or "SPECIFIED_DAY".
        raise_on_error: If True, raise an error on operation failure.
        dry_run: If True, only check the order without sending it.
        **kwargs: Additional keyword arguments for request parameters.

    Returns:
        Dictionary with operation result.
    """
    self.logger.info("Placing market order: %s %s %s", order_side, volume, symbol)
    return self._send_or_check_order(
        request={
            "action": self.mt5.TRADE_ACTION_DEAL,
            "symbol": symbol,
            "volume": volume,
            "type": getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
            "type_filling": getattr(
                self.mt5, f"ORDER_FILLING_{order_filling_mode.upper()}"
            ),
            "type_time": getattr(self.mt5, f"ORDER_TIME_{order_time_mode.upper()}"),
            **kwargs,
        },
        raise_on_error=raise_on_error,
        dry_run=dry_run,
    )

update_sltp_for_open_positions

update_sltp_for_open_positions(
    symbol: str,
    stop_loss: float | None = None,
    take_profit: float | None = None,
    tickets: list[int] | None = None,
    raise_on_error: bool = False,
    dry_run: bool = False,
    **kwargs: Any,
) -> list[dict[str, Any]]

Change Stop Loss and Take Profit for open positions.

Parameters:

Name Type Description Default
symbol str

Symbol for the position.

required
stop_loss float | None

New Stop Loss price. If None, it will not be changed.

None
take_profit float | None

New Take Profit price. If None, it will not be changed.

None
tickets list[int] | None

List of position tickets to filter positions. If None, all open positions for the symbol will be considered.

None
raise_on_error bool

If True, raise an error on operation failure.

False
dry_run bool

If True, only check the order without sending it.

False
**kwargs Any

Additional keyword arguments for request parameters.

{}

Returns:

Type Description
list[dict[str, Any]]

List of dictionaries with operation results for each updated position.

Source code in pdmt5/trading.py
def update_sltp_for_open_positions(
    self,
    symbol: str,
    stop_loss: float | None = None,
    take_profit: float | None = None,
    tickets: list[int] | None = None,
    raise_on_error: bool = False,
    dry_run: bool = False,
    **kwargs: Any,  # noqa: ANN401
) -> list[dict[str, Any]]:
    """Change Stop Loss and Take Profit for open positions.

    Args:
        symbol: Symbol for the position.
        stop_loss: New Stop Loss price. If None, it will not be changed.
        take_profit: New Take Profit price. If None, it will not be changed.
        tickets: List of position tickets to filter positions. If None, all open
            positions for the symbol will be considered.
        raise_on_error: If True, raise an error on operation failure.
        dry_run: If True, only check the order without sending it.
        **kwargs: Additional keyword arguments for request parameters.

    Returns:
        List of dictionaries with operation results for each updated position.
    """
    positions_df = self.positions_get_as_df(symbol=symbol)
    if positions_df.empty:
        self.logger.warning("No open positions found for symbol: %s", symbol)
        return []
    elif tickets:
        filtered_positions_df = positions_df.pipe(
            lambda d: d[d["ticket"].isin(tickets)]
        )
    else:
        filtered_positions_df = positions_df
    if filtered_positions_df.empty:
        self.logger.warning(
            "No open positions found for symbol: %s with specified tickets: %s",
            symbol,
            tickets,
        )
        return []
    else:
        symbol_info = self.symbol_info_as_dict(symbol=symbol)
        sl = round(stop_loss, symbol_info["digits"]) if stop_loss else None
        tp = round(take_profit, symbol_info["digits"]) if take_profit else None
        order_requests = [
            {
                "action": self.mt5.TRADE_ACTION_SLTP,
                "symbol": p["symbol"],
                "position": p["ticket"],
                "sl": (sl or p["sl"]),
                "tp": (tp or p["tp"]),
                **kwargs,
            }
            for _, p in filtered_positions_df.iterrows()
            if sl != p["sl"] or tp != p["tp"]
        ]
        if order_requests:
            self.logger.info(
                "Updating SL/TP for %d positions for %s: %s/%s",
                len(order_requests),
                symbol,
                sl,
                tp,
            )
            return [
                self._send_or_check_order(
                    request=r, raise_on_error=raise_on_error, dry_run=dry_run
                )
                for r in order_requests
            ]
        else:
            self.logger.info(
                "No positions to update for symbol: %s with SL: %s and TP: %s",
                symbol,
                sl,
                tp,
            )
            return []

options: show_bases: false

Advanced trading client class that inherits from Mt5DataClient and provides specialized trading functionality.

Mt5TradingError

pdmt5.trading.Mt5TradingError

Bases: Mt5RuntimeError

MetaTrader5 trading error.

options: show_bases: false

Custom runtime exception for trading-specific errors.

Usage Examples

Basic Trading Operations

import MetaTrader5 as mt5
from pdmt5 import Mt5TradingClient, Mt5Config

# Create configuration
config = Mt5Config(
    login=123456,
    password="your_password",
    server="broker_server",
    timeout=60000,
    portable=False
)

# Create client with dry run mode for testing
client = Mt5TradingClient(config=config, dry_run=True)

# Use as context manager
with client:
    # Get current positions as DataFrame
    positions_df = client.get_positions_as_df()
    print(f"Open positions: {len(positions_df)}")

    # Close positions for specific symbol
    results = client.close_open_positions("EURUSD")
    print(f"Closed positions: {results}")

Production Trading

# Create client for live trading (dry_run=False)
client = Mt5TradingClient(config=config, dry_run=False)

with client:
    # Close all positions for multiple symbols
    results = client.close_open_positions(["EURUSD", "GBPUSD", "USDJPY"])

    # Close all positions (all symbols)
    all_results = client.close_open_positions()

Order Filling Modes

with Mt5TradingClient(config=config) as client:
    # Use IOC (Immediate or Cancel) - default
    results_ioc = client.close_open_positions(
        symbols="EURUSD",
        order_filling_mode="IOC"
    )

    # Use FOK (Fill or Kill)
    results_fok = client.close_open_positions(
        symbols="GBPUSD",
        order_filling_mode="FOK"
    )

    # Use RETURN (Return if not filled)
    results_return = client.close_open_positions(
        symbols="USDJPY",
        order_filling_mode="RETURN"
    )

Custom Order Parameters

with client:
    # Close positions with custom parameters and order filling mode
    results = client.close_open_positions(
        "EURUSD",
        order_filling_mode="IOC",  # Specify per method call
        comment="Closing all EURUSD positions",
        deviation=10  # Maximum price deviation
    )

Error Handling

from pdmt5.trading import Mt5TradingError

try:
    with client:
        results = client.close_open_positions("EURUSD")
except Mt5TradingError as e:
    print(f"Trading error: {e}")
    # Handle specific trading errors

Checking Order Status

with client:
    # Check order (note: send_or_check_order is an internal method)
    # For trading operations, use the provided methods like close_open_positions

    # Example: Check if we can close a position
    positions = client.get_positions_as_df()
    if not positions.empty:
        # Close specific position
        results = client.close_open_positions("EURUSD")

Position Management Features

The Mt5TradingClient provides intelligent position management:

  • Automatic Position Reversal: Automatically determines the correct order type to close positions
  • Batch Operations: Close multiple positions for one or more symbols
  • Dry Run Support: Test trading logic without executing real trades
  • Flexible Filtering: Close positions by symbol, group, or all positions
  • Custom Parameters: Support for additional order parameters like comment, deviation, etc.

Dry Run Mode

Dry run mode is essential for testing trading strategies:

# Test mode - validates orders without execution
test_client = Mt5TradingClient(config=config, dry_run=True)

# Production mode - executes real orders
prod_client = Mt5TradingClient(config=config, dry_run=False)

In dry run mode:

  • Orders are validated using order_check() instead of order_send()
  • No actual trades are executed
  • Full validation of margin requirements and order parameters
  • Same return structure as live trading for easy testing

Return Values

The close_open_positions() method returns a dictionary with symbols as keys:

{
    "EURUSD": [
        {
            "retcode": 10009,  # Trade done
            "deal": 123456,
            "order": 789012,
            "volume": 1.0,
            "price": 1.1000,
            "comment": "Request executed",
            ...
        }
    ],
    "GBPUSD": [...]
}

Best Practices

  1. Always use dry run mode first to test your trading logic
  2. Handle Mt5TradingError exceptions for proper error management
  3. Check return codes to verify successful execution
  4. Use context managers for automatic connection handling
  5. Log trading operations for audit trails
  6. Validate positions exist before attempting to close them
  7. Consider market hours and trading session times

Common Return Codes

  • TRADE_RETCODE_DONE (10009): Trade operation completed successfully
  • TRADE_RETCODE_TRADE_DISABLED: Trading disabled for the account
  • TRADE_RETCODE_MARKET_CLOSED: Market is closed
  • TRADE_RETCODE_NO_MONEY: Insufficient funds
  • TRADE_RETCODE_INVALID_VOLUME: Invalid trade volume

Margin Calculation Methods

The trading client provides advanced margin calculation capabilities:

Calculate Minimum Order Margin

with client:
    # Calculate minimum margin required for BUY order
    min_margin_buy = client.calculate_minimum_order_margin("EURUSD", "BUY")
    print(f"Minimum volume: {min_margin_buy['volume']}")
    print(f"Minimum margin: {min_margin_buy['margin']}")

    # Calculate minimum margin required for SELL order
    min_margin_sell = client.calculate_minimum_order_margin("EURUSD", "SELL")

Calculate Volume by Margin

with client:
    # Calculate maximum volume for given margin amount
    available_margin = 1000.0  # USD
    max_volume_buy = client.calculate_volume_by_margin("EURUSD", available_margin, "BUY")
    max_volume_sell = client.calculate_volume_by_margin("EURUSD", available_margin, "SELL")

    print(f"Max BUY volume for ${available_margin}: {max_volume_buy}")
    print(f"Max SELL volume for ${available_margin}: {max_volume_sell}")

Calculate New Position Margin Ratio

with client:
    # Calculate margin ratio for potential new position
    margin_ratio = client.calculate_new_position_margin_ratio(
        symbol="EURUSD",
        new_position_side="BUY",
        new_position_volume=1.0
    )
    print(f"New position would use {margin_ratio:.2%} of account equity")

    # Check if adding position would exceed risk limits
    if margin_ratio > 0.1:  # 10% risk limit
        print("Position size too large for risk management")

Market Order Placement

Place market orders with flexible configuration:

with client:
    # Place a BUY market order
    result = client.place_market_order(
        symbol="EURUSD",
        volume=1.0,
        order_side="BUY",
        order_filling_mode="IOC",  # Immediate or Cancel
        order_time_mode="GTC",     # Good Till Cancelled
        dry_run=False,             # Set to True for testing
        comment="My buy order"
    )

    # Place a SELL market order with FOK filling
    result = client.place_market_order(
        symbol="EURUSD",
        volume=0.5,
        order_side="SELL",
        order_filling_mode="FOK",  # Fill or Kill
        dry_run=True  # Test mode
    )

    print(f"Order result: {result}")

Stop Loss and Take Profit Management

Update SL/TP for existing positions:

with client:
    # Update SL/TP for all EURUSD positions
    results = client.update_sltp_for_open_positions(
        symbol="EURUSD",
        stop_loss=1.0950,
        take_profit=1.1100,
        dry_run=False
    )

    # Update only specific positions by ticket
    results = client.update_sltp_for_open_positions(
        symbol="EURUSD",
        stop_loss=1.0950,
        tickets=[123456, 789012],  # Specific position tickets
        dry_run=True
    )

Market Data and Analysis Methods

Spread Analysis

with client:
    # Calculate spread ratio for symbol
    spread_ratio = client.calculate_spread_ratio("EURUSD")
    print(f"EURUSD spread ratio: {spread_ratio:.6f}")

OHLC Data Retrieval

with client:
    # Fetch latest rate data as DataFrame
    rates_df = client.fetch_latest_rates_as_df(
        symbol="EURUSD",
        granularity="M1",  # 1-minute bars
        count=1440,        # Last 24 hours
        index_keys="time"
    )
    print(f"Latest rates: {rates_df.tail()}")

Tick Data Analysis

with client:
    # Fetch recent tick data
    ticks_df = client.fetch_latest_ticks_as_df(
        symbol="EURUSD",
        seconds=300,           # Last 5 minutes
        index_keys="time_msc"
    )
    print(f"Tick count: {len(ticks_df)}")

Position Analytics with Enhanced Metrics

with client:
    # Get positions with additional calculated metrics
    positions_df = client.fetch_positions_with_metrics_as_df("EURUSD")

    if not positions_df.empty:
        print("Position metrics:")
        print(f"Total signed volume: {positions_df['signed_volume'].sum()}")
        print(f"Total signed margin: {positions_df['signed_margin'].sum()}")
        print(f"Average profit ratio: {positions_df['underlier_profit_ratio'].mean():.4f}")

Deal History Analysis

with client:
    # Collect entry deals for analysis
    deals_df = client.collect_entry_deals_as_df(
        symbol="EURUSD",
        history_seconds=3600,  # Last hour
        index_keys="ticket"
    )

    if not deals_df.empty:
        print(f"Entry deals found: {len(deals_df)}")
        print(f"Deal types: {deals_df['type'].value_counts()}")

Integration with Mt5DataClient

Since Mt5TradingClient inherits from Mt5DataClient, all data retrieval methods are available:

with Mt5TradingClient(config=config) as client:
    # Get current positions as DataFrame
    positions_df = client.get_positions_as_df()

    # Analyze positions
    if not positions_df.empty:
        # Calculate total exposure
        total_volume = positions_df['volume'].sum()

        # Close losing positions
        losing_positions = positions_df[positions_df['profit'] < 0]
        for symbol in losing_positions['symbol'].unique():
            client.close_open_positions(symbol)

    # Risk management with margin calculations
    for symbol in ["EURUSD", "GBPUSD", "USDJPY"]:
        # Calculate current margin usage
        current_ratio = client.calculate_new_position_margin_ratio(symbol)
        print(f"{symbol} current margin ratio: {current_ratio:.2%}")

        # Calculate maximum safe position size
        safe_margin = 500.0  # USD
        max_safe_volume = client.calculate_volume_by_margin(symbol, safe_margin, "BUY")
        print(f"{symbol} max safe volume: {max_safe_volume}")