Skip to content

bbstrader.metatrader

The Python side of the C++/Python bridge to MetaTrader 5: account management, order/trade helpers, risk management, symbol rates, and the copy-trading engine (CLI and desktop GUI).

metatrader

Overview

This MetaTrader Module provides a direct interface to the MetaTrader 5 trading platform, enabling seamless integration of Python-based trading strategies with a live trading environment. It offers a comprehensive set of tools for account management, trade execution, market data retrieval, and risk management, all tailored for the MetaTrader 5 platform.

Features

  • Direct MetaTrader 5 Integration: Connects to the MetaTrader 5 terminal to access its full range of trading functionalities.
  • Account and Trade Management: Provides tools for querying account information, managing open positions, and executing trades.
  • Market Data Retrieval: Fetches historical and real-time market data, including rates and ticks, directly from MetaTrader 5.
  • Risk Management: Includes utilities for managing risk, such as setting stop-loss and take-profit levels.
  • Trade Copying: Functionality to copy trades between different MetaTrader 5 accounts.

Components

  • Account: Manages account information, including balance, equity, and margin.
  • Broker: Handles the connection to the MetaTrader 5 terminal.
  • Copier: Copies trades between accounts.
  • Rates: Retrieves historical and current market rates.
  • Risk: Provides risk management functionalities.
  • Trade: Manages trade execution and position management.
  • Utils: Contains utility functions for the MetaTrader module.

Examples

from bbstrader.metatrader import Account account = Account() print(account.get_account_info())

Notes

This module requires the MetaTrader 5 terminal to be installed and running.

Account

Account(broker: Broker | None = None, **kwargs)

The Account class is utilized to retrieve information about the current trading account or a specific account. It enables interaction with the MT5 terminal to manage account details, including account informations, terminal status, financial instrument details, active orders, open positions, and trading history.

Example

Instantiating the Account class

account = Account()

Getting account information

account_info = account.get_account_info()

Getting terminal information

terminal_info = account.get_terminal_info()

Getting active orders

orders = account.get_orders()

Fetching open positions

positions = account.get_positions()

Accessing trade history

from_date = datetime(2020, 1, 1) to_date = datetime.now() trade_history = account.get_trade_history(from_date, to_date)

Initialize the Account class.

See bbstrader.metatrader.broker.check_mt5_connection() for more details on how to connect to MT5 terminal.

Source code in src/bbstrader/metatrader/account.py
def __init__(self, broker: Broker | None = None, **kwargs):
    """
    Initialize the Account class.

    See `bbstrader.metatrader.broker.check_mt5_connection()`
    for more details on how to connect to MT5 terminal.

    """
    check_mt5_connection(**kwargs)
    self._info = client.account_info()
    self._symbol_cache: dict[str, SymbolInfo] = {}
    terminal_info = self.get_terminal_info()
    self._broker = (
        broker
        if broker is not None
        else Broker(terminal_info.company if terminal_info else "Unknown")
    )

server property

server: str

The name of the trade server to which the client terminal is connected. (e.g., 'AdmiralsGroup-Demo')

shutdown

shutdown()

Close the connection to the MetaTrader 5 terminal.

Source code in src/bbstrader/metatrader/account.py
def shutdown(self):
    """Close the connection to the MetaTrader 5 terminal."""
    client.shutdown()

refresh

refresh() -> None

Reload account info from the MT5 terminal to reflect current balance/equity.

Source code in src/bbstrader/metatrader/account.py
def refresh(self) -> None:
    """Reload account info from the MT5 terminal to reflect current balance/equity."""
    self._info = client.account_info()

clear_symbol_cache

clear_symbol_cache() -> None

Invalidate the cached symbol info, forcing fresh lookups on next call.

Source code in src/bbstrader/metatrader/account.py
def clear_symbol_cache(self) -> None:
    """Invalidate the cached symbol info, forcing fresh lookups on next call."""
    self._symbol_cache.clear()

get_account_info

get_account_info(account: int | None = None, password: str | None = None, server: str | None = None, timeout: int | None = _DEFAULT_TIMEOUT, path: str | None = None) -> AccountInfo | None

Get info on the current trading account or a specific account .

Parameters:

Name Type Description Default
account (int, optinal)

MT5 Trading account number.

required
password (str, optinal)

MT5 Trading account password.

None
server (str, optinal)

MT5 Trading account server [Brokers or terminal server ["demo", "real"]] If no server is set, the last used server is applied automaticall

None
timeout (int, optinal)

Connection timeout in milliseconds. Optional named parameter. If not specified, the value of 60 000 (60 seconds) is applied. If the connection is not established within the specified time, the call is forcibly terminated and the exception is generated.

_DEFAULT_TIMEOUT
path str

The path to the MetaTrader 5 terminal executable file. Defaults to None (e.g., "C:/Program Files/MetaTrader 5/terminal64.exe").

None

Returns: - AccountInfo - None in case of an error

Raises:

Type Description
MT5TerminalError

A specific exception based on the error code.

Source code in src/bbstrader/metatrader/account.py
def get_account_info(
    self,
    account: int | None = None,
    password: str | None = None,
    server: str | None = None,
    timeout: int | None = _DEFAULT_TIMEOUT,
    path: str | None = None,
) -> AccountInfo | None:
    """
    Get info on the current trading account or a specific account .

    Args:
        account (int, optinal) : MT5 Trading account number.
        password (str, optinal): MT5 Trading account password.

        server (str, optinal): MT5 Trading account server
            [Brokers or terminal server ["demo", "real"]]
            If no server is set, the last used server is applied automaticall

        timeout (int, optinal):
             Connection timeout in milliseconds. Optional named parameter.
             If not specified, the value of 60 000 (60 seconds) is applied.
             If the connection is not established within the specified time,
             the call is forcibly terminated and the exception is generated.
        path (str, optional): The path to the MetaTrader 5 terminal executable file.
            Defaults to None (e.g., "C:/Program Files/MetaTrader 5/terminal64.exe").

    Returns:
    -   AccountInfo
    -   None in case of an error

    Raises:
        MT5TerminalError: A specific exception based on the error code.
    """
    # connect to the trade account specifying a password and a server
    if account is not None and password is not None and server is not None:
        try:
            if path is not None:
                self.broker.initialize_connection(
                    path=path,
                    login=account,
                    password=password,
                    server=server,
                    timeout=timeout,
                )
            authorized = client.login(
                account, password=password, server=server, timeout=timeout
            )
            if not authorized:
                raise_mt5_error(f"Failed to connect to account #{account}")
            info = client.account_info()
            return info
        except Exception as e:
            raise_mt5_error(str(e))
    else:
        try:
            return client.account_info()
        except Exception as e:
            raise_mt5_error(str(e))

get_terminal_info

get_terminal_info() -> TerminalInfo | None

Get the connected MetaTrader 5 client terminal status and settings.

Returns: - TerminalInfo - None in case of an error

Raises:

Type Description
MT5TerminalError

A specific exception based on the error code.

Source code in src/bbstrader/metatrader/account.py
def get_terminal_info(self) -> TerminalInfo | None:
    """
    Get the connected MetaTrader 5 client terminal status and settings.

    Returns:
    -   TerminalInfo
    -   None in case of an error

    Raises:
        MT5TerminalError: A specific exception based on the error code.
    """
    try:
        terminal_info = client.terminal_info()
        if terminal_info is None:
            return None
    except Exception as e:
        raise_mt5_error(str(e))
    return terminal_info

get_symbol_info

get_symbol_info(symbol: str) -> SymbolInfo | None

Get symbol properties

Parameters:

Name Type Description Default
symbol str

Symbol name

required

Returns: - SymbolInfo. - None in case of an error.

Raises:

Type Description
MT5TerminalError

A specific exception based on the error code.

Source code in src/bbstrader/metatrader/account.py
def get_symbol_info(self, symbol: str) -> SymbolInfo | None:
    """Get symbol properties

    Args:
        symbol (str): Symbol name

    Returns:
    -   SymbolInfo.
    -   None in case of an error.

    Raises:
        MT5TerminalError: A specific exception based on the error code.

    """
    if symbol in self._symbol_cache:
        return self._symbol_cache[symbol]
    try:
        symbol_info = client.symbol_info(symbol)
        if symbol_info is None:
            return None
        self._symbol_cache[symbol] = symbol_info
        return symbol_info
    except Exception as e:
        msg = self._symbol_info_msg(symbol)
        raise_mt5_error(message=f"{str(e)} {msg}")

get_tick_info

get_tick_info(symbol: str) -> TickInfo | None

Get symbol tick properties

Parameters:

Name Type Description Default
symbol str

Symbol name

required

Returns: - TickInfo. - None in case of an error.

Raises:

Type Description
MT5TerminalError

A specific exception based on the error code.

Source code in src/bbstrader/metatrader/account.py
def get_tick_info(self, symbol: str) -> TickInfo | None:
    """Get symbol tick properties

    Args:
        symbol (str): Symbol name

    Returns:
    -   TickInfo.
    -   None in case of an error.

    Raises:
        MT5TerminalError: A specific exception based on the error code.

    """
    try:
        tick_info = client.symbol_info_tick(symbol)
        if tick_info is None:
            return None
        else:
            return tick_info
    except Exception as e:
        msg = self._symbol_info_msg(symbol)
        raise_mt5_error(message=f"{str(e)} {msg}")

get_currency_rates

get_currency_rates(symbol: str) -> dict[str, str]

Parameters:

Name Type Description Default
symbol str

The symbol for which to get currencies

required

Returns:

Type Description
dict[str, str]
  • base currency (bc)
dict[str, str]
  • margin currency (mc)
dict[str, str]
  • profit currency (pc)
dict[str, str]
  • account currency (ac)
Exemple

account = Account() account.get_currency_rates('EURUSD')

Source code in src/bbstrader/metatrader/account.py
def get_currency_rates(self, symbol: str) -> dict[str, str]:
    """
    Args:
        symbol (str): The symbol for which to get currencies

    Returns:
        - `base currency` (bc)
        - `margin currency` (mc)
        - `profit currency` (pc)
        - `account currency` (ac)

    Exemple:
        >>> account =  Account()
        >>> account.get_currency_rates('EURUSD')
        {'bc': 'EUR', 'mc': 'EUR', 'pc': 'USD', 'ac': 'USD'}
    """
    info = self.get_symbol_info(symbol)
    if info is None:
        raise_mt5_error(f"Symbol '{symbol}' not found in Market Watch")
    bc = info.currency_base
    pc = info.currency_profit
    mc = info.currency_margin
    ac = self._info.currency
    return {"bc": bc, "mc": mc, "pc": pc, "ac": ac}

get_symbols

get_symbols(symbol_type: SymbolType | str = 'ALL', check_etf=False, save=False, file_name='symbols', include_desc=False, display_total=False) -> list[str]

Get all specified financial instruments from the MetaTrader 5 terminal.

Parameters:

Name Type Description Default
symbol_type SymbolType | str

The type of financial instruments to retrieve.

'ALL'
- `ALL`

For all available symbols

required
check_etf bool

If True and symbol_type is 'etf', check if the ETF description contains 'ETF'.

False
save bool

If True, save the symbols to a file.

False
file_name str

The name of the file to save the symbols to (without the extension).

'symbols'
include_desc bool

If True, include the symbol's description in the output and saved file.

False

Returns:

Name Type Description
list list[str]

A list of symbols.

Raises:

Type Description
Exception

If there is an error connecting to MT5 or retrieving symbols.

Source code in src/bbstrader/metatrader/account.py
def get_symbols(
    self,
    symbol_type: SymbolType | str = "ALL",
    check_etf=False,
    save=False,
    file_name="symbols",
    include_desc=False,
    display_total=False,
) -> list[str]:
    """
    Get all specified financial instruments from the MetaTrader 5 terminal.

    Args:
        symbol_type (SymbolType | str): The type of financial instruments to retrieve.
        - `ALL`: For all available symbols
        - See `bbstrader.metatrader.utils.SymbolType` for more details.

        check_etf (bool): If True and symbol_type is 'etf', check if the
            ETF description contains 'ETF'.

        save (bool): If True, save the symbols to a file.

        file_name (str): The name of the file to save the symbols to
            (without the extension).

        include_desc (bool): If True, include the symbol's description
            in the output and saved file.

    Returns:
        list: A list of symbols.

    Raises:
        Exception: If there is an error connecting to MT5 or retrieving symbols.
    """
    return self.broker.get_symbols(
        symbol_type=symbol_type,
        check_etf=check_etf,
        save=save,
        file_name=file_name,
        include_desc=include_desc,
        display_total=display_total,
    )

get_symbol_type

get_symbol_type(symbol: str) -> SymbolType

Determines the type of a given financial instrument symbol.

Parameters:

Name Type Description Default
symbol str

The symbol of the financial instrument (e.g., GOOGL, EURUSD).

required

Returns:

Name Type Description
SymbolType SymbolType

The type of the financial instrument, one of the following:

SymbolType
  • SymbolType.ETFs
SymbolType
  • SymbolType.BONDS
SymbolType
  • SymbolType.FOREX
SymbolType
  • SymbolType.FUTURES
SymbolType
  • SymbolType.STOCKS
SymbolType
  • SymbolType.INDICES
SymbolType
  • SymbolType.COMMODITIES
SymbolType
  • SymbolType.CRYPTO
  • SymbolType.unknown if the type cannot be determined.
Source code in src/bbstrader/metatrader/account.py
def get_symbol_type(self, symbol: str) -> SymbolType:
    """
    Determines the type of a given financial instrument symbol.

    Args:
        symbol (str): The symbol of the financial instrument (e.g., `GOOGL`, `EURUSD`).

    Returns:
        SymbolType: The type of the financial instrument, one of the following:
        - `SymbolType.ETFs`
        - `SymbolType.BONDS`
        - `SymbolType.FOREX`
        - `SymbolType.FUTURES`
        - `SymbolType.STOCKS`
        - `SymbolType.INDICES`
        - `SymbolType.COMMODITIES`
        - `SymbolType.CRYPTO`
    - `SymbolType.unknown` if the type cannot be determined.

    """
    return self.broker.get_symbol_type(symbol)

get_stocks_from_country

get_stocks_from_country(country_code: str = 'USA', etf=False) -> list[str]

Retrieves a list of stock symbols from a specific country.

Supported countries are
  • Australia: AUS
  • Belgium: BEL
  • Denmark: DNK
  • Finland: FIN
  • France: FRA
  • Germany: DEU
  • Netherlands: NLD
  • Norway: NOR
  • Portugal: PRT
  • Spain: ESP
  • Sweden: SWE
  • United Kingdom: GBR
  • United States: USA
  • Switzerland: CHE
  • Hong Kong: HKG
  • Ireland: IRL
  • Austria: AUT

Parameters:

Name Type Description Default
country str

The country code of stocks to retrieve. Defaults to 'USA'.

required

Returns:

Name Type Description
list list[str]

A list of stock symbol names from the specified country.

Raises:

Type Description
ValueError

If an unsupported country is provided.

Notes

This mthods works primarly with brokers who specify the stock symbols type and exchanges, For other brokers use get_symbols() or this method will use it by default.

Source code in src/bbstrader/metatrader/account.py
def get_stocks_from_country(
    self, country_code: str = "USA", etf=False
) -> list[str]:
    """
    Retrieves a list of stock symbols from a specific country.

    Supported countries are:
        * **Australia:** AUS
        * **Belgium:** BEL
        * **Denmark:** DNK
        * **Finland:** FIN
        * **France:** FRA
        * **Germany:** DEU
        * **Netherlands:** NLD
        * **Norway:** NOR
        * **Portugal:** PRT
        * **Spain:** ESP
        * **Sweden:** SWE
        * **United Kingdom:** GBR
        * **United States:** USA
        * **Switzerland:** CHE
        * **Hong Kong:** HKG
        * **Ireland:** IRL
        * **Austria:** AUT

    Args:
        country (str, optional): The country code of stocks to retrieve.
                                Defaults to 'USA'.

    Returns:
        list: A list of stock symbol names from the specified country.

    Raises:
        ValueError: If an unsupported country is provided.

    Notes:
        This mthods works primarly with brokers who specify the stock symbols type and exchanges,
        For other brokers use `get_symbols()` or this method will use it by default.
    """
    stocks = self._get_symbols_by_category(
        SymbolType.STOCKS, country_code, self.broker.countries_stocks
    )
    etfs = (
        self._get_symbols_by_category(
            SymbolType.ETFs, country_code, self.broker.countries_stocks
        )
        if etf
        else []
    )
    if not stocks and not etfs:
        stocks = self.get_symbols(symbol_type=SymbolType.STOCKS)
        etfs = self.get_symbols(symbol_type=SymbolType.ETFs) if etf else []
    return stocks + etfs

get_stocks_from_exchange

get_stocks_from_exchange(exchange_code: str = 'XNYS', etf=True) -> list[str]

Get stock symbols from a specific exchange using the ISO Code for the exchange.

Supported exchanges are from Admirals Group AS products: * XASX: Australian Securities Exchange * XBRU: Euronext Brussels Exchange * XCSE: Copenhagen Stock Exchange * XHEL: NASDAQ OMX Helsinki * XPAR: Euronext Paris * XETR: Xetra Frankfurt * XOSL: Oslo Stock Exchange * XLIS: Euronext Lisbon * XMAD: Bolsa de Madrid * XSTO: NASDAQ OMX Stockholm * XLON: London Stock Exchange * NYSE: New York Stock Exchange * ARCA: NYSE ARCA * AMEX: NYSE AMEX * XNYS: New York Stock Exchange (AMEX, ARCA, NYSE) * NASDAQ: NASDAQ * BATS: BATS Exchange * XSWX: SWX Swiss Exchange * XAMS: Euronext Amsterdam

Parameters:

Name Type Description Default
exchange_code str

The ISO code of the exchange.

'XNYS'
etf bool

If True, include ETFs from the exchange. Defaults to True.

True

Returns:

Name Type Description
list list[str]

A list of stock symbol names from the specified exchange.

Raises:

Type Description
ValueError

If an unsupported exchange is provided.

Notes

This mthods works primarly with brokers who specify the stock symbols type and exchanges, For other brokers use get_symbols() or this method will use it by default.

Source code in src/bbstrader/metatrader/account.py
def get_stocks_from_exchange(
    self, exchange_code: str = "XNYS", etf=True
) -> list[str]:
    """
    Get stock symbols from a specific exchange using the ISO Code for the exchange.

    Supported exchanges are from Admirals Group AS products:
    * **XASX:**        **Australian Securities Exchange**
    * **XBRU:**        **Euronext Brussels Exchange**
    * **XCSE:**        **Copenhagen Stock Exchange**
    * **XHEL:**        **NASDAQ OMX Helsinki**
    * **XPAR:**        **Euronext Paris**
    * **XETR:**        **Xetra Frankfurt**
    * **XOSL:**        **Oslo Stock Exchange**
    * **XLIS:**        **Euronext Lisbon**
    * **XMAD:**        **Bolsa de Madrid**
    * **XSTO:**        **NASDAQ OMX Stockholm**
    * **XLON:**        **London Stock Exchange**
    * **NYSE:**        **New York Stock Exchange**
    * **ARCA:**        **NYSE ARCA**
    * **AMEX:**        **NYSE AMEX**
    * **XNYS:**        **New York Stock Exchange (AMEX, ARCA, NYSE)**
    * **NASDAQ:**      **NASDAQ**
    * **BATS:**        **BATS Exchange**
    * **XSWX:**        **SWX Swiss Exchange**
    * **XAMS:**        **Euronext Amsterdam**

    Args:
        exchange_code (str, optional): The ISO code of the exchange.
        etf (bool, optional): If True, include ETFs from the exchange. Defaults to True.

    Returns:
        list: A list of stock symbol names from the specified exchange.

    Raises:
        ValueError: If an unsupported exchange is provided.

    Notes:
        This mthods works primarly with brokers who specify the stock symbols type and exchanges,
        For other brokers use `get_symbols()` or this method will use it by default.
    """
    stocks = self._get_symbols_by_category(
        SymbolType.STOCKS, exchange_code, self.broker.exchanges
    )
    etfs = (
        self._get_symbols_by_category(
            SymbolType.ETFs, exchange_code, self.broker.exchanges
        )
        if etf
        else []
    )
    if not stocks and not etfs:
        stocks = self.get_symbols(symbol_type=SymbolType.STOCKS)
        etfs = self.get_symbols(symbol_type=SymbolType.ETFs) if etf else []
    return stocks + etfs

get_rate_info

get_rate_info(symbol: str, timeframe: str = '1m') -> RateInfo | None

Get the most recent bar for a specified symbol and timeframe.

Parameters:

Name Type Description Default
symbol str

The symbol for which to get the rate information.

required
timeframe str

The timeframe for the rate information. Default is '1m'. See bbstrader.metatrader.utils.TIMEFRAMES for supported timeframes.

'1m'

Returns: RateInfo: The most recent bar as a RateInfo named tuple. None: If no rates are found or an error occurs. Raises: MT5TerminalError: A specific exception based on the error code.

Source code in src/bbstrader/metatrader/account.py
def get_rate_info(self, symbol: str, timeframe: str = "1m") -> RateInfo | None:
    """Get the most recent bar for a specified symbol and timeframe.

    Args:
        symbol (str): The symbol for which to get the rate information.
        timeframe (str): The timeframe for the rate information. Default is '1m'.
                        See ``bbstrader.metatrader.utils.TIMEFRAMES`` for supported timeframes.
    Returns:
        RateInfo: The most recent bar as a RateInfo named tuple.
        None: If no rates are found or an error occurs.
    Raises:
        MT5TerminalError: A specific exception based on the error code.
    """
    rates = client.copy_rates_from_pos(symbol, TIMEFRAMES[timeframe], 0, 1)
    if rates is None or len(rates) == 0:
        return None
    rate = rates[0]
    return RateInfo(*rate)

get_positions

get_positions(symbol: str | None = None, group: str | None = None, ticket: int | None = None) -> list[TradePosition] | None

Get open positions with the ability to filter by symbol or ticket. There are four call options:

  • Call without parameters. Returns open positions for all symbols.
  • Call specifying a symbol. Returns open positions for the specified symbol.
  • Call specifying a group of symbols. Returns open positions for the specified group of symbols.
  • Call specifying a position ticket. Returns the position corresponding to the specified ticket.

Parameters:

Name Type Description Default
symbol Optional[str]

Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored.

None
group Optional[str]

The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only positions meeting specified criteria for a symbol name.

None
ticket Optional[int]

Position ticket. Optional named parameter. A unique number assigned to each newly opened position. It usually matches the ticket of the order used to open the position, except when the ticket is changed as a result of service operations on the server, for example, when charging swaps with position re-opening.

None

Returns:

Type Description
list[TradePosition] | None

list[TradePosition] | None:

list[TradePosition] | None
  • List of TradePosition.
Notes

The method allows receiving all open positions within a specified period.

The group parameter may contain several comma-separated conditions.

A condition can be set as a mask using '*'.

The logical negation symbol '!' can be used for exclusion.

All conditions are applied sequentially, which means conditions for inclusion in a group should be specified first, followed by an exclusion condition.

For example, group="*, !EUR" means that deals for all symbols should be selected first, and those containing "EUR" in symbol names should be excluded afterward.

Source code in src/bbstrader/metatrader/account.py
def get_positions(
    self,
    symbol: str | None = None,
    group: str | None = None,
    ticket: int | None = None,
) -> list[TradePosition] | None:
    """
    Get open positions with the ability to filter by symbol or ticket.
    There are four call options:

    - Call without parameters. Returns open positions for all symbols.
    - Call specifying a symbol. Returns open positions for the specified symbol.
    - Call specifying a group of symbols. Returns open positions for the specified group of symbols.
    - Call specifying a position ticket. Returns the position corresponding to the specified ticket.

    Args:
        symbol (Optional[str]): Symbol name. Optional named parameter.
            If a symbol is specified, the `ticket` parameter is ignored.

        group (Optional[str]): The filter for arranging a group of necessary symbols.
            Optional named parameter. If the group is specified,
            the function returns only positions meeting specified criteria
            for a symbol name.

        ticket (Optional[int]): Position ticket. Optional named parameter.
            A unique number assigned to each newly opened position.
            It usually matches the ticket of the order used to open the position,
            except when the ticket is changed as a result of service operations on the server,
            for example, when charging swaps with position re-opening.


    Returns:
        list[TradePosition] | None:
        - List of `TradePosition`.

    Notes:
        The method allows receiving all open positions within a specified period.

        The `group` parameter may contain several comma-separated conditions.

        A condition can be set as a mask using '*'.

        The logical negation symbol '!' can be used for exclusion.

        All conditions are applied sequentially, which means conditions for inclusion
        in a group should be specified first, followed by an exclusion condition.

        For example, `group="*, !EUR"` means that deals for all symbols should be selected first,
        and those containing "EUR" in symbol names should be excluded afterward.
    """

    if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
        raise ValueError(
            "Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them."
        )

    if symbol is not None:
        positions = client.positions_get(symbol)
    elif group is not None:
        positions = client.positions_get_by_group(group)
    elif ticket is not None:
        positions = client.position_get_by_ticket(ticket)
    else:
        positions = client.positions_get()

    if positions is None:
        return None
    if isinstance(positions, TradePosition):
        return [positions]
    if len(positions) == 0:
        return None

    return positions

get_orders

get_orders(symbol: str | None = None, group: str | None = None, ticket: int | None = None) -> list[TradeOrder] | None

Get active orders with the ability to filter by symbol or ticket. There are four call options:

  • Call without parameters. Returns open positions for all symbols.
  • Call specifying a symbol, open positions should be received for.
  • Call specifying a group of symbols, open positions should be received for.
  • Call specifying a position ticket.

Parameters:

Name Type Description Default
symbol Optional[str]

Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored.

None
group Optional[str]

The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only positions meeting a specified criteria for a symbol name.

None
ticket Optional[int]

Order ticket. Optional named parameter. Unique number assigned to each order.

None
to_df bool

If True, a DataFrame is returned.

required

Returns:

Type Description
list[TradeOrder] | None

[List[TradeOrder] | None]:

list[TradeOrder] | None
  • List of TradeOrder .
Notes

The method allows receiving all history orders within a specified period. The group parameter may contain several comma-separated conditions. A condition can be set as a mask using '*'.

The logical negation symbol '!' can be used for exclusion. All conditions are applied sequentially, which means conditions for inclusion in a group should be specified first, followed by an exclusion condition.

For example, group="*, !EUR" means that deals for all symbols should be selected first and the ones containing "EUR" in symbol names should be excluded afterward.

Source code in src/bbstrader/metatrader/account.py
def get_orders(
    self,
    symbol: str | None = None,
    group: str | None = None,
    ticket: int | None = None,
) -> list[TradeOrder] | None:
    """
    Get active orders with the ability to filter by symbol or ticket.
    There are four call options:

    - Call without parameters. Returns open positions for all symbols.
    - Call specifying a symbol, open positions should be received for.
    - Call specifying a group of symbols, open positions should be received for.
    - Call specifying a position ticket.

    Args:
        symbol (Optional[str]): Symbol name. Optional named parameter.
            If a symbol is specified, the ticket parameter is ignored.

        group (Optional[str]): The filter for arranging a group of necessary symbols.
            Optional named parameter. If the group is specified,
            the function returns only positions meeting a specified criteria
            for a symbol name.

        ticket (Optional[int]): Order ticket. Optional named parameter.
            Unique number assigned to each order.

        to_df (bool): If True, a DataFrame is returned.

    Returns:
        [List[TradeOrder] | None]:
        - List of `TradeOrder` .

    Notes:
        The method allows receiving all history orders within a specified period.
        The `group` parameter may contain several comma-separated conditions.
        A condition can be set as a mask using '*'.

        The logical negation symbol '!' can be used for exclusion.
        All conditions are applied sequentially, which means conditions for inclusion
        in a group should be specified first, followed by an exclusion condition.

        For example, `group="*, !EUR"` means that deals for all symbols should be selected first
        and the ones containing "EUR" in symbol names should be excluded afterward.
    """

    if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
        raise ValueError(
            "Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them."
        )

    orders = None
    if symbol is not None:
        orders = client.orders_get(symbol)
    elif group is not None:
        orders = client.orders_get_by_group(group)
    elif ticket is not None:
        orders = client.order_get_by_ticket(ticket)
    else:
        orders = client.orders_get()

    if orders is None or len(orders) == 0:
        return None
    return orders

get_trades_history

get_trades_history(date_from: datetime = datetime(2000, 1, 1), date_to: datetime | None = None, group: str | None = None, ticket: int | None = None, position: int | None = None, to_df: bool = True) -> pd.DataFrame | list[TradeDeal] | None

Get deals from trading history within the specified interval with the ability to filter by ticket or position.

This method is useful if you need panda dataframe.

You can call this method in the following ways:

  • Call with a time interval. Returns all deals falling within the specified interval.

  • Call specifying the order ticket. Returns all deals having the specified order ticket in the DEAL_ORDER property.

  • Call specifying the position ticket. Returns all deals having the specified position ticket in the DEAL_POSITION_ID property.

Parameters:

Name Type Description Default
date_from datetime

Date the bars are requested from. Set by the datetime object or as a number of seconds elapsed since 1970-01-01. Bars with the open time >= date_from are returned. Required unnamed parameter.

datetime(2000, 1, 1)
date_to Optional[datetime]

Same as date_from.

None
group Optional[str]

The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only positions meeting specified criteria for a symbol name.

None
ticket Optional[int]

Ticket of an order (stored in DEAL_ORDER) for which all deals should be received. Optional parameter. If not specified, the filter is not applied.

None
position Optional[int]

Ticket of a position (stored in DEAL_POSITION_ID) for which all deals should be received. Optional parameter. If not specified, the filter is not applied.

None
to_df bool

If True, a DataFrame is returned.

True

Returns:

Type Description
DataFrame | list[TradeDeal] | None

Union[pd.DataFrame, Tuple[TradeDeal], None]:

DataFrame | list[TradeDeal] | None
  • TradeDeal in the form of a named tuple structure (namedtuple) or pd.DataFrame().
Notes

The method allows receiving all history orders within a specified period.

The group parameter may contain several comma-separated conditions.

A condition can be set as a mask using '*'.

The logical negation symbol '!' can be used for exclusion.

All conditions are applied sequentially, which means conditions for inclusion in a group should be specified first, followed by an exclusion condition.

For example, group="*, !EUR" means that deals for all symbols should be selected first and those containing "EUR" in symbol names should be excluded afterward.

Example
Get the number of deals in history

from datetime import datetime from_date = datetime(2020, 1, 1) to_date = datetime.now() account = Account() history = account.get_trades_history(from_date, to_date)

Source code in src/bbstrader/metatrader/account.py
def get_trades_history(
    self,
    date_from: datetime = datetime(2000, 1, 1),
    date_to: datetime | None = None,
    group: str | None = None,
    ticket: int | None = None,  # TradeDeal.ticket
    position: int | None = None,  # TradePosition.ticket
    to_df: bool = True,
) -> pd.DataFrame | list[TradeDeal] | None:
    """
    Get deals from trading history within the specified interval
    with the ability to filter by `ticket` or `position`.

    This method is useful if you need panda dataframe.

    You can call this method in the following ways:

    - Call with a `time interval`. Returns all deals falling within the specified interval.

    - Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.

    - Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.

    Args:
        date_from (datetime): Date the bars are requested from.
            Set by the `datetime` object or as a number of seconds elapsed since 1970-01-01.
            Bars with the open time >= `date_from` are returned. Required unnamed parameter.

        date_to (Optional[datetime]): Same as `date_from`.

        group (Optional[str]): The filter for arranging a group of necessary symbols.
            Optional named parameter. If the group is specified,
            the function returns only positions meeting specified criteria
            for a symbol name.

        ticket (Optional[int]): Ticket of an order (stored in `DEAL_ORDER`) for which all deals should be received.
            Optional parameter. If not specified, the filter is not applied.

        position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) for which all deals should be received.
            Optional parameter. If not specified, the filter is not applied.

        to_df (bool): If True, a DataFrame is returned.

    Returns:
        Union[pd.DataFrame, Tuple[TradeDeal], None]:
        - `TradeDeal` in the form of a named tuple structure (namedtuple) or pd.DataFrame().

    Notes:
        The method allows receiving all history orders within a specified period.

        The `group` parameter may contain several comma-separated conditions.

        A condition can be set as a mask using '*'.

        The logical negation symbol '!' can be used for exclusion.

        All conditions are applied sequentially, which means conditions for inclusion
        in a group should be specified first, followed by an exclusion condition.

        For example, `group="*, !EUR"` means that deals for all symbols should be selected first
        and those containing "EUR" in symbol names should be excluded afterward.

    Example:
        >>> # Get the number of deals in history
        >>> from datetime import datetime
        >>> from_date = datetime(2020, 1, 1)
        >>> to_date = datetime.now()
        >>> account = Account()
        >>> history = account.get_trades_history(from_date, to_date)
    """
    return self._fetch_history(
        fetch_type="deals",
        drop_cols=["time_msc", "external_id"],
        time_cols=["time"],
        **dict(
            date_from=date_from,
            date_to=date_to,
            group=group,
            ticket=ticket,
            position=position,
            to_df=to_df,
        ),
    )

get_orders_history

get_orders_history(date_from: datetime = datetime(2000, 1, 1), date_to: datetime | None = None, group: str | None = None, ticket: int | None = None, position: int | None = None, to_df: bool = True) -> pd.DataFrame | list[TradeOrder] | None

Get orders from trading history within the specified interval with the ability to filter by ticket or position.

You can call this method in the following ways:

  • Call with a time interval. Returns all deals falling within the specified interval.

  • Call specifying the order ticket. Returns all deals having the specified order ticket in the DEAL_ORDER property.

  • Call specifying the position ticket. Returns all deals having the specified position ticket in the DEAL_POSITION_ID property.

Parameters:

Name Type Description Default
date_from datetime

Date the bars are requested from. Set by the datetime object or as a number of seconds elapsed since 1970-01-01. Bars with the open time >= date_from are returned. Required unnamed parameter.

datetime(2000, 1, 1)
date_to Optional[datetime]

Same as date_from.

None
group Optional[str]

The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only positions meeting specified criteria for a symbol name.

None
ticket Optional[int]

Order ticket to filter results. Optional parameter. If not specified, the filter is not applied.

None
position Optional[int]

Ticket of a position (stored in DEAL_POSITION_ID) to filter results. Optional parameter. If not specified, the filter is not applied.

None
to_df bool

If True, a DataFrame is returned.

True
save bool

If True, a CSV file will be created to save the history.

required

Returns:

Type Description
DataFrame | list[TradeOrder] | None

Union[pd.DataFrame, List[TradeOrder], None]

DataFrame | list[TradeOrder] | None
  • List of TradeOrder .
Notes

The method allows receiving all history orders within a specified period.

The group parameter may contain several comma-separated conditions.

A condition can be set as a mask using '*'.

The logical negation symbol '!' can be used for exclusion.

All conditions are applied sequentially, which means conditions for inclusion in a group should be specified first, followed by an exclusion condition.

For example, group="*, !EUR" means that deals for all symbols should be selected first and those containing "EUR" in symbol names should be excluded afterward.

Example
Get the number of deals in history

from datetime import datetime from_date = datetime(2020, 1, 1) to_date = datetime.now() account = Account() history = account.get_orders_history(from_date, to_date)

Source code in src/bbstrader/metatrader/account.py
def get_orders_history(
    self,
    date_from: datetime = datetime(2000, 1, 1),
    date_to: datetime | None = None,
    group: str | None = None,
    ticket: int | None = None,  # order ticket
    position: int | None = None,  # position ticket
    to_df: bool = True,
) -> pd.DataFrame | list[TradeOrder] | None:
    """
    Get orders from trading history within the specified interval
    with the ability to filter by `ticket` or `position`.

    You can call this method in the following ways:

    - Call with a `time interval`. Returns all deals falling within the specified interval.

    - Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.

    - Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.

    Args:
        date_from (datetime): Date the bars are requested from.
            Set by the `datetime` object or as a number of seconds elapsed since 1970-01-01.
            Bars with the open time >= `date_from` are returned. Required unnamed parameter.

        date_to (Optional[datetime]): Same as `date_from`.

        group (Optional[str]): The filter for arranging a group of necessary symbols.
            Optional named parameter. If the group is specified,
            the function returns only positions meeting specified criteria
            for a symbol name.

        ticket (Optional[int]): Order ticket to filter results. Optional parameter.
            If not specified, the filter is not applied.

        position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) to filter results.
            Optional parameter. If not specified, the filter is not applied.

        to_df (bool): If True, a DataFrame is returned.

        save (bool): If True, a CSV file will be created to save the history.

    Returns:
        Union[pd.DataFrame, List[TradeOrder], None]
        - List of `TradeOrder` .

    Notes:
        The method allows receiving all history orders within a specified period.

        The `group` parameter may contain several comma-separated conditions.

        A condition can be set as a mask using '*'.

        The logical negation symbol '!' can be used for exclusion.

        All conditions are applied sequentially, which means conditions for inclusion
        in a group should be specified first, followed by an exclusion condition.

        For example, `group="*, !EUR"` means that deals for all symbols should be selected first
        and those containing "EUR" in symbol names should be excluded afterward.

    Example:
        >>> # Get the number of deals in history
        >>> from datetime import datetime
        >>> from_date = datetime(2020, 1, 1)
        >>> to_date = datetime.now()
        >>> account = Account()
        >>> history = account.get_orders_history(from_date, to_date)
    """
    return self._fetch_history(
        fetch_type="orders",
        drop_cols=[
            "time_expiration",
            "type_time",
            "state",
            "position_by_id",
            "reason",
            "volume_current",
            "price_stoplimit",
            "sl",
            "tp",
        ],
        time_cols=["time_setup", "time_done"],
        **dict(
            date_from=date_from,
            date_to=date_to,
            group=group,
            ticket=ticket,
            position=position,
            to_df=to_df,
        ),
    )

get_today_deals

get_today_deals(strategy_id: int, group: str | None = None, lookback_days: int = 3) -> list[TradeDeal]

Get all today deals for a specific strategy magic number.

Parameters:

Name Type Description Default
strategy_id int

Strategy or expert magic number.

required
group str | None

Symbol or group filter.

None
lookback_days int

How many days back to search for open positions.

3

Returns: list[TradeDeal]: Deals closed today belonging to the strategy.

Source code in src/bbstrader/metatrader/account.py
def get_today_deals(
    self,
    strategy_id: int,
    group: str | None = None,
    lookback_days: int = 3,
) -> list[TradeDeal]:
    """
    Get all today deals for a specific strategy magic number.

    Args:
        strategy_id (int): Strategy or expert magic number.
        group (str | None): Symbol or group filter.
        lookback_days (int): How many days back to search for open positions.
    Returns:
        list[TradeDeal]: Deals closed today belonging to the strategy.
    """
    from_date = datetime.now() - timedelta(days=lookback_days)
    history = (
        self.get_trades_history(date_from=from_date, group=group, to_df=False) or []
    )
    positions_ids = {
        deal.position_id for deal in history if deal.magic == strategy_id
    }
    today_deals = []
    for position in positions_ids:
        deals = self.get_trades_history(position=position, to_df=False) or []
        if not deals:
            continue
        last_deal = deals[-1]
        deal_time = datetime.fromtimestamp(last_deal.time)
        if deal_time.date() == datetime.now().date():
            today_deals.append(last_deal)
    return today_deals

Rates

Rates(symbol: str, timeframe: str = 'D1', start_pos: int = 0, count: int | None = MAX_BARS, **kwargs)

Provides methods to retrieve historical financial data from MetaTrader 5.

This class encapsulates interactions with the MetaTrader 5 (MT5) terminal to fetch historical price data for a given symbol and timeframe. It offers flexibility in retrieving data either by specifying a starting position and count of bars or by providing a specific date range .

Notes

All data is rerturn as pandas.DataFrame

  1. Befor using this class, ensure that the Max bars in chart in your terminal is set to a value that is greater than the number of bars you want to retrieve or just set it to Unlimited. In your MT5 terminal, go to Tools -> Options -> Charts -> Max bars in chart.

  2. The open, high, low, close, adjclose, returns, volume properties returns data in Broker's timezone by default.

See bbstrader.metatrader.broker.check_mt5_connection() for more details on how to connect to MT5 terminal.

Example

rates = Rates("EURUSD", "1h") df = rates.get_historical_data( ... date_from=datetime(2023, 1, 1), ... date_to=datetime(2023, 1, 10), ... ) print(df.head())

Initializes a new Rates instance.

Parameters:

Name Type Description Default
symbol str

Financial instrument symbol (e.g., "EURUSD").

required
timeframe str

Timeframe string (e.g., "D1", "1h", "5m").

'D1'
start_pos int

Starting index (int) for data retrieval.

0
count int

Number of bars to retrieve default is the maximum bars availble in the MT5 terminal.

MAX_BARS

Raises: ValueError: If the provided timeframe is invalid.

Source code in src/bbstrader/metatrader/rates.py
def __init__(
    self,
    symbol: str,
    timeframe: str = "D1",
    start_pos: int = 0,
    count: int | None = MAX_BARS,
    **kwargs,
):
    """
    Initializes a new Rates instance.

    Args:
        symbol (str): Financial instrument symbol (e.g., "EURUSD").
        timeframe (str): Timeframe string (e.g., "D1", "1h", "5m").
        start_pos (int): Starting index (int) for data retrieval.
        count (int, optional): Number of bars to retrieve default is
            the maximum bars availble in the MT5 terminal.
    Raises:
        ValueError: If the provided timeframe is invalid.
    """
    self.symbol = symbol
    self.start_pos = start_pos
    self.count = count
    self.time_frame = self._validate_time_frame(timeframe)
    self.__account = Account(**kwargs)
    self.__data = self.get_rates_from_pos

returns property

returns

Fractional change between the current and a prior element.

Computes the fractional change from the immediately previous row by default. This is useful in comparing the fraction of change in a time series of elements.

Note

It calculates fractional change (also known as per unit change or relative change) and not percentage change. If you need the percentage change, multiply these values by 100.

get_rates_from_pos

get_rates_from_pos(filter=False, fill_na=False, lower_colnames=False, utc=False) -> pd.DataFrame | None

Retrieves historical data starting from a specific position.

Uses the start_pos and count attributes specified during initialization to fetch data.

Parameters:

Name Type Description Default
filter

See Rates.get_historical_data for more details.

required
fill_na

See Rates.get_historical_data for more details.

required
lower_colnames

If True, the column names will be converted to lowercase.

required
utc bool

If True, the data will be in UTC timezone. Defaults to False.

False

Returns:

Type Description
DataFrame | None

Union[pd.DataFrame, None]: A DataFrame containing historical

DataFrame | None

data if successful, otherwise None.

Raises:

Type Description
ValueError

If start_pos or count is not provided during initialization.

Notes

The Datetime for this method is in Broker's timezone.

Source code in src/bbstrader/metatrader/rates.py
def get_rates_from_pos(
    self, filter=False, fill_na=False, lower_colnames=False, utc=False
) -> pd.DataFrame | None:
    """
    Retrieves historical data starting from a specific position.

    Uses the `start_pos` and `count` attributes specified during
    initialization to fetch data.

    Args:
        filter : See `Rates.get_historical_data` for more details.
        fill_na : See `Rates.get_historical_data` for more details.
        lower_colnames : If True, the column names will be converted to lowercase.
        utc (bool, optional): If True, the data will be in UTC timezone.
            Defaults to False.

    Returns:
        Union[pd.DataFrame, None]: A DataFrame containing historical
        data if successful, otherwise None.

    Raises:
        ValueError: If `start_pos` or `count` is not provided during
            initialization.

    Notes:
        The Datetime for this method is in Broker's timezone.
    """
    if self.start_pos is None or self.count is None:
        raise ValueError(
            "Both 'start_pos' and 'count' must be provided "
            "when calling 'get_rates_from_pos'."
        )
    utc = self._check_filter(filter, utc)
    df = self._fetch_data(
        self.start_pos, self.count, lower_colnames=lower_colnames, utc=utc
    )
    if df is None:
        return None
    if filter:
        return self._filter_data(df, fill_na=fill_na)
    return df

get_rates_from

get_rates_from(date_from: datetime | Timestamp, count: int = MAX_BARS, filter=False, fill_na=False, lower_colnames=False, utc=False) -> pd.DataFrame | None

Retrieves historical data within a specified date range.

Parameters:

Name Type Description Default
date_from

Starting date for data retrieval. The data will be retrieved from this date going to the past.

required
count

Number of bars to retrieve.

required
filter

See Rates.get_historical_data for more details.

required
fill_na

See Rates.get_historical_data for more details.

required
lower_colnames

If True, the column names will be converted to lowercase.

required
utc bool

If True, the data will be in UTC timezone. Defaults to False.

False

Returns:

Type Description
DataFrame | None

Union[pd.DataFrame, None]: A DataFrame containing historical

DataFrame | None

data if successful, otherwise None.

Source code in src/bbstrader/metatrader/rates.py
def get_rates_from(
    self,
    date_from: datetime | pd.Timestamp,
    count: int = MAX_BARS,
    filter=False,
    fill_na=False,
    lower_colnames=False,
    utc=False,
) -> pd.DataFrame | None:
    """
    Retrieves historical data within a specified date range.

    Args:
        date_from : Starting date for data retrieval.
            The data will be retrieved from this date going to the past.

        count : Number of bars to retrieve.

        filter : See `Rates.get_historical_data` for more details.
        fill_na : See `Rates.get_historical_data` for more details.
        lower_colnames : If True, the column names will be converted to lowercase.
        utc (bool, optional): If True, the data will be in UTC timezone.
            Defaults to False.

    Returns:
        Union[pd.DataFrame, None]: A DataFrame containing historical
        data if successful, otherwise None.
    """
    utc = self._check_filter(filter, utc)
    df = self._fetch_data(date_from, count, lower_colnames=lower_colnames, utc=utc)
    if df is None:
        return None
    if filter:
        return self._filter_data(df, fill_na=fill_na)
    return df

get_historical_data

get_historical_data(date_from: datetime | Timestamp, date_to: datetime | Timestamp = pd.Timestamp.now(), utc: bool = False, filter: bool | None = False, fill_na: bool | str | None = False, lower_colnames: bool | None = True, save_csv: bool | None = False) -> pd.DataFrame | None

Retrieves historical data within a specified date range.

Parameters:

Name Type Description Default
date_from

Starting date for data retrieval.

required
date_to

Ending date for data retrieval. Defaults to the current time.

required
utc

If True, the data will be in UTC timezone. Defaults to False.

required
filter

If True, the data will be filtered based on the trading sessions for the symbol. This is use when we want to use the data for backtesting using Zipline.

required
fill_na

If True, the data will be filled with the nearest value. This is use only when filter is True and time frame is "1m" or "D1", this is because we use calendar.minutes_in_range or calendar.sessions_in_range where calendar is the ExchangeCalendar from exchange_calendars package. So, for "1m" or "D1" time frame, the data will be filled with the nearest value because the data from MT5 will have approximately the same number of rows as the number of trading days or minute in the exchange calendar, so we can fill the missing data with the nearest value.

But for other time frames, the data will be reindexed with the exchange calendar because the data from MT5 will have more rows than the number of trading days or minute in the exchange calendar. So we only take the data that is in the range of the exchange calendar sessions or minutes.

required
lower_colnames

If True, the column names will be converted to lowercase.

required
save_csv

File path to save the data as a CSV. If None, the data won't be saved.

required

Returns:

Type Description
DataFrame | None

Union[pd.DataFrame, None]: A DataFrame containing historical data if successful, otherwise None.

Raises:

Type Description
ValueError

If the starting date is greater than the ending date.

Notes

The filter for this method can be use only for Admira Markets Group (AMG) symbols. The Datetime for this method is in Local timezone by default. All STK symbols are filtered based on the the exchange calendar. All FX symbols are filtered based on the us_futures calendar. All IDX symbols are filtered based on the exchange calendar of margin currency. All COMD symbols are filtered based on the exchange calendar of the commodity.

Source code in src/bbstrader/metatrader/rates.py
def get_historical_data(
    self,
    date_from: datetime | pd.Timestamp,
    date_to: datetime | pd.Timestamp = pd.Timestamp.now(),
    utc: bool = False,
    filter: bool | None = False,
    fill_na: bool | str | None = False,
    lower_colnames: bool | None = True,
    save_csv: bool | None = False,
) -> pd.DataFrame | None:
    """
    Retrieves historical data within a specified date range.

    Args:
        date_from : Starting date for data retrieval.

        date_to : Ending date for data retrieval.
            Defaults to the current time.

        utc : If True, the data will be in UTC timezone.
            Defaults to False.

        filter : If True, the data will be filtered based
            on the trading sessions for the symbol.
            This is use when we want to use the data for backtesting using Zipline.

        fill_na : If True, the data will be filled with the nearest value.
            This is use only when `filter` is True and time frame is "1m" or "D1",
            this is because we use ``calendar.minutes_in_range`` or ``calendar.sessions_in_range``
            where calendar is the ``ExchangeCalendar`` from `exchange_calendars` package.
            So, for "1m" or "D1" time frame, the data will be filled with the nearest value
            because the data from MT5 will have approximately the same number of rows as the
            number of trading days or minute in the exchange calendar, so we can fill the missing
            data with the nearest value.

            But for other time frames, the data will be reindexed with the exchange calendar
            because the data from MT5 will have more rows than the number of trading days or minute
            in the exchange calendar. So we only take the data that is in the range of the exchange
            calendar sessions or minutes.

        lower_colnames : If True, the column names will be converted to lowercase.

        save_csv : File path to save the data as a CSV.
            If None, the data won't be saved.

    Returns:
        Union[pd.DataFrame, None]: A DataFrame containing historical data
            if successful, otherwise None.

    Raises:
        ValueError: If the starting date is greater than the ending date.

    Notes:
        The `filter` for this method can be use only for Admira Markets Group (AMG) symbols.
        The Datetime for this method is in Local timezone by default.
        All STK symbols are filtered based on the the exchange calendar.
        All FX symbols are filtered based on the ``us_futures`` calendar.
        All IDX symbols are filtered based on the exchange calendar of margin currency.
        All COMD symbols are filtered based on the exchange calendar of the commodity.
    """
    utc = self._check_filter(filter, utc)
    df = self._fetch_data(
        date_from, date_to, lower_colnames=lower_colnames, utc=utc
    )
    if df is None:
        return None
    if filter:
        df = self._filter_data(
            df, date_from=date_from, date_to=date_to, fill_na=fill_na
        )
    if save_csv:
        df.to_csv(f"{self.symbol}.csv")
    return df

RiskManagement

RiskManagement(symbol: str, max_risk: float = 10.0, daily_risk: float | None = None, max_trades: int | None = None, std_stop: bool = False, pchange_sl: float | None = None, account_leverage: bool = True, time_frame: TimeFrame = 'D1', start_time: str = '1:00', finishing_time: str = '23:00', broker_tz: bool = False, sl: int | None = None, tp: int | None = None, be: int | None = None, rr: float = 3.0, **kwargs)

The RiskManagement class provides foundational risk management functionalities for trading activities. It calculates risk levels, determines stop loss and take profit levels, and ensures trading activities align with predefined risk parameters.

Exemple

risk_manager = RiskManagement( ... symbol="EURUSD", ... max_risk=5.0, ... daily_risk=2.0, ... max_trades=10, ... std_stop=True, ... act_leverage=True, ... start_time="09:00", ... finishing_time="17:00", ... time_frame="1h" ... )

Calculate risk level

risk_level = risk_manager.risk_level()

Get appropriate lot size for a trade

lot_size = risk_manager.get_lot()

Determine stop loss and take profit levels

stop_loss = risk_manager.get_stop_loss() take_profit = risk_manager.get_take_profit()

Check if current risk is acceptable

is_risk_acceptable = risk_manager.is_risk_ok()

Initialize the RiskManagement class to manage risk in trading activities.

Parameters:

Name Type Description Default
symbol str

The symbol of the financial instrument to trade.

required
max_risk float

The maximum risk allowed on the trading account.

10.0
daily_risk float

Daily Max risk allowed. If Set to None it will be determine based on Maximum risk. The day is based on the start and the ending time

None
max_trades int

Maximum number of trades at any point in time. If set to None it will be determine based on the timeframe of trading.

None
std_stop bool

If set to True, the Stop loss is calculated based On historical volatility of the trading instrument. Defaults to False.

False
pchange_sl float

If set, the Stop loss is calculated based On percentage change of the trading instrument.

None
act_leverage bool

If set to True the account leverage will be used In risk management setting. Defaults to False.

required
time_frame str

The time frame on which the program is working (1m, 3m, 5m, 10m, 15m, 30m, 1h, 2h, 4h, D1). Defaults to 'D1'.

'D1'
start_time str

The starting time for the trading session (HH:MM, H and M do not star with 0). Defaults to "1:00".

'1:00'
finishing_time str

The finishing time for the trading strategy (HH:MM, H and M do not star with 0). Defaults to "23:00".

'23:00'
sl int

Stop Loss in points, Must be a positive number.

None
tp int

Take Profit in points, Must be a positive number.

None
be int

Break Even in points, Must be a positive number.

None
rr float

Risk reward ratio, Must be a positive number. Defaults to 1.5.

3.0
Source code in src/bbstrader/metatrader/risk.py
def __init__(
    self,
    symbol: str,
    max_risk: float = 10.0,
    daily_risk: float | None = None,
    max_trades: int | None = None,
    std_stop: bool = False,
    pchange_sl: float | None = None,
    account_leverage: bool = True,
    time_frame: TimeFrame = "D1",
    start_time: str = "1:00",
    finishing_time: str = "23:00",
    broker_tz: bool = False,
    sl: int | None = None,
    tp: int | None = None,
    be: int | None = None,
    rr: float = 3.0,
    **kwargs,
):
    """
    Initialize the RiskManagement class to manage risk in trading activities.

    Args:
        symbol (str): The symbol of the financial instrument to trade.
        max_risk (float): The `maximum risk allowed` on the trading account.
        daily_risk (float, optional): `Daily Max risk allowed`.
            If Set to None it will be determine based on Maximum risk.
            The day is based on the start and the ending time
        max_trades (int, optional): Maximum number of trades at any point in time.
            If set to None it will be determine based on the timeframe of trading.
        std_stop (bool, optional): If set to True, the Stop loss is calculated based
            On `historical volatility` of the trading instrument. Defaults to False.
        pchange_sl (float, optional): If set, the Stop loss is calculated based
            On `percentage change` of the trading instrument.
        act_leverage (bool, optional): If set to True the account leverage will be used
            In risk management setting. Defaults to False.
        time_frame (str, optional): The time frame on which the program is working
            `(1m, 3m, 5m, 10m, 15m, 30m, 1h, 2h, 4h, D1)`. Defaults to 'D1'.
        start_time (str, optional): The starting time for the trading session
            `(HH:MM, H and M do not star with 0)`. Defaults to "1:00".
        finishing_time (str, optional): The finishing time for the trading strategy
            `(HH:MM, H and M do not star with 0)`. Defaults to "23:00".
        sl (int, optional): Stop Loss in points, Must be a positive number.
        tp (int, optional): Take Profit in points, Must be a positive number.
        be (int, optional): Break Even in points, Must be a positive number.
        rr (float, optional): Risk reward ratio, Must be a positive number. Defaults to 1.5.
    """

    assert max_risk > 0
    assert daily_risk > 0 if daily_risk is not None else ...
    daily_risk = round(daily_risk, 5) if daily_risk is not None else None
    assert all(isinstance(v, int) and v > 0 for v in [sl, tp] if v is not None)
    assert isinstance(be, (int, float)) and be > 0 if be else ...
    assert time_frame in TIMEFRAMES

    self.kwargs = kwargs
    self.symbol = symbol
    self.timeframe = time_frame
    self.start_time = start_time
    self.finishing_time = finishing_time
    self.max_trades = max_trades
    self.std_stop = std_stop
    self.pchange = pchange_sl
    self.act_leverage = account_leverage
    self.daily_dd = daily_risk
    self.max_risk = max_risk
    self.broker_tz = broker_tz
    self.rr = rr
    self.sl = sl
    self.tp = tp
    self.be = be

    self.account = Account(**kwargs)
    self.symbol_info = client.symbol_info(self.symbol)

get_minutes

get_minutes() -> int

calculates the number of minutes between the starting of the session and the end of the session

Source code in src/bbstrader/metatrader/risk.py
def get_minutes(self) -> int:
    """calculates the number of minutes between
    the starting of the session and the end of the session"""

    fmt = "%H:%M"
    start = datetime.strptime(self.start_time, fmt)
    end = datetime.strptime(self.finishing_time, fmt)
    if self.broker_tz:
        start = self.account.broker.get_broker_time(self.start_time, fmt)
        end = self.account.broker.get_broker_time(self.finishing_time, fmt)
    diff = (end - start).total_seconds()
    diff += 86400 if diff < 0 else diff
    return int(diff // 60)

get_hours

get_hours() -> int

Calculates the number of hours between the starting of the session and the end of the session

Source code in src/bbstrader/metatrader/risk.py
def get_hours(self) -> int:
    """Calculates the number of hours between
    the starting of the session and the end of the session"""
    return self.get_minutes() // 60

risk_level

risk_level(balance_value=False) -> float | tuple[float, float]

Calculates the risk level of a trade

Returns: - Risk level in the form of a float percentage.

Source code in src/bbstrader/metatrader/risk.py
def risk_level(self, balance_value=False) -> float | tuple[float, float]:
    """
    Calculates the risk level of a trade

    Returns:
    -   Risk level in the form of a float percentage.
    """
    account_info = self.account.get_account_info()
    balance = account_info.balance
    equity = account_info.equity
    if equity == 0:
        return 0.0
    trades_history = self.account.get_trades_history()

    realized_profit = None
    if trades_history is None or len(trades_history) == 1:
        realized_profit = 0
    else:
        profit_df = trades_history.iloc[1:]
        profit = profit_df["profit"].sum()
        commisions = trades_history["commission"].sum()
        fees = trades_history["fee"].sum()
        swap = trades_history["swap"].sum()
        realized_profit = commisions + fees + swap + profit

    initial_balance = balance - realized_profit
    dd_percent = ((equity - initial_balance) / equity) * 100
    dd_percent = round(abs(dd_percent) if dd_percent < 0 else 0.0, 2)
    if balance_value:
        return (initial_balance, equity)
    return dd_percent

max_trade

max_trade() -> int

calculates the maximum number of trades allowed

Source code in src/bbstrader/metatrader/risk.py
def max_trade(self) -> int:
    """calculates the maximum number of trades allowed"""
    minutes = self.get_minutes()
    tf_int = self._convert_time_frame(self.timeframe)
    max_trades = self.max_trades or round(minutes / tf_int)
    return max(max_trades, 1)

get_std_stop

get_std_stop() -> int

Calculate the standard deviation-based stop loss level for a given financial instrument.

Returns: - Standard deviation-based stop loss level, rounded to the nearest point. - 0 if the calculated stop loss is less than or equal to 0.

Source code in src/bbstrader/metatrader/risk.py
def get_std_stop(self) -> int:
    """
    Calculate the standard deviation-based stop loss level
    for a given financial instrument.

    Returns:
    -   Standard deviation-based stop loss level, rounded to the nearest point.
    -   0 if the calculated stop loss is less than or equal to 0.
    """
    std = np.std(self._get_returns())
    return self._get_stop(std)

get_pchange_stop

get_pchange_stop(pchange: float | None) -> int

Calculate the percentage change-based stop loss level for a given financial instrument.

Parameters:

Name Type Description Default
pchange float

Percentage change in price to use for calculating stop loss level. If pchange is set to None, the stop loss is calculate using std.

required

Returns: - Percentage change-based stop loss level, rounded to the nearest point. - 0 if the calculated stop loss is <= 0.

Source code in src/bbstrader/metatrader/risk.py
def get_pchange_stop(self, pchange: float | None) -> int:
    """
    Calculate the percentage change-based stop loss level
    for a given financial instrument.

    Args:
        pchange (float): Percentage change in price to use for calculating stop loss level.
            If pchange is set to None, the stop loss is calculate using std.

    Returns:
    -   Percentage change-based stop loss level, rounded to the nearest point.
    -   0 if the calculated stop loss is <= 0.
    """
    if pchange is not None:
        return self._get_stop(pchange)
    else:
        # Use std as default pchange
        return self.get_std_stop()

calculate_var

calculate_var(tf: TimeFrame = 'D1', c=0.95) -> float

Calculate Value at Risk (VaR) for a given portfolio.

Parameters:

Name Type Description Default
tf str

Time frame to use to calculate volatility.

'D1'
c float

Confidence level for VaR calculation (default is 95%).

0.95

Returns: - VaR value

Source code in src/bbstrader/metatrader/risk.py
def calculate_var(self, tf: TimeFrame = "D1", c=0.95) -> float:
    """
    Calculate Value at Risk (VaR) for a given portfolio.

    Args:
        tf (str): Time frame to use to calculate volatility.
        c (float): Confidence level for VaR calculation (default is 95%).

    Returns:
    -   VaR value
    """
    returns = self._get_returns()
    P = self.account.get_account_info().margin_free
    mu = returns.mean()
    sigma = returns.std()
    alpha = norm.ppf(1 - c, mu, sigma)
    return P - P * (alpha + 1)

get_trade_risk

get_trade_risk() -> float

Calculate risk per trade as percentage

Source code in src/bbstrader/metatrader/risk.py
def get_trade_risk(self) -> float:
    """Calculate risk per trade as percentage"""
    total_risk = self.risk_level()
    max_trades = self.max_trade()
    if total_risk < self.max_risk:
        if self.daily_dd is not None:
            trade_risk = self.daily_dd / max_trades
        else:
            trade_risk = (self.max_risk - total_risk) / max_trades
        return trade_risk
    else:
        return 0

var_loss_value

var_loss_value() -> float

Calculate the stop-loss level based on VaR.

Notes

The Var is Estimated using the Variance-Covariance method on the daily returns. If you want to use the VaR for a different time frame .

Source code in src/bbstrader/metatrader/risk.py
def var_loss_value(self) -> float:
    """
    Calculate the stop-loss level based on VaR.

    Notes:
        The Var is Estimated using the Variance-Covariance method on the daily returns.
        If you want to use the VaR for a different time frame .
    """
    P = self.account.get_account_info().margin_free
    trade_risk = self.get_trade_risk()
    loss_allowed = P * trade_risk / 100
    var = self.calculate_var()
    return min(var, loss_allowed)

get_take_profit

get_take_profit() -> int

calculates the take profit of a trade in points

Source code in src/bbstrader/metatrader/risk.py
def get_take_profit(self) -> int:
    """calculates the take profit of a trade in points"""
    deviation = self.get_deviation()
    if self.tp is not None:
        return self.tp + deviation
    else:
        return round(self.get_stop_loss() * self.rr)

get_currency_risk

get_currency_risk() -> float

calculates the currency risk of a trade

Source code in src/bbstrader/metatrader/risk.py
def get_currency_risk(self) -> float:
    """calculates the currency risk of a trade"""
    return round(self.currency_risk()["currency_risk"], 2)

expected_profit

expected_profit()

Calculate the expected profit per trade

Source code in src/bbstrader/metatrader/risk.py
def expected_profit(self):
    """Calculate the expected profit per trade"""
    risk = self.get_currency_risk()
    return round(risk * self.rr, 2)

volume

volume()

Volume per trade

Source code in src/bbstrader/metatrader/risk.py
def volume(self):
    """Volume per trade"""

    return self.currency_risk()["volume"]

currency_risk

currency_risk() -> dict[str, int | float | Any]

Calculates the currency risk of a trade.

Returns:

Type Description
dict[str, int | float | Any]

Dict[str, Union[int, float, Any]]: A dictionary containing the following keys:

dict[str, int | float | Any]
  • 'currency_risk': Dollar amount risk on a single trade.
dict[str, int | float | Any]
  • 'trade_loss': Loss value per tick in dollars.
dict[str, int | float | Any]
  • 'trade_profit': Profit value per tick in dollars.
dict[str, int | float | Any]
  • 'volume': Contract size multiplied by the average price.
dict[str, int | float | Any]
  • 'lot': Lot size per trade.
Source code in src/bbstrader/metatrader/risk.py
def currency_risk(self) -> dict[str, int | float | Any]:
    """
    Calculates the currency risk of a trade.

    Returns:
        Dict[str, Union[int, float, Any]]: A dictionary containing the following keys:

        - `'currency_risk'`: Dollar amount risk on a single trade.
        - `'trade_loss'`: Loss value per tick in dollars.
        - `'trade_profit'`: Profit value per tick in dollars.
        - `'volume'`: Contract size multiplied by the average price.
        - `'lot'`: Lot size per trade.
    """
    s_info = self.account.get_symbol_info(self.symbol)
    leverage = self.account.broker.get_leverage_for_symbol(
        self.symbol, self.act_leverage
    )
    contract_size = s_info.trade_contract_size
    av_price = (s_info.bid + s_info.ask) / 2
    trade_risk = self.get_trade_risk()
    symbol_type = self.account.get_symbol_type(self.symbol)

    tick_value_loss, tick_value_profit = self.account.broker.adjust_tick_values(
        self.symbol,
        s_info.trade_tick_value_loss,
        s_info.trade_tick_value_profit,
        contract_size,
    )
    tick_value = s_info.trade_tick_value  # For checks

    if tick_value == 0 or tick_value_loss == 0 or tick_value_profit == 0:
        logger.error(
            f"The Tick Values for {self.symbol} is 0.0. Check broker conditions for {self.symbol}."
        )
        return {
            "currency_risk": 0.0,
            "trade_loss": 0.0,
            "trade_profit": 0.0,
            "volume": 0,
            "lot": 0.01,
        }

    if trade_risk > 0:
        currency_risk = round(self.var_loss_value(), 5)
        volume = round(currency_risk * leverage)
        lot = (
            round(volume / (contract_size * av_price), 2)
            if contract_size * av_price != 0
            else 0.0
        )
        lot = self.account.broker.validate_lot_size(self.symbol, lot)

        if symbol_type == SymbolType.COMMODITIES and contract_size > 1:
            lot = (
                volume / (av_price * contract_size)
                if av_price * contract_size != 0
                else 0.0
            )
            lot = self.account.broker.validate_lot_size(self.symbol, lot)
        if symbol_type == SymbolType.FOREX:
            lot = round(volume / contract_size, 2) if contract_size != 0 else 0.0
            lot = self.account.broker.validate_lot_size(self.symbol, lot)

        if self.sl is not None:
            trade_loss = currency_risk / self.sl if self.sl != 0 else 0.0
            trade_profit = (
                (currency_risk * (self.tp // self.sl if self.tp else self.rr))
                / (self.tp or (self.sl * self.rr))
                if self.sl != 0
                else 0.0
            )
            lot = (
                round(trade_loss / (contract_size * tick_value_loss), 2)
                if contract_size * tick_value_loss != 0
                else 0.0
            )
            lot = self.account.broker.validate_lot_size(self.symbol, lot)
            volume = round(lot * contract_size * av_price)

            if (
                symbol_type in [SymbolType.COMMODITIES, SymbolType.CRYPTO]
            ) and contract_size > 1:
                lot = (
                    currency_risk / (self.sl * tick_value_loss * contract_size)
                    if self.sl * tick_value_loss * contract_size != 0
                    else 0.0
                )
                lot = self.account.broker.validate_lot_size(self.symbol, lot)
                trade_loss = lot * contract_size * tick_value_loss

            if symbol_type == SymbolType.FOREX:
                volume = (
                    round(trade_loss * contract_size / tick_value_loss)
                    if tick_value_loss != 0
                    else 0
                )
                lot = (
                    round(volume / contract_size, 2) if contract_size != 0 else 0.0
                )
                lot = self.account.broker.validate_lot_size(self.symbol, lot)

        elif self.std_stop and self.pchange is None and self.sl is None:
            sl = self.get_std_stop()
            trade_loss, trade_profit, lot, volume = self._std_pchange_stop(
                currency_risk, sl, contract_size, tick_value_loss
            )

        elif self.pchange is not None and not self.std_stop and self.sl is None:
            sl = self.get_pchange_stop(self.pchange)
            trade_loss, trade_profit, lot, volume = self._std_pchange_stop(
                currency_risk, sl, contract_size, tick_value_loss
            )

        else:
            if symbol_type == SymbolType.FOREX:
                trade_loss = (
                    tick_value_loss * (volume / contract_size)
                    if contract_size != 0
                    else 0.0
                )
                trade_profit = (
                    tick_value_profit * (volume / contract_size)
                    if contract_size != 0
                    else 0.0
                )
            else:
                trade_loss = (lot * contract_size) * tick_value_loss
                trade_profit = (lot * contract_size) * tick_value_profit

        # Apply currency conversion
        rates = self.account.get_currency_rates(self.symbol)
        factor = self.account.broker.get_currency_conversion_factor(
            self.symbol, rates.get("pc", ""), self.account.currency
        )
        trade_profit *= factor
        trade_loss *= factor
        currency_risk *= factor

        return {
            "currency_risk": currency_risk,
            "trade_loss": trade_loss,
            "trade_profit": trade_profit,
            "volume": round(volume),
            "lot": lot,
        }
    else:
        return {
            "currency_risk": 0.0,
            "trade_loss": 0.0,
            "trade_profit": 0.0,
            "volume": 0,
            "lot": 0.01,
        }

get_break_even

get_break_even(thresholds: list[tuple[int, float]] = None) -> int

Calculates the break-even price level based on stop-loss tiers.

The function determines the break-even point by applying a multiplier to the sum of the current stop-loss and market spread. If an explicit break-even value (self.be) is already set, it returns that value (converting percentage-based floats to absolute points if necessary).

Parameters:

Name Type Description Default
thresholds list[tuple[int, float]]

A list of tiers defined as (threshold_limit, multiplier). Example: [(150, 0.25), (100, 0.35), (0, 0.5)]. If None, defaults to standard conservative tiers.

None

Returns:

Name Type Description
int int

The calculated break-even value in points/pips.

Note

The function automatically sorts thresholds in descending order to ensure the 'stop' value is matched against the highest possible tier first.

Source code in src/bbstrader/metatrader/risk.py
def get_break_even(self, thresholds: list[tuple[int, float]] = None) -> int:
    """
    Calculates the break-even price level based on stop-loss tiers.

    The function determines the break-even point by applying a multiplier to the
    sum of the current stop-loss and market spread. If an explicit break-even
    value (`self.be`) is already set, it returns that value (converting
    percentage-based floats to absolute points if necessary).

    Args:
        thresholds (list[tuple[int, float]], optional): A list of tiers defined
            as (threshold_limit, multiplier).
            Example: [(150, 0.25), (100, 0.35), (0, 0.5)].
            If None, defaults to standard conservative tiers.

    Returns:
        int: The calculated break-even value in points/pips.

    Note:
        The function automatically sorts thresholds in descending order to
        ensure the 'stop' value is matched against the highest possible tier first.
    """
    if self.be is not None:
        return (
            self.be if isinstance(self.be, int) else self.get_pchange_stop(self.be)
        )

    if thresholds is None:
        thresholds = [(150, 0.25), (100, 0.35), (0, 0.50)]

    stop = self.get_stop_loss()
    spread = client.symbol_info(self.symbol).spread
    sorted_thresholds = sorted(thresholds, key=lambda x: x[0], reverse=True)

    for limit, multiplier in sorted_thresholds:
        if stop > limit:
            return round((stop + spread) * multiplier)
    return 0

Trade

Trade(symbol: str = 'EURUSD', expert_name: str = 'bbstrader', expert_id: int = EXPERT_ID, version: str = '3.0', target: float = 5.0, start_time: str = '1:00', finishing_time: str = '23:00', ending_time: str = '23:30', time_frame: str = 'D1', broker_tz=False, verbose: bool = False, console_log: bool = False, logger: Logger | str = 'bbstrader.log', **kwargs)

Extends the RiskManagement class to include specific trading operations, incorporating risk management strategies directly into trade executions. It offers functionalities to execute trades while managing risks.

Exemple

import time

Initialize the Trade class with parameters

trade = Trade( ... symbol="EURUSD", # Symbol to trade ... expert_name="bbstrader", # Name of the expert advisor ... expert_id=12345, # Unique ID for the expert advisor ... version="1.0", # Version of the expert advisor ... target=5.0, # Daily profit target in percentage ... start_time="09:00", # Start time for trading ... finishing_time="17:00", # Time to stop opening new positions ... ending_time="17:30", # Time to close any open positions ... max_risk=2.0, # Maximum risk allowed on the account in percentage ... daily_risk=1.0, # Daily risk allowed in percentage ... max_trades=5, # Maximum number of trades per session ... rr=2.0, # Risk-reward ratio ... account_leverage=True, # Use account leverage in calculations ... std_stop=True, # Use standard deviation for stop loss calculation ... sl=20, # Stop loss in points (optional) ... tp=30, # Take profit in points (optional) ... be=10 # Break-even in points (optional) ... )

Example to open a buy position

trade.open_buy_position(mm=True, comment="Opening Buy Position")

Example to open a sell position

trade.open_sell_position(mm=True, comment="Opening Sell Position")

Check current open positions

opened_positions = trade.get_opened_positions if opened_positions is not None: ... print(f"Current open positions: {opened_positions}")

Close all open positions at the end of the trading session

if trade.days_end(): ... trade.close_all_positions(comment="Closing all positions at day's end")

Print trading session statistics

trade.statistics(save=True, dir="my_trading_stats")

Sleep until the next trading session if needed (example usage)

sleep_time = trade.sleep_time() print(f"Sleeping for {sleep_time} minutes until the next trading session.") time.sleep(sleep_time * 60)

Initializes the Trade class with the specified parameters.

Parameters:

Name Type Description Default
symbol str

The symbol that the expert advisor will trade.

'EURUSD'
expert_name str

The name of the expert advisor.

'bbstrader'
expert_id int

The unique ID used to identify the expert advisor or the strategy used on the symbol.

EXPERT_ID
version str

The version of the expert advisor.

'3.0'
target float

Trading period (day, week, month) profit target in percentage.

5.0
start_time str

Thehour and minutes that the expert advisor is able to start to run.

'1:00'
finishing_time str

The time after which no new position can be opened.

'23:00'
ending_time str

The time after which any open position will be closed.

'23:30'
verbose bool | None

If set to None (default), account summary and risk managment parameters are printed in the terminal.

False
console_log bool

If set to True, log messages are displayed in the console.

False
logger Logger | str

The logger object to use for logging messages could be a string or a logger object.

'bbstrader.log'
**kwargs

Params for the RiskManagement and Account See the bbstrader.metatrader.risk.RiskManagement class for more details on these parameters. See bbstrader.metatrader.broker.check_mt5_connection() for more details on how to connect to MT5 terminal.

{}
Source code in src/bbstrader/metatrader/trade.py
def __init__(
    self,
    symbol: str = "EURUSD",
    expert_name: str = "bbstrader",
    expert_id: int = EXPERT_ID,
    version: str = "3.0",
    target: float = 5.0,
    start_time: str = "1:00",
    finishing_time: str = "23:00",
    ending_time: str = "23:30",
    time_frame: str = "D1",
    broker_tz=False,
    verbose: bool = False,
    console_log: bool = False,
    logger: Logger | str = "bbstrader.log",
    **kwargs,
):
    """
    Initializes the Trade class with the specified parameters.

    Args:
        symbol (str): The `symbol` that the expert advisor will trade.
        expert_name (str): The name of the `expert advisor`.
        expert_id (int): The `unique ID` used to identify the expert advisor
            or the strategy used on the symbol.
        version (str): The `version` of the expert advisor.
        target (float): `Trading period (day, week, month) profit target` in percentage.
        start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
        finishing_time (str): The time after which no new position can be opened.
        ending_time (str): The time after which any open position will be closed.
        verbose (bool | None): If set to None (default), account summary and risk managment
            parameters are printed in the terminal.
        console_log (bool): If set to True, log messages are displayed in the console.
        logger (Logger | str): The logger object to use for logging messages could be a string or a logger object.
        **kwargs: Params for the RiskManagement and Account
            See the ``bbstrader.metatrader.risk.RiskManagement`` class for more details on these parameters.
            See `bbstrader.metatrader.broker.check_mt5_connection()` for more details on how to connect to MT5 terminal.
    """

    self.symbol = symbol
    self.expert_name = expert_name
    self.expert_id = expert_id
    self.version = version
    self.target = target
    self.verbose = verbose
    self.start = start_time
    self.end = ending_time
    self.finishing = finishing_time
    self.broker_tz = broker_tz
    self.console_log = console_log
    self.timeframe = time_frame
    self.kwargs = kwargs

    self.account = Account(**kwargs)
    self.rm = RiskManagement(
        symbol=symbol,
        start_time=start_time,
        finishing_time=finishing_time,
        time_frame=time_frame,
        broker_tz=broker_tz,
        **kwargs,
    )

    self.buy_positions = []
    self.sell_positions = []
    self.opened_positions = []
    self.opened_orders = []
    self.break_even_status = []
    self.break_even_points = {}
    self.trail_after_points = []
    self._retcodes = []

    self._get_logger(logger, console_log)
    self.initialize(**kwargs)
    self.select_symbol(**kwargs)
    self.prepare_symbol()

    if self.verbose:
        self.summary()
        print()
        self.risk_managment()
        print(f">>> Everything is OK, @{self.expert_name} is Running ...>>>\n")

retcodes property

retcodes: list[int]

Return all the retcodes

orders property

orders

Return all opened order's tickets

positions property

positions

Return all opened position's tickets

buypos property

buypos

Return all buy opened position's tickets

sellpos property

sellpos

Return all sell opened position's tickets

bepos property

bepos

Return All positon's tickets for which a break even has been set

initialize

initialize(**kwargs)

Initializes the MetaTrader 5 (MT5) terminal for trading operations. This method attempts to establish a connection with the MT5 terminal. If the initial connection attempt fails due to a timeout, it retries after a specified delay. Successful initialization is crucial for the execution of trading operations.

Raises:

Type Description
MT5TerminalError

If initialization fails.

Source code in src/bbstrader/metatrader/trade.py
def initialize(self, **kwargs):
    """
    Initializes the MetaTrader 5 (MT5) terminal for trading operations.
    This method attempts to establish a connection with the MT5 terminal.
    If the initial connection attempt fails due to a timeout, it retries after a specified delay.
    Successful initialization is crucial for the execution of trading operations.

    Raises:
        MT5TerminalError: If initialization fails.
    """
    try:
        if self.verbose:
            print("\nInitializing the basics.")
        check_mt5_connection(**kwargs)
        if self.verbose:
            print(
                f"You are running the @{self.expert_name} Expert advisor,"
                f" Version @{self.version}, on {self.symbol}."
            )
    except Exception as e:
        LOGGER.error(f"During initialization: {e}")

select_symbol

select_symbol(**kwargs)

Selects the trading symbol in the MetaTrader 5 (MT5) terminal. This method ensures that the specified trading symbol is selected and visible in the MT5 terminal, allowing subsequent trading operations such as opening and closing positions on this symbol.

Raises:

Type Description
MT5TerminalError

If symbole selection fails.

Source code in src/bbstrader/metatrader/trade.py
def select_symbol(self, **kwargs):
    """
    Selects the trading symbol in the MetaTrader 5 (MT5) terminal.
    This method ensures that the specified trading
    symbol is selected and visible in the MT5 terminal,
    allowing subsequent trading operations such as opening and
    closing positions on this symbol.

    Raises:
        MT5TerminalError: If symbole selection fails.
    """
    try:
        check_mt5_connection(**kwargs)
        if not client.symbol_select(self.symbol, True):
            raise_mt5_error(message=INIT_MSG)
    except Exception as e:
        LOGGER.error(f"Selecting symbol '{self.symbol}': {e}")

prepare_symbol

prepare_symbol()

Prepares the selected symbol for trading. This method checks if the symbol is available and visible in the MT5 terminal. If the symbol is not visible, it attempts to select the symbol again. This step ensures that trading operations can be performed on the selected symbol without issues.

Raises:

Type Description
MT5TerminalError

If the symbol cannot be made visible for trading operations.

Source code in src/bbstrader/metatrader/trade.py
def prepare_symbol(self):
    """
    Prepares the selected symbol for trading.
    This method checks if the symbol is available and visible in the
    MT5 terminal. If the symbol is not visible, it attempts to select the symbol again.
    This step ensures that trading operations can be performed on the selected symbol without issues.

    Raises:
        MT5TerminalError: If the symbol cannot be made visible for trading operations.
    """
    try:
        symbol_info = client.symbol_info(self.symbol)
        if symbol_info is None:
            raise_mt5_error(message=INIT_MSG)

        if not symbol_info.visible:
            raise_mt5_error(message=INIT_MSG)
        if self.verbose:
            print("Initialization successfully completed.")
    except Exception as e:
        LOGGER.error(f"Preparing symbol '{self.symbol}': {e}")

summary

summary()

Show a brief description about the trading program

Source code in src/bbstrader/metatrader/trade.py
def summary(self):
    """Show a brief description about the trading program"""
    fmt = "%H:%M"
    start = datetime.strptime(self.start, fmt).time()
    finish = datetime.strptime(self.finishing, fmt).time()
    end = datetime.strptime(self.end, fmt).time()
    if self.broker_tz:
        start = self.account.broker.get_broker_time(self.start, fmt).time()
        finish = self.account.broker.get_broker_time(self.finishing, fmt).time()
        end = self.account.broker.get_broker_time(self.end, fmt).time()
    summary_data = [
        ["Expert Advisor Name", f"@{self.expert_name}"],
        ["Expert Advisor Version", f"@{self.version}"],
        ["Expert | Strategy ID", self.expert_id],
        ["Trading Symbol", self.symbol],
        ["Trading Time Frame", self.timeframe],
        ["Start Trading Time", f"{start}"],
        ["Finishing Trading Time", f"{finish}"],
        ["Closing Position After", f"{end}"],
    ]
    # Custom table format
    summary_table = tabulate(
        summary_data, headers=["Summary", "Values"], tablefmt="outline"
    )

    # Print the table
    print("\n[============ Trade Account Summary ==============]")
    print(summary_table)

risk_managment

risk_managment()

Show the risk management parameters

Source code in src/bbstrader/metatrader/trade.py
def risk_managment(self):
    """Show the risk management parameters"""

    loss = self.rm.currency_risk()["trade_loss"]
    trade_profit = self.rm.currency_risk()["trade_profit"]
    ok = "OK" if self.rm.is_risk_ok() else "Not OK"
    account_info = self.account.get_account_info()
    total_profit = round(self.get_stats()[1]["total_profit"], 2)
    currency = account_info.currency
    rates = self.account.get_currency_rates(self.symbol)

    account_data = [
        ["Account Name", account_info.name],
        ["Account Number", account_info.login],
        ["Account Server", account_info.server],
        ["Account Balance", f"{account_info.balance} {currency}"],
        ["Account Profit", f"{total_profit} {currency}"],
        ["Account Equity", f"{account_info.equity} {currency}"],
        ["Account Leverage", account_info.leverage],
        ["Account Margin", f"{round(account_info.margin, 2)} {currency}"],
        ["Account Free Margin", f"{account_info.margin_free} {currency}"],
        ["Maximum Drawdown", f"{self.rm.max_risk}%"],
        ["Risk Allowed", f"{round((self.rm.max_risk - self.rm.risk_level()), 2)}%"],
        ["Volume", f"{self.rm.volume()} {rates.get('pc')}"],
        ["Risk Per trade", f"{-self.rm.get_currency_risk()} {currency}"],
        ["Profit Expected Per trade", f"{self.rm.expected_profit()} {currency}"],
        ["Lot Size", f"{self.rm.get_lot()} Lots"],
        ["Stop Loss", f"{self.rm.get_stop_loss()} Points"],
        ["Loss Value Per Tick", f"{round(loss, 5)} {currency}"],
        ["Take Profit", f"{self.rm.get_take_profit()} Points"],
        ["Profit Value Per Tick", f"{round(trade_profit, 5)} {currency}"],
        ["Break Even", f"{self.rm.get_break_even()} Points"],
        ["Deviation", f"{self.rm.get_deviation()} Points"],
        ["Trading Time Interval", f"{self.rm.get_minutes()} Minutes"],
        ["Risk Level", ok],
        ["Maximum Trades", self.rm.max_trade()],
    ]
    # Custom table format
    print("\n[======= Account Risk Management Overview =======]")
    table = tabulate(
        account_data, headers=["Risk Metrics", "Values"], tablefmt="outline"
    )

    # Print the table
    print(table)

statistics

statistics(save=True, dir=None)

Print some statistics for the trading session and save to CSV if specified.

Parameters:

Name Type Description Default
save bool

Whether to save the statistics to a CSV file.

True
dir str

The directory to save the CSV file.

None
Source code in src/bbstrader/metatrader/trade.py
def statistics(self, save=True, dir=None):
    """
    Print some statistics for the trading session and save to CSV if specified.

    Args:
        save (bool, optional): Whether to save the statistics to a CSV file.
        dir (str, optional): The directory to save the CSV file.
    """
    stats, additional_stats = self.get_stats()

    profit = round(stats["profit"], 2)
    win_rate = stats["win_rate"]
    total_fees = round(stats["total_fees"], 3)
    average_fee = round(stats["average_fee"], 3)
    currency = self.account.info.currency
    net_profit = round((profit + total_fees), 2)
    trade_risk = round(self.rm.get_currency_risk() * -1, 2)

    # Formatting the statistics output
    session_data = [
        ["Total Trades", stats["deals"]],
        ["Winning Trades", stats["win_trades"]],
        ["Losing Trades", stats["loss_trades"]],
        ["Session Profit", f"{profit} {currency}"],
        ["Total Fees", f"{total_fees} {currency}"],
        ["Average Fees", f"{average_fee} {currency}"],
        ["Net Profit", f"{net_profit} {currency}"],
        ["Risk per Trade", f"{trade_risk} {currency}"],
        ["Expected Profit per Trade", f"{self.rm.expected_profit()} {currency}"],
        ["Risk Reward Ratio", self.rm.rr],
        ["Win Rate", f"{win_rate}%"],
        ["Sharpe Ratio", self.sharpe()],
        ["Trade Profitability", additional_stats["profitability"]],
    ]
    session_table = tabulate(
        session_data, headers=["Statistics", "Values"], tablefmt="outline"
    )

    if self.verbose:
        print("\n[========== Trading Session Statistics ===========]")
        print(session_table)

    if save and stats["deals"] > 0:
        today_date = datetime.now().strftime("%Y%m%d%H%M%S")
        statistics_dict = {item[0]: item[1] for item in session_data}
        stats_df = pd.DataFrame(statistics_dict, index=[0])

        dir = dir or ".sessions"
        os.makedirs(dir, exist_ok=True)
        symbol = self.symbol.split(".")[0] if "." in self.symbol else self.symbol

        filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
        filepath = os.path.join(dir, filename)
        stats_df.to_csv(filepath, index=False)
        LOGGER.info(f"Session statistics saved to {filepath}")

open_position

open_position(action: Buys | Sells, price: float | None = None, stoplimit: float | None = None, id: int | None = None, mm: bool = True, trail: bool = True, comment: str | None = None, symbol: str | None = None, volume: float | None = None, sl: float | None = None, tp: float | None = None) -> bool

Opens a Buy or Sell position (Market or Pending).

Parameters:

Name Type Description Default
action str

('BMKT', 'SMKT') for Market orders or ('BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT') for pending orders

required
price float

The price at which to open an order

None
stoplimit float

A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory). The pending order is not passed to the trading system until that moment

None
id int

The strategy id or expert Id

None
mm bool

Weither to put stop loss and tp or not

True
trail bool

Weither to trail the stop loss or not

True
comment str

The comment for the closing position

None
symbol str

The symbol to trade

None
volume float

The volume (lot) to trade

None
sl float

The stop loss price

None
tp float

The take profit price

None
Source code in src/bbstrader/metatrader/trade.py
def open_position(
    self,
    action: Buys | Sells,
    price: float | None = None,
    stoplimit: float | None = None,
    id: int | None = None,
    mm: bool = True,
    trail: bool = True,
    comment: str | None = None,
    symbol: str | None = None,
    volume: float | None = None,
    sl: float | None = None,
    tp: float | None = None,
) -> bool:
    """Opens a Buy or Sell position (Market or Pending).

    Args:
        action (str): (`'BMKT'`, `'SMKT'`) for Market orders
            or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
        price (float): The price at which to open an order
        stoplimit (float): A price a pending Limit order is set at
            when the price reaches the 'price' value (this condition is mandatory).
            The pending order is not passed to the trading system until that moment
        id (int): The strategy id or expert Id
        mm (bool): Weither to put stop loss and tp or not
        trail (bool): Weither to trail the stop loss or not
        comment (str): The comment for the closing position
        symbol (str): The symbol to trade
        volume (float): The volume (lot) to trade
        sl (float): The stop loss price
        tp (float): The take profit price
    """
    is_buy = action.startswith("B")
    symbol = symbol or self.symbol
    expert_id = id if id is not None else self.expert_id
    point = client.symbol_info(symbol).point
    tick = client.symbol_info_tick(symbol)

    req_price = None
    if "MKT" in action:
        req_price = tick.bid if is_buy else tick.ask
    else:
        if price is None:
            raise ValueError(f"Price is required for pending order: {action}")
        req_price = price

    mm_price = req_price
    if "TPLMT" in action:
        if stoplimit is None:
            raise ValueError(f"StopLimit price required for {action}")
        if (is_buy and stoplimit > req_price) or (
            not is_buy and stoplimit < req_price
        ):
            raise ValueError("Invalid StopLimit relationship to Price.")
        mm_price = stoplimit

    order_type, _ = self._order_type()[action]
    trade_action = (
        Mt5.TRADE_ACTION_DEAL if "MKT" in action else Mt5.TRADE_ACTION_PENDING
    )
    request = {
        "action": trade_action,
        "symbol": symbol,
        "volume": float(volume or self.rm.get_lot()),
        "type": order_type,
        "price": req_price,
        "deviation": self.rm.get_deviation(),
        "magic": expert_id,
        "comment": comment or f"@{self.expert_name}",
        "type_time": Mt5.ORDER_TIME_GTC,
        "type_filling": Mt5.ORDER_FILLING_FOK,
    }

    if "TPLMT" in action:
        request["stoplimit"] = stoplimit

    if mm:
        direction = 1 if is_buy else -1
        request["sl"] = sl or (
            mm_price - (direction * self.rm.get_stop_loss() * point)
        )
        request["tp"] = tp or (
            mm_price + (direction * self.rm.get_take_profit() * point)
        )

    self.break_even(mm=mm, id=expert_id, trail=trail)

    if self.check(comment):
        final_price = stoplimit if "TPLMT" in action else req_price
        return self.request_result(final_price, request, action)

    return False

open_buy_position

open_buy_position(**kwargs)

Open a buy position or order.

See Trade.open_position for the kwargs parameters.

Source code in src/bbstrader/metatrader/trade.py
def open_buy_position(self, **kwargs):
    """
    Open a buy position or order.

    See Trade.open_position for the ``kwargs`` parameters.
    """
    return self.open_position(action=kwargs.pop("action", "BMKT"), **kwargs)

open_sell_position

open_sell_position(**kwargs)

Open a sell position or order.

See Trade.open_position for the kwargs parameters.

Source code in src/bbstrader/metatrader/trade.py
def open_sell_position(self, **kwargs):
    """
    Open a sell position or order.

    See Trade.open_position for the ``kwargs`` parameters.
    """
    return self.open_position(action=kwargs.pop("action", "SMKT"), **kwargs)

check

check(comment)

Verify if all conditions for taking a position are valide, These conditions are based on the Maximum risk ,daily risk, the starting, the finishing, and ending trading time.

Parameters:

Name Type Description Default
comment str

The comment for the closing position

required
Source code in src/bbstrader/metatrader/trade.py
def check(self, comment):
    """
    Verify if all conditions for taking a position are valide,
    These conditions are based on the Maximum risk ,daily risk,
    the starting, the finishing, and ending trading time.

    Args:
        comment (str): The comment for the closing position
    """

    def _check(txt: str = ""):
        if (
            self.positive_profit(id=self.expert_id)
            or self.get_current_positions() is None
        ):
            self.close_positions(position_type="all")
            LOGGER.info(txt)
            self.statistics(save=True)

    if self.days_end():
        LOGGER.warning(f"End of the trading Day, SYMBOL={self.symbol}")
        return False
    elif not self.trading_time():
        LOGGER.warning(f"Not Trading time, SYMBOL={self.symbol}")
        return False
    elif not self.rm.is_risk_ok():
        LOGGER.warning(f"Account Risk not allowed, SYMBOL={self.symbol}")
        _check(comment)
        return False
    elif self.is_max_trades_reached():
        LOGGER.warning(f"Maximum trades reached for Today, SYMBOL={self.symbol}")
        return False
    elif self.profit_target():
        _check(f"Profit target Reached !!! SYMBOL={self.symbol}")
    return True

request_result

request_result(price: float, request: dict[str, Any], type: Buys | Sells)

Check if a trading order has been sent correctly

Parameters:

Name Type Description Default
price float

Price for opening the position

required
request Dict[str, Any]

A trade request to sent to Mt5.order_sent()

required
all detail in request can be found here https

//www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py

required
type str

The type of the order (BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)

required
Source code in src/bbstrader/metatrader/trade.py
def request_result(self, price: float, request: dict[str, Any], type: Buys | Sells):
    """
    Check if a trading order has been sent correctly

    Args:
        price (float): Price for opening the position
        request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
        all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py

        type (str): The type of the order `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
    """
    # Send a trading request
    # Check the execution result
    pos = self._order_type()[type][1]
    addtionnal = f", SYMBOL={self.symbol}"
    result = None
    try:
        client.order_check(request)
        result = client.order_send(request)
    except Exception as e:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
        return False
    if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
        if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL:  # 10030
            for fill in FILLING_TYPE:
                request["type_filling"] = fill
                result = client.order_send(request)
                if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
                    break
        elif result and result.retcode == Mt5.TRADE_RETCODE_INVALID_VOLUME:  # 10014
            new_volume = int(request["volume"])
            if new_volume >= 1:
                request["volume"] = new_volume
                result = client.order_send(request)
        elif result and result.retcode not in self._retcodes:
            self._retcodes.append(result.retcode)
            msg = trade_retcode_message(result.retcode) if result else "N/A"
            retcode = result.retcode if result else None
            LOGGER.error(
                f"Trade Order Request, RETCODE={retcode}: {msg}{addtionnal}"
            )
        elif result and result.retcode in [
            Mt5.TRADE_RETCODE_CONNECTION,
            Mt5.TRADE_RETCODE_TIMEOUT,
        ]:
            tries = 0
            while result and result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
                try:
                    client.order_check(request)
                    result = client.order_send(request)
                except Exception as e:
                    msg = trade_retcode_message(result.retcode) if result else "N/A"
                    LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
                    return False
                if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
                    break
                tries += 1
    # Print the result
    if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
        msg = trade_retcode_message(result.retcode)
        LOGGER.info(f"Trade Order {msg}{addtionnal}")
        if type != "BMKT" and type != "SMKT":
            self.opened_orders.append(result.order)
        long_msg = (
            f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{round(price, 5)}, "
            f"Lot(s): {result.volume}, Sl: {self.rm.get_stop_loss()}, "
            f"Tp: {self.rm.get_take_profit()}"
        )
        LOGGER.info(long_msg)
        if type == "BMKT" or type == "SMKT":
            self.opened_positions.append(result.order)
            positions = self.account.get_positions(symbol=self.symbol)
            if positions is not None:
                for position in positions:
                    if position.ticket == result.order:
                        if position.type == 0:
                            order_type = "BUY"
                            self.buy_positions.append(position.ticket)
                        else:
                            order_type = "SELL"
                            self.sell_positions.append(position.ticket)
                        profit = round(client.account_info().profit, 5)
                        order_info = (
                            f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open, 5)}, "
                            f"Sl: @{round(position.sl, 5)} Tp: @{round(position.tp, 5)}"
                        )
                        LOGGER.info(order_info)
                        pos_info = (
                            f"3. [OPEN POSITIONS ON {self.symbol} = {len(positions)}, ACCOUNT OPEN PnL = {profit} "
                            f"{client.account_info().currency}]\n"
                        )
                        LOGGER.info(pos_info)
        return True
    else:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        retcode = result.retcode if result else None
        LOGGER.error(
            f"Unable to Open Position, RETCODE={retcode}: {msg}{addtionnal}"
        )
        return False

get_filtered_tickets

get_filtered_tickets(id: int | None = None, filter_type: str | None = None, th=None) -> list[int] | None

Get tickets for positions or orders based on filters.

Parameters:

Name Type Description Default
id int

The strategy id or expert Id

None
filter_type str

Filter type to apply on the tickets, - orders are current open orders - buy_stops are current buy stop orders - sell_stops are current sell stop orders - buy_limits are current buy limit orders - sell_limits are current sell limit orders - buy_stop_limits are current buy stop limit orders - sell_stop_limits are current sell stop limit orders - positions are all current open positions - buys and sells are current buy or sell open positions - profitables are current open position that have a profit greater than a threshold - losings are current open position that have a negative profit

None
th bool

the minimum treshold for winning position (only relevant when filter_type is 'profitables')

None

Returns:

Type Description
list[int] | None

List[int] | None: A list of filtered tickets or None if no tickets match the criteria.

Source code in src/bbstrader/metatrader/trade.py
def get_filtered_tickets(
    self, id: int | None = None, filter_type: str | None = None, th=None
) -> list[int] | None:
    """
    Get tickets for positions or orders based on filters.

    Args:
        id (int): The strategy id or expert Id
        filter_type (str): Filter type to apply on the tickets,
            - `orders` are current open orders
            - `buy_stops` are current buy stop orders
            - `sell_stops` are current sell stop orders
            - `buy_limits` are current buy limit orders
            - `sell_limits` are current sell limit orders
            - `buy_stop_limits` are current buy stop limit orders
            - `sell_stop_limits` are current sell stop limit orders
            - `positions` are all current open positions
            - `buys` and `sells` are current buy or sell open positions
            - `profitables` are current open position that have a profit greater than a threshold
            - `losings` are current open position that have a negative profit
        th (bool): the minimum treshold for winning position
            (only relevant when filter_type is 'profitables')

    Returns:
        List[int] | None: A list of filtered tickets
            or None if no tickets match the criteria.
    """
    Id = id if id is not None else self.expert_id
    POSITIONS = ["positions", "buys", "sells", "profitables", "losings"]

    if filter_type not in POSITIONS:
        items = self.account.get_orders(symbol=self.symbol)
    else:
        items = self.account.get_positions(symbol=self.symbol)

    filtered_tickets = []

    if items is None:
        return []
    for item in items:
        if item.magic == Id:
            if filter_type == "buys" and item.type != 0:
                continue
            if filter_type == "sells" and item.type != 1:
                continue
            if filter_type == "losings" and item.profit > 0:
                continue
            if filter_type == "profitables" and not self.win_trade(item, th=th):
                continue
            if (
                filter_type == "buy_stops"
                and item.type != self._order_type()["BSTP"][0]
            ):
                continue
            if (
                filter_type == "sell_stops"
                and item.type != self._order_type()["SSTP"][0]
            ):
                continue
            if (
                filter_type == "buy_limits"
                and item.type != self._order_type()["BLMT"][0]
            ):
                continue
            if (
                filter_type == "sell_limits"
                and item.type != self._order_type()["SLMT"][0]
            ):
                continue
            if (
                filter_type == "buy_stop_limits"
                and item.type != self._order_type()["BSTPLMT"][0]
            ):
                continue
            if (
                filter_type == "sell_stop_limits"
                and item.type != self._order_type()["SSTPLMT"][0]
            ):
                continue
            filtered_tickets.append(item.ticket)
    return filtered_tickets

positive_profit

positive_profit(th: float | None = None, id: int | None = None, account: bool = True) -> bool

Check is the total profit on current open positions Is greater than a minimum profit express as percentage of the profit target.

Parameters:

Name Type Description Default
th float

The minimum profit target on current positions

None
id int

The strategy id or expert Id

None
account bool

Weither to check positions on the account or on the symbol

True
Source code in src/bbstrader/metatrader/trade.py
def positive_profit(
    self, th: float | None = None, id: int | None = None, account: bool = True
) -> bool:
    """
    Check is the total profit on current open positions
    Is greater than a minimum profit express as percentage
    of the profit target.

    Args:
        th (float): The minimum profit target on current positions
        id (int): The strategy id or expert Id
        account (bool): Weither to check positions on the account or on the symbol
    """
    if account and id is None:
        # All open positions no matter the symbol or strategy or expert
        positions = self.account.get_positions()
    elif account and id is not None:
        # All open positions for a specific strategy or expert no matter the symbol
        positions = self.account.get_positions()
        if positions is not None:
            positions = [position for position in positions if position.magic == id]
    elif not account and id is None:
        # All open positions for the current symbol no matter the strategy or expert
        positions = self.account.get_positions(symbol=self.symbol)
    elif not account and id is not None:
        # All open positions for the current symbol and a specific strategy or expert
        positions = self.account.get_positions(symbol=self.symbol)
        if positions is not None:
            positions = [position for position in positions if position.magic == id]

    if positions is not None:
        profit = 0.0
        balance = client.account_info().balance
        target = round((balance * self.target) / 100, 2)
        for position in positions:
            profit += position.profit
        fees = self.get_average_fees()
        current_profit = profit + fees
        th_profit = (target * th) / 100 if th is not None else (target * 0.01)
        return current_profit >= th_profit
    return False

break_even

break_even(mm=True, id: int | None = None, trail: bool | None = True, stop_trail: int | str = None, trail_after_points: int | str = None, be_plus_points: int | None = None)

Manages the break-even level of a trading position.

This function checks whether it is time to set a break-even stop loss for an open position. If the break-even level is already set, it monitors price movement and updates the stop loss accordingly if the trail parameter is enabled.

When trail is enabled, the function dynamically adjusts the break-even level based on the trail_after_points and stop_trail parameters.

Parameters:

Name Type Description Default
id int

The strategy ID or expert ID.

None
mm bool

Whether to manage the position or not.

True
trail bool

Whether to trail the stop loss or not.

True
stop_trail int

Number of points to trail the stop loss by. It represent the distance from the current price to the stop loss.

None
trail_after_points (int, str)

Number of points in profit from where the strategy will start to trail the stop loss. If set to str, it must be one of the following values: - 'SL' to trail the stop loss after the profit reaches the stop loss level in points. - 'TP' to trail the stop loss after the profit reaches the take profit level in points. - 'BE' to trail the stop loss after the profit reaches the break-even level in points.

None
be_plus_points int

Number of points to add to the break-even level. Represents the minimum profit to secure.

None
Source code in src/bbstrader/metatrader/trade.py
def break_even(
    self,
    mm=True,
    id: int | None = None,
    trail: bool | None = True,
    stop_trail: int | str = None,
    trail_after_points: int | str = None,
    be_plus_points: int | None = None,
):
    """
    Manages the break-even level of a trading position.

    This function checks whether it is time to set a break-even stop loss for an open position.
    If the break-even level is already set, it monitors price movement and updates the stop loss
    accordingly if the `trail` parameter is enabled.

    When `trail` is enabled, the function dynamically adjusts the break-even level based on the
    `trail_after_points` and `stop_trail` parameters.

    Args:
        id (int): The strategy ID or expert ID.
        mm (bool): Whether to manage the position or not.
        trail (bool): Whether to trail the stop loss or not.
        stop_trail (int): Number of points to trail the stop loss by.
            It represent the distance from the current price to the stop loss.
        trail_after_points (int, str): Number of points in profit
            from where the strategy will start to trail the stop loss.
            If set to str, it must be one of the following values:
            - 'SL' to trail the stop loss after the profit reaches the stop loss level in points.
            - 'TP' to trail the stop loss after the profit reaches the take profit level in points.
            - 'BE' to trail the stop loss after the profit reaches the break-even level in points.
        be_plus_points (int): Number of points to add to the break-even level.
            Represents the minimum profit to secure.
    """

    if not mm:
        return False

    Id = id if id is not None else self.expert_id
    positions = self.account.get_positions(symbol=self.symbol)
    be = self.rm.get_break_even()
    if trail_after_points is not None:
        if isinstance(trail_after_points, int):
            assert trail_after_points > be, (
                "trail_after_points must be greater than break even or set to None"
            )
        trail_after_points = self._get_trail_after_points(trail_after_points)

    if not positions:
        return False

    for position in positions:
        if position.magic == Id:
            symbol_info = client.symbol_info(self.symbol)

            point = symbol_info.point
            digits = symbol_info.digits

            points = position.profit * (
                symbol_info.trade_tick_size
                / symbol_info.trade_tick_value
                / position.volume
            )
            break_even = float(points / point) >= be
            if not break_even:
                continue
            # Check if break-even has already been set for this position
            if position.ticket not in self.break_even_status:
                price = None
                if be_plus_points is not None:
                    price = position.price_open + (be_plus_points * point)
                self.set_break_even(position, be, price=price)
                self.break_even_status.append(position.ticket)
                self.break_even_points[position.ticket] = be
            else:
                # Skip this if the trail is not set to True
                if not trail:
                    continue
                # Check if the price has moved favorably
                new_be = (
                    round(be * 0.10) if be_plus_points is None else be_plus_points
                )
                if trail_after_points is not None:
                    if position.ticket not in self.trail_after_points:
                        # This ensures that the position rich the minimum points required
                        # before the trail can be set
                        new_be = trail_after_points - be
                        self.trail_after_points.append(position.ticket)
                new_be_points = self.break_even_points[position.ticket] + new_be
                favorable_move = float(points / point) >= new_be_points
                if not favorable_move:
                    continue
                # This allows the position to go to take profit in case of a swing trade
                # If is a scalping position, we can set the stop_trail close to the current price.
                trail_points = (
                    round(be * 0.50) if stop_trail is None else stop_trail
                )
                # Calculate the new break-even level and price
                if position.type == 0:
                    # This level validate the favorable move of the price
                    new_level = round(
                        position.price_open + (new_be_points * point),
                        digits,
                    )
                    # This price is set away from the current price by the trail_points
                    new_price = round(
                        position.price_current - (trail_points * point),
                        digits,
                    )
                    if new_price < position.sl:
                        new_price = position.sl
                elif position.type == 1:
                    new_level = round(
                        position.price_open - (new_be_points * point),
                        digits,
                    )
                    new_price = round(
                        position.price_current + (trail_points * point),
                        digits,
                    )
                    if new_price > position.sl:
                        new_price = position.sl
                return self.set_break_even(
                    position, be, price=new_price, level=new_level
                )
    return False

set_break_even

set_break_even(position: TradePosition, be: int, price: float | None = None, level: float | None = None)

Sets the break-even level for a given trading position.

Parameters:

Name Type Description Default
position TradePosition

The trading position for which the break-even is to be set. This is the value return by mt5.positions_get().

required
be int

The break-even level in points.

required
level float

The break-even level in price, if set to None , it will be calated automaticaly.

None
price float

The break-even price, if set to None , it will be calated automaticaly.

None
Source code in src/bbstrader/metatrader/trade.py
def set_break_even(
    self,
    position: TradePosition,
    be: int,
    price: float | None = None,
    level: float | None = None,
):
    """
    Sets the break-even level for a given trading position.

    Args:
        position (TradePosition): The trading position for which the break-even is to be set.
            This is the value return by `mt5.positions_get()`.
        be (int): The break-even level in points.
        level (float): The break-even level in price, if set to None , it will be calated automaticaly.
        price (float): The break-even price, if set to None , it will be calated automaticaly.
    """

    symbol_info = client.symbol_info(self.symbol)
    average_fee = abs(self.get_average_fees())
    point_value = self.rm.currency_risk().get("trade_profit", 1)
    fees_points = round((average_fee / point_value), 3) if point_value != 0 else 0

    is_buy = position.type == 0
    direction = 1 if is_buy else -1
    if not position.profit > 0:
        return False
    calc_be_level = position.price_open + (direction * be * symbol_info.point)
    calc_be_price = position.price_open + (
        direction * (fees_points + symbol_info.spread) * symbol_info.point
    )
    if price is None:
        be_price = calc_be_price
    else:
        be_price = (
            max(price, calc_be_price) if is_buy else min(price, calc_be_price)
        )
    be_level = calc_be_level if level is None else level
    tick = client.symbol_info_tick(self.symbol)
    send_request = (tick.ask > be_level) if is_buy else (tick.bid < be_level)

    if send_request:
        request = {
            "action": Mt5.TRADE_ACTION_SLTP,
            "position": position.ticket,
            "sl": round(be_price, symbol_info.digits),
            "tp": position.tp,
        }
        return self.break_even_request(
            position.ticket, round(be_price, symbol_info.digits), request
        )
    return False

break_even_request

break_even_request(tiket, price, request)

Send a request to set the stop loss to break even for a given trading position.

Parameters:

Name Type Description Default
tiket int

The ticket number of the trading position.

required
price float

The price at which the stop loss is to be set.

required
request dict

The request to set the stop loss to break even.

required
Source code in src/bbstrader/metatrader/trade.py
def break_even_request(self, tiket, price, request):
    """
    Send a request to set the stop loss to break even for a given trading position.

    Args:
        tiket (int): The ticket number of the trading position.
        price (float): The price at which the stop loss is to be set.
        request (dict): The request to set the stop loss to break even.
    """
    addtionnal = f", SYMBOL={self.symbol}"
    result = None
    try:
        client.order_check(request)
        result = client.order_send(request)
    except Exception as e:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        LOGGER.error(f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}")
        return False
    if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
        msg = trade_retcode_message(result.retcode)
        if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
            LOGGER.error(
                f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}"
            )
        tries = 0
        while result and result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
            if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
                break
            else:
                try:
                    client.order_check(request)
                    result = client.order_send(request)
                except Exception as e:
                    msg = trade_retcode_message(result.retcode) if result else "N/A"
                    LOGGER.error(
                        f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}"
                    )
                    return False
                if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
                    break
            tries += 1
    if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
        msg = trade_retcode_message(result.retcode)
        LOGGER.info(f"Break-Even Order {msg}{addtionnal}")
        info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{round(price, 5)}"
        LOGGER.info(info)
        self.break_even_status.append(tiket)
        return True
    return False

win_trade

win_trade(position: TradePosition, th: int | None = None) -> bool

Determines if a position has met the minimum 'win' threshold in points.

Source code in src/bbstrader/metatrader/trade.py
def win_trade(self, position: TradePosition, th: int | None = None) -> bool:
    """
    Determines if a position has met the minimum 'win' threshold in points.
    """
    points = self._convert_profit_to_points(position)
    if th is not None:
        win_threshold = th
    else:
        win_threshold = self._calculate_dynamic_threshold()

    is_profitable = points >= win_threshold
    not_processed = position.ticket not in self.break_even_status

    return is_profitable and not_processed

profit_target

profit_target() -> bool

Checks if the net profit for today's deals has reached the percentage target.

Source code in src/bbstrader/metatrader/trade.py
def profit_target(self) -> bool:
    """Checks if the net profit for today's deals has reached the percentage target."""
    from bbstrader.api import trade_object_to_df

    balance = client.account_info().balance
    target_amount = (balance * self.target) / 100

    opened_positions = self.get_today_deals(group=self.symbol)
    history_df = trade_object_to_df(opened_positions)

    if history_df.empty:
        return False
    net_profit = history_df[["profit", "commission", "swap", "fee"]].sum().sum()

    return net_profit >= target_amount

close_request

close_request(request: dict, type: str)

Close a trading order or position

Parameters:

Name Type Description Default
request dict

The request to close a trading order or position

required
type str

Type of the request ('order', 'position')

required
Source code in src/bbstrader/metatrader/trade.py
def close_request(self, request: dict, type: str):
    """
    Close a trading order or position

    Args:
        request (dict): The request to close a trading order or position
        type (str): Type of the request ('order', 'position')
    """
    ticket = request[type]
    addtionnal = f", SYMBOL={self.symbol}"
    result = None
    try:
        client.order_check(request)
        result = client.order_send(request)
    except Exception as e:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        LOGGER.error(
            f"Closing {type.capitalize()} Request, RETCODE={msg}{addtionnal}, Error: {e}"
        )
        return False

    if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
        if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL:  # 10030
            for fill in FILLING_TYPE:
                request["type_filling"] = fill
                result = client.order_send(request)
                if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
                    break
        elif result and result.retcode not in self._retcodes:
            self._retcodes.append(result.retcode)
            msg = trade_retcode_message(result.retcode)
            LOGGER.error(
                f"Closing Order Request, {type.capitalize()}: #{ticket}, "
                f"RETCODE={result.retcode}: {msg}{addtionnal}"
            )
        else:
            tries = 0
            while result and result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
                try:
                    client.order_check(request)
                    result = client.order_send(request)
                except Exception as e:
                    msg = trade_retcode_message(result.retcode) if result else "N/A"
                    LOGGER.error(
                        f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}"
                    )
                    return False
                if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
                    break
                tries += 1
    if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
        msg = trade_retcode_message(result.retcode)
        LOGGER.info(f"Closing Order {msg}{addtionnal}")
        info = (
            f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol},"
            f"Price: @{round(request.get('price', 0.0), 5)}"
        )
        LOGGER.info(info)
        return True
    else:
        return False

modify_order

modify_order(ticket: int, price: float | None = None, stoplimit: float | None = None, sl: float | None = None, tp: float | None = None)

Modify an open order by it ticket

Parameters:

Name Type Description Default
ticket int

Order ticket to modify (e.g TradeOrder.ticket)

required
price float

The price at which to modify the order

None
stoplimit float

A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory). The pending order is not passed to the trading system until that moment

None
sl float

The stop loss in points

None
tp float

The take profit in points

None
Source code in src/bbstrader/metatrader/trade.py
def modify_order(
    self,
    ticket: int,
    price: float | None = None,
    stoplimit: float | None = None,
    sl: float | None = None,
    tp: float | None = None,
):
    """
    Modify an open order by it ticket

    Args:
        ticket (int): Order ticket to modify (e.g TradeOrder.ticket)
        price (float): The price at which to modify the order
        stoplimit (float): A price a pending Limit order is set at
            when the price reaches the 'price' value (this condition is mandatory).
            The pending order is not passed to the trading system until that moment
        sl (float): The stop loss in points
        tp (float): The take profit in points
    """
    orders = self.account.get_orders(ticket=ticket) or []
    if len(orders) == 0:
        LOGGER.error(
            f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={round(price, 5) if price else 'N/A'}"
        )
        return False
    order = orders[0]
    request = {
        "action": Mt5.TRADE_ACTION_MODIFY,
        "order": ticket,
        "price": price or order.price_open,
        "sl": sl or order.sl,
        "tp": tp or order.tp,
        "stoplimit": stoplimit or order.price_stoplimit,
    }
    try:
        client.order_check(request)
        result = client.order_send(request)
    except Exception as e:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        LOGGER.error(f"Unable to modify Order #{ticket}, RETCODE={msg}, Error: {e}")
        return False
    if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
        LOGGER.info(
            f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={round(request['price'], 5)},"
            f"SL={round(request['sl'], 5)}, TP={round(request['tp'], 5)}, STOP_LIMIT={round(request['stoplimit'], 5)}"
        )
        return True
    else:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        retcode = result.retcode if result else None
        LOGGER.error(
            f"Unable to modify Order #{ticket}, RETCODE={retcode}: {msg}, SYMBOL={self.symbol}"
        )
        return False

close_order

close_order(ticket: int, id: int | None = None, comment: str | None = None)

Close an open order by it ticket

Parameters:

Name Type Description Default
ticket int

Order ticket to close (e.g TradeOrder.ticket)

required
id int

The unique ID of the Expert or Strategy

None
comment str

Comment for the closing position

None

Returns: - True if order closed, False otherwise

Source code in src/bbstrader/metatrader/trade.py
def close_order(
    self, ticket: int, id: int | None = None, comment: str | None = None
):
    """
    Close an open order by it ticket

    Args:
        ticket (int): Order ticket to close (e.g TradeOrder.ticket)
        id (int): The unique ID of the Expert or Strategy
        comment (str): Comment for the closing position

    Returns:
    -   True if order closed, False otherwise
    """
    request = {
        "action": Mt5.TRADE_ACTION_REMOVE,
        "symbol": self.symbol,
        "order": ticket,
        "magic": id if id is not None else self.expert_id,
        "comment": f"@{self.expert_name}" if comment is None else comment,
    }
    return self.close_request(request, type="order")

close_position

close_position(ticket: int, id: int | None = None, pct: float | None = 1.0, comment: str | None = None, symbol: str | None = None) -> bool

Close an open position by it ticket

Parameters:

Name Type Description Default
ticket int

Positon ticket to close (e.g TradePosition.ticket)

required
id int

The unique ID of the Expert or Strategy

None
pct float

Percentage of the position to close

1.0
comment str

Comment for the closing position

None

Returns: - True if position closed, False otherwise

Source code in src/bbstrader/metatrader/trade.py
def close_position(
    self,
    ticket: int,
    id: int | None = None,
    pct: float | None = 1.0,
    comment: str | None = None,
    symbol: str | None = None,
) -> bool:
    """
    Close an open position by it ticket

    Args:
        ticket (int): Positon ticket to close (e.g TradePosition.ticket)
        id (int): The unique ID of the Expert or Strategy
        pct (float): Percentage of the position to close
        comment (str): Comment for the closing position

    Returns:
    -   True if position closed, False otherwise
    """
    symbol = symbol or self.symbol
    Id = id if id is not None else self.expert_id
    positions = self.account.get_positions(ticket=ticket)
    deviation = self.rm.get_deviation()
    if positions is not None and len(positions) == 1:
        position = positions[0]
        if position.ticket == ticket and position.magic == Id:
            buy = position.type == 0
            request = {
                "action": Mt5.TRADE_ACTION_DEAL,
                "symbol": symbol,
                "volume": (position.volume * pct),
                "type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
                "position": ticket,
                "price": position.price_current,
                "deviation": deviation,
                "comment": f"@{self.expert_name}" if comment is None else comment,
                "type_time": Mt5.ORDER_TIME_GTC,
                "type_filling": Mt5.ORDER_FILLING_FOK,
            }
            return self.close_request(request, type="position")
    return False

bulk_close

bulk_close(tickets: list, tikets_type: Literal['positions', 'orders'], close_func: Callable, order_type: str, id: int | None = None, comment: str | None = None)

Close multiple orders or positions at once.

Parameters:

Name Type Description Default
tickets List

List of tickets to close

required
tikets_type str

Type of tickets to close ('positions', 'orders')

required
close_func Callable

The function to close the tickets

required
order_type str

Type of orders or positions to close

required
id int

The unique ID of the Expert or Strategy

None
comment str

Comment for the closing position

None
Source code in src/bbstrader/metatrader/trade.py
def bulk_close(
    self,
    tickets: list,
    tikets_type: Literal["positions", "orders"],
    close_func: Callable,
    order_type: str,
    id: int | None = None,
    comment: str | None = None,
):
    """
    Close multiple orders or positions at once.

    Args:
        tickets (List): List of tickets to close
        tikets_type (str): Type of tickets to close ('positions', 'orders')
        close_func (Callable): The function to close the tickets
        order_type (str): Type of orders or positions to close
        id (int): The unique ID of the Expert or Strategy
        comment (str): Comment for the closing position
    """
    if order_type == "all":
        order_type = "open"

    if not tickets:
        return
    failed_tickets = []
    with ThreadPoolExecutor(max_workers=min(len(tickets), 20)) as executor:
        future_to_ticket = {
            executor.submit(close_func, ticket, id=id, comment=comment): ticket
            for ticket in tickets
        }
        for future in as_completed(future_to_ticket):
            ticket = future_to_ticket[future]
            try:
                success = future.result()
                if not success:
                    failed_tickets.append(ticket)
            except Exception as exc:
                LOGGER.error(f"Ticket {ticket} generated an exception: {exc}")
                failed_tickets.append(ticket)
    if not failed_tickets:
        LOGGER.info(
            f"ALL {order_type.upper()} {tikets_type.upper()} closed, SYMBOL={self.symbol}."
        )
    else:
        LOGGER.info(
            f"{len(failed_tickets)}/{len(tickets)} {order_type.upper()} {tikets_type.upper()} NOT closed, SYMBOL={self.symbol}"
        )

close_orders

close_orders(order_type: Orders, id: int | None = None, comment: str | None = None)

Parameters:

Name Type Description Default
order_type str

Type of orders to close ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')

required
id int

The unique ID of the Expert or Strategy

None
comment str

Comment for the closing position

None
Source code in src/bbstrader/metatrader/trade.py
def close_orders(
    self,
    order_type: Orders,
    id: int | None = None,
    comment: str | None = None,
):
    """
    Args:
        order_type (str): Type of orders to close
            ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
        id (int): The unique ID of the Expert or Strategy
        comment (str): Comment for the closing position
    """
    id = id if id is not None else self.expert_id
    if order_type == "all":
        orders = self.get_current_orders(id=id)
    elif order_type == "buy_stops":
        orders = self.get_current_buy_stops(id=id)
    elif order_type == "sell_stops":
        orders = self.get_current_sell_stops(id=id)
    elif order_type == "buy_limits":
        orders = self.get_current_buy_limits(id=id)
    elif order_type == "sell_limits":
        orders = self.get_current_sell_limits(id=id)
    elif order_type == "buy_stop_limits":
        orders = self.get_current_buy_stop_limits(id=id)
    elif order_type == "sell_stop_limits":
        orders = self.get_current_sell_stop_limits(id=id)
    else:
        LOGGER.error(f"Invalid order type: {order_type}")
        return
    self.bulk_close(
        orders, "orders", self.close_order, order_type, id=id, comment=comment
    )

close_positions

close_positions(position_type: Positions, id: int | None = None, comment: str | None = None)

Parameters:

Name Type Description Default
position_type str

Type of positions to close ('all', 'buy', 'sell', 'profitable', 'losing')

required
id int

The unique ID of the Expert or Strategy

None
comment str

Comment for the closing position

None
Source code in src/bbstrader/metatrader/trade.py
def close_positions(
    self,
    position_type: Positions,
    id: int | None = None,
    comment: str | None = None,
):
    """
    Args:
        position_type (str): Type of positions to close ('all', 'buy', 'sell', 'profitable', 'losing')
        id (int): The unique ID of the Expert or Strategy
        comment (str): Comment for the closing position
    """
    id = id if id is not None else self.expert_id
    if position_type == "all":
        positions = self.get_current_positions(id=id)
    elif position_type == "buy":
        positions = self.get_current_buys(id=id)
    elif position_type == "sell":
        positions = self.get_current_sells(id=id)
    elif position_type == "profitable":
        positions = self.get_current_profitables(id=id)
    elif position_type == "losing":
        positions = self.get_current_losings(id=id)
    else:
        LOGGER.error(f"Invalid position type: {position_type}")
        return
    self.bulk_close(
        positions,
        "positions",
        self.close_position,
        position_type,
        id=id,
        comment=comment,
    )

is_max_trades_reached

is_max_trades_reached() -> bool

Check if the maximum number of trades for the day has been reached.

:return: bool

Source code in src/bbstrader/metatrader/trade.py
def is_max_trades_reached(self) -> bool:
    """
    Check if the maximum number of trades for the day has been reached.

    :return: bool
    """
    max_trades = self.rm.max_trade()
    today_deals = self.get_today_deals(group=self.symbol)
    negative_deals = [deal for deal in today_deals if deal.profit < 0]
    return len(negative_deals) >= max_trades

get_stats

get_stats() -> tuple[dict[str, Any], dict[str, Any]]

Retrieves aggregated session and historical trading performance.

Source code in src/bbstrader/metatrader/trade.py
def get_stats(self) -> tuple[dict[str, Any], dict[str, Any]]:
    """Retrieves aggregated session and historical trading performance."""
    today_deals = self.get_today_deals(group=self.symbol)
    stats1 = self._calculate_session_stats(today_deals)
    stats2 = self._calculate_historical_stats()

    return stats1, stats2

sharpe

sharpe()

Calculate the Sharpe ratio of a returns stream based on a number of trading periods. The function assumes that the returns are the excess of those compared to a benchmark.

Source code in src/bbstrader/metatrader/trade.py
def sharpe(self):
    """
    Calculate the Sharpe ratio of a returns stream
    based on a number of trading periods.
    The function assumes that the returns are the excess of
    those compared to a benchmark.
    """
    import warnings

    warnings.filterwarnings("ignore")
    history = self.account.get_trades_history()
    if history is None or len(history) < 2:
        return 0.0
    df = history.iloc[1:]
    profit = df[["profit", "commission", "fee", "swap"]].sum(axis=1)
    returns = profit.pct_change(fill_method=None)
    periods = self.rm.max_trade() * 252
    sharpe = qs.stats.sharpe(returns, periods=periods)

    return round(sharpe, 3)

days_end

days_end() -> bool

Check if it is the end of the trading day.

Source code in src/bbstrader/metatrader/trade.py
def days_end(self) -> bool:
    """Check if it is the end of the trading day."""
    fmt = "%H:%M"
    now = datetime.now().time()
    end = datetime.strptime(self.end, fmt).time()
    if self.broker_tz:
        now = self.account.broker.get_broker_time(self.current_time(), fmt).time()
        end = self.account.broker.get_broker_time(self.end, fmt).time()
    if now >= end:
        return True
    return False

trading_time

trading_time()

Check if it is time to trade.

Source code in src/bbstrader/metatrader/trade.py
def trading_time(self):
    """Check if it is time to trade."""
    fmt = "%H:%M"
    now = datetime.now()
    start = datetime.strptime(self.start, fmt).time()
    end = datetime.strptime(self.finishing, fmt).time()
    if self.broker_tz:
        now = self.account.broker.get_broker_time(self.current_time(), fmt).time()
        start = self.account.broker.get_broker_time(self.start, fmt).time()
        now = self.account.broker.get_broker_time(self.finishing, fmt).time()
    if start <= now.time() <= end:
        return True
    return False

TimeFrame

Bases: Enum

Rrepresent a time frame object

SymbolType

Bases: Enum

Represents the type of a symbol.

RateInfo

Bases: NamedTuple

Reprents a candle (bar) for a specified period. * time: Time in seconds since 1970.01.01 00:00 * open: Open price * high: High price * low: Low price * close: Close price * tick_volume: Tick volume * spread: Spread value * real_volume: Real volume

InvalidBroker

InvalidBroker(message='Invalid broker.')

Bases: Exception

Exception raised for invalid broker errors.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Invalid broker."):
    super().__init__(message)

GenericFail

GenericFail(message='Generic fail')

Bases: MT5TerminalError

Exception raised for generic failure.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Generic fail"):
    super().__init__(MT5.RES_E_FAIL, message)

InvalidParams

InvalidParams(message='Invalid arguments or parameters.')

Bases: MT5TerminalError

Exception raised for invalid arguments or parameters.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Invalid arguments or parameters."):
    super().__init__(MT5.RES_E_INVALID_PARAMS, message)

HistoryNotFound

HistoryNotFound(message='No history found.')

Bases: MT5TerminalError

Exception raised when no history is found.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="No history found."):
    super().__init__(MT5.RES_E_NOT_FOUND, message)

InvalidVersion

InvalidVersion(message='Invalid version.')

Bases: MT5TerminalError

Exception raised for an invalid version.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Invalid version."):
    super().__init__(MT5.RES_E_INVALID_VERSION, message)

AuthFailed

AuthFailed(message='Authorization failed.')

Bases: MT5TerminalError

Exception raised for authorization failure.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Authorization failed."):
    super().__init__(MT5.RES_E_AUTH_FAILED, message)

UnsupportedMethod

UnsupportedMethod(message='Unsupported method.')

Bases: MT5TerminalError

Exception raised for an unsupported method.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Unsupported method."):
    super().__init__(MT5.RES_E_UNSUPPORTED, message)

AutoTradingDisabled

AutoTradingDisabled(message='Auto-trading is disabled.')

Bases: MT5TerminalError

Exception raised when auto-trading is disabled.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Auto-trading is disabled."):
    super().__init__(MT5.RES_E_AUTO_TRADING_DISABLED, message)

InternalFailSend

InternalFailSend(message='Internal IPC send failed.')

Bases: InternalFailError

Exception raised for internal IPC send failure.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Internal IPC send failed."):
    super().__init__(MT5.RES_E_INTERNAL_FAIL_SEND, message)

InternalFailReceive

InternalFailReceive(message='Internal IPC receive failed.')

Bases: InternalFailError

Exception raised for internal IPC receive failure.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Internal IPC receive failed."):
    super().__init__(MT5.RES_E_INTERNAL_FAIL_RECEIVE, message)

InternalFailInit

InternalFailInit(message='Internal IPC initialization failed.')

Bases: InternalFailError

Exception raised for internal IPC initialization failure.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Internal IPC initialization failed."):
    super().__init__(MT5.RES_E_INTERNAL_FAIL_INIT, message)

InternalFailConnect

InternalFailConnect(message='No IPC connection.')

Bases: InternalFailError

Exception raised for no IPC connection.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="No IPC connection."):
    super().__init__(MT5.RES_E_INTERNAL_FAIL_CONNECT, message)

InternalFailTimeout

InternalFailTimeout(message='Internal timeout.')

Bases: InternalFailError

Exception raised for an internal timeout.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Internal timeout."):
    super().__init__(MT5.RES_E_INTERNAL_FAIL_TIMEOUT, message)

TradeCopier

TradeCopier(source: dict, destinations: list[dict], /, sleeptime: float = 0.1, start_time: str = None, end_time: str = None, *, custom_logger=None, shutdown_event=None, log_queue=None)

Bases: object

TradeCopier responsible for copying trading orders and positions from a source account to multiple destination accounts.

This class facilitates the synchronization of trades between a source account and multiple destination accounts. It handles copying new orders, modifying existing orders, updating and closing positions based on updates from the source account.

Initializes the TradeCopier instance, setting up the source and destination trading accounts for trade copying.

Parameters:

Name Type Description Default
source dict

A dictionary containing the connection details for the source trading account. This dictionary must include all parameters required to successfully connect to the source account. Refer to the bbstrader.metatrader.check_mt5_connection function for a comprehensive list of required keys and their expected values. Common parameters include, but are not limited to

- `login`:  The account login ID (integer).
- `password`: The account password (string).
- `server`:  The server address (string), e.g., "Broker-Demo".
- `path`:  The path to the MetaTrader 5 installation directory (string).
- `portable`:  A boolean indicating whether to open MetaTrader 5 installation in portable mode.
- `id`: A unique identifier for all trades opened buy the source source account.
    This Must be a positive number greater than 0 and less than 2^32 / 2.
- `unique`: A boolean indication whehter to allow destination accounts to copy from other sources.
    If Set to True, all destination accounts won't be allow to accept trades from other accounts even
    manually opened positions or orders will be removed.
required
destinations list[dict]

A list of dictionaries, where each dictionary represents a destination trading account to which trades will be copied. Each destination dictionary must contain the following keys

- Authentication details (e.g., `login`, `password`, `server`)
Identical in structure and requirements to the `source` dictionary,
ensuring a connection can be established to the destination account.
Refer to ``bbstrader.metatrader.check_mt5_connection``.

- `symbols` (Union[list[str], Dict[str, str], str])
Specifies which symbols should be copied from the source
account to this destination account.  Possible values include
`list[str]` A list of strings, where each string is a symbol to be copied.
    The same symbol will be traded on the destination account.  Example `["EURUSD", "GBPUSD"]`
`Dict[str, str]` A dictionary mapping source symbols to destination symbols.
    This allows for trading a different symbol on the destination account than the one traded on the source.
    Example `{"EURUSD": "EURUSD_i", "GBPUSD": "GBPUSD_i"}`.
`"all"` or `"*"`  Indicates that all symbols traded on the source account should be
    copied to this destination account, using the same symbol name.

- `mode` (str) The risk management mode to use.  Valid options are
`"fix"` Use a fixed lot size.  The `value` key must specify the fixed lot size.
`"multiply"` Multiply the source account's lot size by a factor.
    The `value` key must specify the multiplier.
`"percentage"`  Trade a percentage of the source account's lot size.
    The `value` key must specify the percentage (as a decimal, e.g., 50 for 50%).
`"dynamic"` Calculate the lot size dynamically based on account equity and risk parameters.
    The `value` key is ignored.
`"replicate"` Copy the exact lot size from the source account. The `value` key is ignored.
`"specific"` Use a specific lot size defined in the `value` key for each symbol.

- `value` (float or dict,  optional)  A numerical value or dict used in conjunction with the selected `mode`.
    Its meaning depends on the chosen `mode` (see above). Required for "fix", "multiply", specific
    and "percentage" modes; optional for "dynamic".

- `slippage` (float, optional) The maximum allowed slippage in percentage when opening trades on the destination account,
defaults to 0.1% (0.1), if the slippage exceeds this value, the trade will not be copied.

- `comment` (str, optional) An optional comment to be added to trades opened on the destination account,
defaults to an empty string.

- ``copy_what`` (str, optional)
Specifies what to copy from the source account to the destination accounts.  Valid options are
`"orders"` Copy only orders from the source account to the destination accounts.
`"positions"` Copy only positions from the source account to the destination accounts.
`"all"` Copy both orders and positions from the source account to the destination accounts.
Defaults to `"all"`.
required
sleeptime float

The time interval in seconds between each iteration of the trade copying process. Defaults to 0.1 seconds. It can be useful if you know the frequency of new trades on the source account.

0.1
start_time str

The time (HH:MM) from which the copier start copying from the source.

None
end_time str

The time (HH:MM) from which the copier stop copying from the source.

None
custom_logger (Any, Optional)

Used to set a cutum logger (default is loguru.logger)

None
shutdown_event (Any, Otional)

Use to terminate the copy process when runs in a custum environment like web App or GUI.

None
log_queue (Queue, Optional)

Use to send log to an external program, usefule in GUI apps

None
Note

The source account and the destination accounts must be connected to different MetaTrader 5 platforms. you can copy the initial installation of MetaTrader 5 to a different directory and rename it to create a new instance Then you can connect destination accounts to the new instance while the source account is connected to the original instance.

Source code in src/bbstrader/metatrader/copier.py
def __init__(
    self,
    source: dict,
    destinations: list[dict],
    /,
    sleeptime: float = 0.1,
    start_time: str = None,
    end_time: str = None,
    *,
    custom_logger=None,
    shutdown_event=None,
    log_queue=None,
):
    """
    Initializes the ``TradeCopier`` instance, setting up the source and destination trading accounts for trade copying.

    Args:
        source (dict):
            A dictionary containing the connection details for the source trading account. This dictionary
            **must** include all parameters required to successfully connect to the source account.
            Refer to the ``bbstrader.metatrader.check_mt5_connection`` function for a comprehensive list
            of required keys and their expected values.  Common parameters include, but are not limited to

                - `login`:  The account login ID (integer).
                - `password`: The account password (string).
                - `server`:  The server address (string), e.g., "Broker-Demo".
                - `path`:  The path to the MetaTrader 5 installation directory (string).
                - `portable`:  A boolean indicating whether to open MetaTrader 5 installation in portable mode.
                - `id`: A unique identifier for all trades opened buy the source source account.
                    This Must be a positive number greater than 0 and less than 2^32 / 2.
                - `unique`: A boolean indication whehter to allow destination accounts to copy from other sources.
                    If Set to True, all destination accounts won't be allow to accept trades from other accounts even
                    manually opened positions or orders will be removed.

        destinations (list[dict]):
            A list of dictionaries, where each dictionary represents a destination trading account to which
            trades will be copied.  Each destination dictionary **must** contain the following keys

                - Authentication details (e.g., `login`, `password`, `server`)
                Identical in structure and requirements to the `source` dictionary,
                ensuring a connection can be established to the destination account.
                Refer to ``bbstrader.metatrader.check_mt5_connection``.

                - `symbols` (Union[list[str], Dict[str, str], str])
                Specifies which symbols should be copied from the source
                account to this destination account.  Possible values include
                `list[str]` A list of strings, where each string is a symbol to be copied.
                    The same symbol will be traded on the destination account.  Example `["EURUSD", "GBPUSD"]`
                `Dict[str, str]` A dictionary mapping source symbols to destination symbols.
                    This allows for trading a different symbol on the destination account than the one traded on the source.
                    Example `{"EURUSD": "EURUSD_i", "GBPUSD": "GBPUSD_i"}`.
                `"all"` or `"*"`  Indicates that all symbols traded on the source account should be
                    copied to this destination account, using the same symbol name.

                - `mode` (str) The risk management mode to use.  Valid options are
                `"fix"` Use a fixed lot size.  The `value` key must specify the fixed lot size.
                `"multiply"` Multiply the source account's lot size by a factor.
                    The `value` key must specify the multiplier.
                `"percentage"`  Trade a percentage of the source account's lot size.
                    The `value` key must specify the percentage (as a decimal, e.g., 50 for 50%).
                `"dynamic"` Calculate the lot size dynamically based on account equity and risk parameters.
                    The `value` key is ignored.
                `"replicate"` Copy the exact lot size from the source account. The `value` key is ignored.
                `"specific"` Use a specific lot size defined in the `value` key for each symbol.

                - `value` (float or dict,  optional)  A numerical value or dict used in conjunction with the selected `mode`.
                    Its meaning depends on the chosen `mode` (see above). Required for "fix", "multiply", specific
                    and "percentage" modes; optional for "dynamic".

                - `slippage` (float, optional) The maximum allowed slippage in percentage when opening trades on the destination account,
                defaults to 0.1% (0.1), if the slippage exceeds this value, the trade will not be copied.

                - `comment` (str, optional) An optional comment to be added to trades opened on the destination account,
                defaults to an empty string.

                - ``copy_what`` (str, optional)
                Specifies what to copy from the source account to the destination accounts.  Valid options are
                `"orders"` Copy only orders from the source account to the destination accounts.
                `"positions"` Copy only positions from the source account to the destination accounts.
                `"all"` Copy both orders and positions from the source account to the destination accounts.
                Defaults to `"all"`.

        sleeptime (float, optional):
            The time interval in seconds between each iteration of the trade copying process.
            Defaults to 0.1 seconds. It can be useful if you know the frequency of new trades on the source account.

        start_time (str, optional): The time (HH:MM) from which the copier start copying from the source.
        end_time (str, optional): The time (HH:MM) from which the copier stop copying from the source.
        custom_logger (Any, Optional): Used to set a cutum logger (default is ``loguru.logger``)
        shutdown_event (Any, Otional): Use to terminate the copy process when runs in a custum environment like web App or GUI.
        log_queue (multiprocessing.Queue, Optional): Use to send log to an external program, usefule in GUI apps

    Note:
        The source account and the destination accounts must be connected to different MetaTrader 5 platforms.
        you can copy the initial installation of MetaTrader 5 to a different directory and rename it to create a new instance
        Then you can connect destination accounts to the new instance while the source account is connected to the original instance.
    """
    self.source = source
    self.source_id = source.get("id", 0)
    self.source_isunique = source.get("unique", True)
    self.destinations = destinations
    self.sleeptime = sleeptime
    self.start_time = start_time
    self.end_time = end_time
    self.errors = set()
    self.log_queue = log_queue
    self._add_logger(custom_logger)
    self._validate_source()
    self.shutdown_event = (
        shutdown_event if shutdown_event is not None else mp.Event()
    )
    self._last_session = datetime.now().date()
    self._running = True

running property

running

Check if the Trade Copier is running.

start_copy_process

start_copy_process(destination: dict)

Worker process: copies orders and positions concurrently for a single destination account.

Source code in src/bbstrader/metatrader/copier.py
def start_copy_process(self, destination: dict):
    """
    Worker process: copies orders and positions concurrently for a single destination account.
    """
    if destination.get("path") == self.source.get("path"):
        self.log_message(
            f"Source and destination accounts are on the same MetaTrader 5 "
            f"installation ({self.source.get('path')}), which is not allowed."
        )
        return

    self.log_message(
        f"Copy process started for source @{self.source.get('login')} "
        f"and destination @{destination.get('login')}"
    )
    while not self.shutdown_event.is_set():
        try:
            self.copy_positions(destination)
            self.copy_orders(destination)
        except KeyboardInterrupt:
            self.log_message(
                "KeyboardInterrupt received, stopping the Trade Copier..."
            )
            self.stop()
        except Exception as e:
            self.log_error(f"An error occurred during the sync cycle: {e}")
        time.sleep(self.sleeptime)

    self.log_message(
        f"Process exiting for destination @{destination.get('login')} due to shutdown event."
    )

run

run()

Entry point: Starts a dedicated worker thread for EACH destination account to run concurrently.

Source code in src/bbstrader/metatrader/copier.py
def run(self):
    """
    Entry point: Starts a dedicated worker thread for EACH destination account to run concurrently.
    """
    self.log_message(
        f"Main Copier instance starting for source @{self.source.get('login')}."
    )
    self.log_message(
        f"Found {len(self.destinations)} destination accounts to process in parallel."
    )
    if len(set([d.get("path") for d in self.destinations])) < len(
        self.destinations
    ):
        self.log_message(
            "Two or more destination accounts have the same Terminal path, which is not allowed.",
            type="error",
        )
        return

    worker_threads = []

    for destination in self.destinations:
        self.log_message(
            f"Creating worker thread for destination @{destination.get('login')}"
        )
        try:
            thread = threading.Thread(
                target=self.start_copy_process,
                args=(destination,),
                name=f"Worker-{destination.get('login')}",
            )
            worker_threads.append(thread)
            thread.start()
        except Exception as e:
            self.log_error(
                f"Error executing thread Worker-{destination.get('login')} : {e}"
            )

    self.log_message(f"All {len(worker_threads)} worker threads have been started.")
    try:
        while not self.shutdown_event.is_set():
            time.sleep(1)
    except KeyboardInterrupt:
        self.log_message(
            "\nKeyboardInterrupt detected by main thread. Initiating shutdown..."
        )
    finally:
        self.stop()
        self.log_message("Waiting for all worker threads to complete...")
        for thread in worker_threads:
            thread.join()

        self.log_message("All worker threads have shut down. Copier exiting.")

stop

stop()

Stop the Trade Copier gracefully by setting the shutdown event.

Source code in src/bbstrader/metatrader/copier.py
def stop(self):
    """
    Stop the Trade Copier gracefully by setting the shutdown event.
    """
    if self._running:
        self.log_message(
            f"Signaling stop for Trade Copier on source account @{self.source.get('login')}..."
        )
        self._running = False
        self.shutdown_event.set()
    self.log_message("Trade Copier stopped successfully.")

download_historical_data

download_historical_data(symbol, timeframe, date_from, date_to=pd.Timestamp.now(), lower_colnames=True, utc=False, filter=False, fill_na=False, save_csv=False, **kwargs)

Download historical data from MetaTrader 5 terminal. See Rates.get_historical_data for more details.

Source code in src/bbstrader/metatrader/rates.py
def download_historical_data(
    symbol,
    timeframe,
    date_from,
    date_to=pd.Timestamp.now(),
    lower_colnames=True,
    utc=False,
    filter=False,
    fill_na=False,
    save_csv=False,
    **kwargs,
):
    """Download historical data from MetaTrader 5 terminal.
    See `Rates.get_historical_data` for more details.
    """
    rates = Rates(symbol, timeframe, **kwargs)
    data = rates.get_historical_data(
        date_from=date_from,
        date_to=date_to,
        save_csv=save_csv,
        utc=utc,
        filter=filter,
        lower_colnames=lower_colnames,
    )
    return data

get_data_from_pos

get_data_from_pos(symbol, timeframe, start_pos=0, fill_na=False, count=MAX_BARS, lower_colnames=False, utc=False, filter=False, session_duration=23.0, **kwargs)

Get historical data from a specific position. See Rates.get_rates_from_pos for more details.

Source code in src/bbstrader/metatrader/rates.py
def get_data_from_pos(
    symbol,
    timeframe,
    start_pos=0,
    fill_na=False,
    count=MAX_BARS,
    lower_colnames=False,
    utc=False,
    filter=False,
    session_duration=23.0,
    **kwargs,
):
    """Get historical data from a specific position.
    See `Rates.get_rates_from_pos` for more details.
    """
    rates = Rates(symbol, timeframe, start_pos, count, **kwargs)
    data = rates.get_rates_from_pos(
        filter=filter, fill_na=fill_na, lower_colnames=lower_colnames, utc=utc
    )
    return data

get_data_from_date

get_data_from_date(symbol, timeframe, date_from, count=MAX_BARS, fill_na=False, lower_colnames=False, utc=False, filter=False, **kwargs)

Get historical data from a specific date. See Rates.get_rates_from for more details.

Source code in src/bbstrader/metatrader/rates.py
def get_data_from_date(
    symbol,
    timeframe,
    date_from,
    count=MAX_BARS,
    fill_na=False,
    lower_colnames=False,
    utc=False,
    filter=False,
    **kwargs,
):
    """Get historical data from a specific date.
    See `Rates.get_rates_from` for more details.
    """
    rates = Rates(symbol, timeframe, **kwargs)
    data = rates.get_rates_from(
        date_from,
        count,
        filter=filter,
        fill_na=fill_na,
        lower_colnames=lower_colnames,
        utc=utc,
    )
    return data

create_trade_instance

create_trade_instance(symbols: list[str], params: dict[str, Any], daily_risk: dict[str, float] | None = None, max_risk: dict[str, float] | None = None, pchange_sl: dict[str, float] | float | None = None, **kwargs) -> dict[str, Trade]

Creates Trade instances for each symbol provided.

Parameters:

Name Type Description Default
symbols list[str]

A list of trading symbols (e.g., ['AAPL', 'MSFT']).

required
params dict[str, Any]

A dictionary containing parameters for the Trade instance.

required
daily_risk dict[str, float] | None

A dictionary containing daily risk weight for each symbol.

None
max_risk dict[str, float] | None

A dictionary containing maximum risk weight for each symbol.

None

Returns:

Type Description
dict[str, Trade]

A dictionary where keys are symbols and values are corresponding Trade instances.

Raises:

Type Description
ValueError

If the 'symbols' list is empty or the 'params' dictionary is missing required keys.

Note

daily_risk and max_risk can be used to manage the risk of each symbol based on the importance of the symbol in the portfolio or strategy. See bbstrader.metatrader.risk.RiskManagement for more details.

Source code in src/bbstrader/metatrader/trade.py
def create_trade_instance(
    symbols: list[str],
    params: dict[str, Any],
    daily_risk: dict[str, float] | None = None,
    max_risk: dict[str, float] | None = None,
    pchange_sl: dict[str, float] | float | None = None,
    **kwargs,
) -> dict[str, Trade]:
    """
    Creates Trade instances for each symbol provided.

    Args:
        symbols: A list of trading symbols (e.g., ['AAPL', 'MSFT']).
        params: A dictionary containing parameters for the Trade instance.
        daily_risk: A dictionary containing daily risk weight for each symbol.
        max_risk: A dictionary containing maximum risk weight for each symbol.

    Returns:
        A dictionary where keys are symbols and values are corresponding Trade instances.

    Raises:
        ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.

    Note:
        `daily_risk` and `max_risk`  can be used to manage the risk of each symbol
        based on the importance of the symbol in the portfolio or strategy.
        See bbstrader.metatrader.risk.RiskManagement for more details.
    """
    if not symbols or not params:
        raise ValueError("Symbols and params are required.")

    logger = params.get("logger") if isinstance(params.get("logger"), Logger) else log
    base_id = params.get("expert_id", EXPERT_ID)

    def get_val(source, symbol, default=None):
        if isinstance(source, dict):
            if symbol not in source:
                raise ValueError(f"Missing key '{symbol}' in configuration.")
            return source[symbol]
        return source if source is not None else default

    trades = {}
    for sym in symbols:
        try:
            conf = {
                **params,
                "symbol": sym,
                "expert_id": get_val(base_id, sym),
                "daily_risk": get_val(daily_risk, sym, params.get("daily_risk")),
                "max_risk": get_val(max_risk, sym, params.get("max_risk", 10.0)),
                "pchange_sl": get_val(pchange_sl, sym, params.get("pchange_sl")),
            }
            trades[sym] = Trade(**conf)

        except Exception as e:
            logger.error(f"Failed trade init: SYMBOL={sym} | ERR={e}")

    # Final Audit
    if len(trades) < len(symbols):
        missing = set(symbols) - set(trades.keys())
        logger.warning(f"Partial success. Missing symbols: {missing}")

    logger.info(f"Initialized {len(trades)} trade instances.")
    return trades

raise_mt5_error

raise_mt5_error(message: Optional[str] = None)

Raises an exception based on the given error code.

Parameters:

Name Type Description Default
message Optional[str]

An optional custom error message.

None

Raises:

Type Description
MT5TerminalError

A specific exception based on the error code.

Source code in src/bbstrader/metatrader/utils.py
def raise_mt5_error(message: Optional[str] = None):
    """Raises an exception based on the given error code.

    Args:
        message: An optional custom error message.

    Raises:
        MT5TerminalError: A specific exception based on the error code.
    """
    if message and isinstance(message, Exception):
        message = str(message)
    exception = _ERROR_CODE_TO_EXCEPTION_.get(MT5.last_error()[0])
    if exception is not None:
        raise exception(f"{message or MT5.last_error()[1]}")
    else:
        raise Exception(f"{message or MT5.last_error()[1]}")

retry_on_disconnect

retry_on_disconnect(max_retries: int = 3, delay: float = 1.0) -> Callable[[_F], _F]

Decorator that retries a function on MT5 connection errors with exponential back-off.

Catches InternalFailConnect and InternalFailTimeout, waits delay * 2**attempt seconds between tries, then re-raises on the last attempt.

Source code in src/bbstrader/metatrader/utils.py
def retry_on_disconnect(max_retries: int = 3, delay: float = 1.0) -> Callable[[_F], _F]:
    """Decorator that retries a function on MT5 connection errors with exponential back-off.

    Catches ``InternalFailConnect`` and ``InternalFailTimeout``, waits
    ``delay * 2**attempt`` seconds between tries, then re-raises on the last attempt.
    """

    def decorator(func: _F) -> _F:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except (InternalFailConnect, InternalFailTimeout):
                    if attempt == max_retries - 1:
                        raise
                    time.sleep(delay * (2**attempt))

        return wrapper  # type: ignore[return-value]

    return decorator  # type: ignore[return-value]

trade_retcode_message

trade_retcode_message(code, display=False, add_msg='')

Retrieves a user-friendly message corresponding to a given trade return code.

Parameters:

Name Type Description Default
code int

The trade return code to look up.

required
display bool

Whether to print the message to the console. Defaults to False.

False

Returns:

Name Type Description
str

The message associated with the provided trade return code. If the code is not found, it returns "Unknown trade error.".

Source code in src/bbstrader/metatrader/utils.py
def trade_retcode_message(code, display=False, add_msg=""):
    """
    Retrieves a user-friendly message corresponding to a given trade return code.

    Args:
        code (int): The trade return code to look up.
        display (bool, optional): Whether to print the message to the console. Defaults to False.

    Returns:
        str: The message associated with the provided trade return code. If the code is not found,
             it returns "Unknown trade error.".
    """
    message = _TRADE_RETCODE_MESSAGES_.get(code, "Unknown trade error")
    if display:
        print(message + add_msg)
    return message

copier_worker_process

copier_worker_process(source_config: dict, destination_config: dict, sleeptime: float, start_time: str, end_time: str, /, custom_logger=None, shutdown_event=None, log_queue=None)

A top-level worker function for handling a single source-to-destination copy task.

This function is the cornerstone of the robust, multi-process architecture. It is designed to be the target of a multiprocessing.Process. By being a top-level function, it avoids pickling issues on Windows and ensures that each copy task runs in a completely isolated process.

A controller (like a GUI or a master script) should spawn one process with this target for each destination account it needs to manage.

Parameters:

Name Type Description Default
source_config dict

Configuration dictionary for the source account. Must contain 'login', 'password', 'server', and 'path'.

required
destination_config dict

Configuration dictionary for a single destination account.

required
sleeptime float

The time in seconds to wait between copy cycles.

required
start_time str

The time of day to start copying (e.g., "08:00").

required
end_time str

The time of day to stop copying (e.g., "22:00").

required
custom_logger

An optional custom logger instance.

None
shutdown_event Event

An event object that, when set, will signal this process to terminate gracefully.

None
log_queue Queue

A queue for sending log messages back to the parent process in a thread-safe manner.

None
Source code in src/bbstrader/metatrader/copier.py
def copier_worker_process(
    source_config: dict,
    destination_config: dict,
    sleeptime: float,
    start_time: str,
    end_time: str,
    /,
    custom_logger=None,
    shutdown_event=None,
    log_queue=None,
):
    """A top-level worker function for handling a single source-to-destination copy task.

    This function is the cornerstone of the robust, multi-process architecture. It is
    designed to be the `target` of a `multiprocessing.Process`. By being a top-level
    function, it avoids pickling issues on Windows and ensures that each copy task
    runs in a completely isolated process.

    A controller (like a GUI or a master script) should spawn one process with this
    target for each destination account it needs to manage.

    Args:
        source_config (dict): Configuration dictionary for the source account.
            Must contain 'login', 'password', 'server', and 'path'.
        destination_config (dict): Configuration dictionary for a *single*
            destination account.
        sleeptime (float): The time in seconds to wait between copy cycles.
        start_time (str): The time of day to start copying (e.g., "08:00").
        end_time (str): The time of day to stop copying (e.g., "22:00").
        custom_logger: An optional custom logger instance.
        shutdown_event (multiprocessing.Event): An event object that, when set,
            will signal this process to terminate gracefully.
        log_queue (multiprocessing.Queue): A queue for sending log messages back
            to the parent process in a thread-safe manner.
    """
    copier = TradeCopier(
        source_config,
        [destination_config],
        sleeptime=sleeptime,
        start_time=start_time,
        end_time=end_time,
        custom_logger=custom_logger,
        shutdown_event=shutdown_event,
        log_queue=log_queue,
    )
    copier.start_copy_process(destination_config)

RunCopier

RunCopier(source: dict, destinations: list, sleeptime: float, start_time: str, end_time: str, /, custom_logger=None, shutdown_event=None, log_queue=None)

Initialize and run a TradeCopier instance in a single process.

This function serves as a straightforward wrapper to start a copying session that handles one source account and one or more destination accounts sequentially within the same thread. It does not create any new processes itself.

Use Cases
  • Simpler, command-line based use cases.
  • Scenarios where parallelism is not required.
  • As the target for RunMultipleCopier, where each process handles a full source-to-destinations session.
Parameters

source : dict Configuration dictionary for the source account. destinations : list A list of configuration dictionaries, one for each destination account to be processed sequentially. sleeptime : float The time in seconds to wait after completing a full cycle through all destinations. start_time : str The time of day to start copying (e.g., "08:00"). end_time : str The time of day to stop copying (e.g., "22:00"). custom_logger : logging.Logger, optional An optional custom logger instance. shutdown_event : multiprocessing.Event, optional An event to signal shutdown. log_queue : multiprocessing.Queue, optional A queue for log messages.

Returns

None Runs until stopped via shutdown_event or external interruption.

Source code in src/bbstrader/metatrader/copier.py
def RunCopier(
    source: dict,
    destinations: list,
    sleeptime: float,
    start_time: str,
    end_time: str,
    /,
    custom_logger=None,
    shutdown_event=None,
    log_queue=None,
):
    """
    Initialize and run a TradeCopier instance in a single process.

    This function serves as a straightforward wrapper to start a copying session
    that handles one source account and one or more destination accounts
    sequentially within the same thread. It does not create any new processes itself.

    Use Cases
    ---------
    * Simpler, command-line based use cases.
    * Scenarios where parallelism is not required.
    * As the target for ``RunMultipleCopier``, where each process handles a
      full source-to-destinations session.

    Parameters
    ----------
    source : dict
        Configuration dictionary for the source account.
    destinations : list
        A list of configuration dictionaries, one for each
        destination account to be processed sequentially.
    sleeptime : float
        The time in seconds to wait after completing a full
        cycle through all destinations.
    start_time : str
        The time of day to start copying (e.g., ``"08:00"``).
    end_time : str
        The time of day to stop copying (e.g., ``"22:00"``).
    custom_logger : logging.Logger, optional
        An optional custom logger instance.
    shutdown_event : multiprocessing.Event, optional
        An event to signal shutdown.
    log_queue : multiprocessing.Queue, optional
        A queue for log messages.

    Returns
    -------
    None
        Runs until stopped via ``shutdown_event`` or external interruption.
    """
    copier = TradeCopier(
        source,
        destinations,
        sleeptime=sleeptime,
        start_time=start_time,
        end_time=end_time,
        custom_logger=custom_logger,
        shutdown_event=shutdown_event,
        log_queue=log_queue,
    )
    copier.run()

RunMultipleCopier

RunMultipleCopier(accounts: list[dict], sleeptime: float = 0.01, start_delay: float = 1.0, start_time: str = None, end_time: str = None, shutdown_event=None, custom_logger=None, log_queue=None)

Manage multiple, independent trade copying sessions in parallel.

This function acts as a high-level manager that takes a list of account setups and creates a separate, dedicated process for each one. Each process is responsible for copying from one source account to its associated list of destination accounts.

The parallelism occurs at the source account level. Within each spawned process, the destinations for that source are handled sequentially by RunCopier.

Example

An example accounts structure:

.. code-block:: python

accounts = [
    {"source": {...}, "destinations": [{...}, {...}]},  # -> Process 1
    {"source": {...}, "destinations": [{...}]}          # -> Process 2
]
Parameters

accounts : list of dict A list of account configurations. Each item must be a dictionary with a source key and a destinations key. sleeptime : float, optional The sleep time passed down to each RunCopier process. start_delay : float, optional A delay in seconds between starting each new process. Helps prevent resource contention by staggering the initialization of multiple MetaTrader 5 terminals. start_time : str, optional The start time passed down to each RunCopier process. end_time : str, optional The end time passed down to each RunCopier process. shutdown_event : multiprocessing.Event, optional An event to signal shutdown to all child processes. custom_logger : logging.Logger, optional An optional custom logger instance. log_queue : multiprocessing.Queue, optional A queue for aggregating log messages from all child processes.

Returns

None Runs until stopped via shutdown_event or external interruption.

Source code in src/bbstrader/metatrader/copier.py
def RunMultipleCopier(
    accounts: list[dict],
    sleeptime: float = 0.01,
    start_delay: float = 1.0,
    start_time: str = None,
    end_time: str = None,
    shutdown_event=None,
    custom_logger=None,
    log_queue=None,
):
    """
    Manage multiple, independent trade copying sessions in parallel.

    This function acts as a high-level manager that takes a list of account
    setups and creates a separate, dedicated process for each one. Each process
    is responsible for copying from one source account to its associated list of
    destination accounts.

    The parallelism occurs at the **source account level**. Within each spawned
    process, the destinations for that source are handled sequentially by
    ``RunCopier``.

    Example
    -------
    An example ``accounts`` structure:

    .. code-block:: python

        accounts = [
            {"source": {...}, "destinations": [{...}, {...}]},  # -> Process 1
            {"source": {...}, "destinations": [{...}]}          # -> Process 2
        ]

    Parameters
    ----------
    accounts : list of dict
        A list of account configurations. Each item must be a dictionary with
        a ``source`` key and a ``destinations`` key.
    sleeptime : float, optional
        The sleep time passed down to each ``RunCopier`` process.
    start_delay : float, optional
        A delay in seconds between starting each new process.
        Helps prevent resource contention by staggering the initialization of
        multiple MetaTrader 5 terminals.
    start_time : str, optional
        The start time passed down to each ``RunCopier`` process.
    end_time : str, optional
        The end time passed down to each ``RunCopier`` process.
    shutdown_event : multiprocessing.Event, optional
        An event to signal shutdown to all child processes.
    custom_logger : logging.Logger, optional
        An optional custom logger instance.
    log_queue : multiprocessing.Queue, optional
        A queue for aggregating log messages from all child processes.

    Returns
    -------
    None
        Runs until stopped via ``shutdown_event`` or external interruption.
    """
    processes = []

    for account in accounts:
        source = account.get("source")
        destinations = account.get("destinations")

        if not source or not destinations:
            logger.warning("Skipping account due to missing source or destinations.")
            continue
        paths = set([source.get("path")] + [dest.get("path") for dest in destinations])
        if len(paths) == 1 and len(destinations) >= 1:
            logger.warning(
                "Skipping account: source and destination cannot share the same MetaTrader 5 terminal path."
            )
            continue
        logger.info(f"Starting process for source account @{source.get('login')}")
        process = mp.Process(
            target=RunCopier,
            args=(
                source,
                destinations,
                sleeptime,
                start_time,
                end_time,
            ),
            kwargs=dict(
                custom_logger=custom_logger,
                shutdown_event=shutdown_event,
                log_queue=log_queue,
            ),
        )
        processes.append(process)
        process.start()

        if start_delay:
            time.sleep(start_delay)

    for process in processes:
        process.join()

config_copier

config_copier(source_section: str = None, dest_sections: str | list[str] = None, inifile: str | Path = None) -> tuple[dict, list[dict]]

Read the configuration file and return the source and destination account details.

Parameters:

Name Type Description Default
inifile str | Path

The path to the INI configuration file.

None
source_section str

The section name of the source account, defaults to "SOURCE".

None
dest_sections str | list[str]

The section name(s) of the destination account(s).

None

Returns:

Type Description
tuple[dict, list[dict]]

tuple[dict, list[dict]]: A tuple containing the source account and a list of destination accounts.

Example
from pathlib import Path
config_file = ~/.bbstrader/copier/copier.ini
source, destinations = config_copier(config_file, "SOURCE", ["DEST1", "DEST2"])
Source code in src/bbstrader/metatrader/copier.py
def config_copier(
    source_section: str = None,
    dest_sections: str | list[str] = None,
    inifile: str | Path = None,
) -> tuple[dict, list[dict]]:
    """
    Read the configuration file and return the source and destination account details.

    Args:
        inifile (str | Path): The path to the INI configuration file.
        source_section (str): The section name of the source account, defaults to "SOURCE".
        dest_sections (str | list[str]): The section name(s) of the destination account(s).

    Returns:
        tuple[dict, list[dict]]: A tuple containing the source account and a list of destination accounts.

    Example:
        ```python
        from pathlib import Path
        config_file = ~/.bbstrader/copier/copier.ini
        source, destinations = config_copier(config_file, "SOURCE", ["DEST1", "DEST2"])
        ```
    """

    if not inifile:
        inifile = Path().home() / ".bbstrader" / "copier" / "copier.ini"
        if not inifile.exists() or not inifile.is_file():
            raise FileNotFoundError(f"{inifile} not found")

    if not source_section:
        source_section = "SOURCE"

    config = dict_from_ini(inifile)
    try:
        source = config.pop(source_section)
    except KeyError:
        raise ValueError(f"Source section {source_section} not found in {inifile}")
    dest_sections = dest_sections or config.keys()
    if not dest_sections:
        raise ValueError("No destination sections found in the configuration file")

    destinations = []

    if isinstance(dest_sections, str):
        dest_sections = [dest_sections]

    for dest_section in dest_sections:
        try:
            section = config[dest_section]
        except KeyError:
            raise ValueError(
                f"Destination section {dest_section} not found in {inifile}"
            )
        _parse_symbols(section)
        _parse_lots(section)
        destinations.append(section)

    return source, destinations

account

Account

Account(broker: Broker | None = None, **kwargs)

The Account class is utilized to retrieve information about the current trading account or a specific account. It enables interaction with the MT5 terminal to manage account details, including account informations, terminal status, financial instrument details, active orders, open positions, and trading history.

Example
Instantiating the Account class

account = Account()

Getting account information

account_info = account.get_account_info()

Getting terminal information

terminal_info = account.get_terminal_info()

Getting active orders

orders = account.get_orders()

Fetching open positions

positions = account.get_positions()

Accessing trade history

from_date = datetime(2020, 1, 1) to_date = datetime.now() trade_history = account.get_trade_history(from_date, to_date)

Initialize the Account class.

See bbstrader.metatrader.broker.check_mt5_connection() for more details on how to connect to MT5 terminal.

Source code in src/bbstrader/metatrader/account.py
def __init__(self, broker: Broker | None = None, **kwargs):
    """
    Initialize the Account class.

    See `bbstrader.metatrader.broker.check_mt5_connection()`
    for more details on how to connect to MT5 terminal.

    """
    check_mt5_connection(**kwargs)
    self._info = client.account_info()
    self._symbol_cache: dict[str, SymbolInfo] = {}
    terminal_info = self.get_terminal_info()
    self._broker = (
        broker
        if broker is not None
        else Broker(terminal_info.company if terminal_info else "Unknown")
    )
server property
server: str

The name of the trade server to which the client terminal is connected. (e.g., 'AdmiralsGroup-Demo')

shutdown
shutdown()

Close the connection to the MetaTrader 5 terminal.

Source code in src/bbstrader/metatrader/account.py
def shutdown(self):
    """Close the connection to the MetaTrader 5 terminal."""
    client.shutdown()
refresh
refresh() -> None

Reload account info from the MT5 terminal to reflect current balance/equity.

Source code in src/bbstrader/metatrader/account.py
def refresh(self) -> None:
    """Reload account info from the MT5 terminal to reflect current balance/equity."""
    self._info = client.account_info()
clear_symbol_cache
clear_symbol_cache() -> None

Invalidate the cached symbol info, forcing fresh lookups on next call.

Source code in src/bbstrader/metatrader/account.py
def clear_symbol_cache(self) -> None:
    """Invalidate the cached symbol info, forcing fresh lookups on next call."""
    self._symbol_cache.clear()
get_account_info
get_account_info(account: int | None = None, password: str | None = None, server: str | None = None, timeout: int | None = _DEFAULT_TIMEOUT, path: str | None = None) -> AccountInfo | None

Get info on the current trading account or a specific account .

Parameters:

Name Type Description Default
account (int, optinal)

MT5 Trading account number.

required
password (str, optinal)

MT5 Trading account password.

None
server (str, optinal)

MT5 Trading account server [Brokers or terminal server ["demo", "real"]] If no server is set, the last used server is applied automaticall

None
timeout (int, optinal)

Connection timeout in milliseconds. Optional named parameter. If not specified, the value of 60 000 (60 seconds) is applied. If the connection is not established within the specified time, the call is forcibly terminated and the exception is generated.

_DEFAULT_TIMEOUT
path str

The path to the MetaTrader 5 terminal executable file. Defaults to None (e.g., "C:/Program Files/MetaTrader 5/terminal64.exe").

None

Returns: - AccountInfo - None in case of an error

Raises:

Type Description
MT5TerminalError

A specific exception based on the error code.

Source code in src/bbstrader/metatrader/account.py
def get_account_info(
    self,
    account: int | None = None,
    password: str | None = None,
    server: str | None = None,
    timeout: int | None = _DEFAULT_TIMEOUT,
    path: str | None = None,
) -> AccountInfo | None:
    """
    Get info on the current trading account or a specific account .

    Args:
        account (int, optinal) : MT5 Trading account number.
        password (str, optinal): MT5 Trading account password.

        server (str, optinal): MT5 Trading account server
            [Brokers or terminal server ["demo", "real"]]
            If no server is set, the last used server is applied automaticall

        timeout (int, optinal):
             Connection timeout in milliseconds. Optional named parameter.
             If not specified, the value of 60 000 (60 seconds) is applied.
             If the connection is not established within the specified time,
             the call is forcibly terminated and the exception is generated.
        path (str, optional): The path to the MetaTrader 5 terminal executable file.
            Defaults to None (e.g., "C:/Program Files/MetaTrader 5/terminal64.exe").

    Returns:
    -   AccountInfo
    -   None in case of an error

    Raises:
        MT5TerminalError: A specific exception based on the error code.
    """
    # connect to the trade account specifying a password and a server
    if account is not None and password is not None and server is not None:
        try:
            if path is not None:
                self.broker.initialize_connection(
                    path=path,
                    login=account,
                    password=password,
                    server=server,
                    timeout=timeout,
                )
            authorized = client.login(
                account, password=password, server=server, timeout=timeout
            )
            if not authorized:
                raise_mt5_error(f"Failed to connect to account #{account}")
            info = client.account_info()
            return info
        except Exception as e:
            raise_mt5_error(str(e))
    else:
        try:
            return client.account_info()
        except Exception as e:
            raise_mt5_error(str(e))
get_terminal_info
get_terminal_info() -> TerminalInfo | None

Get the connected MetaTrader 5 client terminal status and settings.

Returns: - TerminalInfo - None in case of an error

Raises:

Type Description
MT5TerminalError

A specific exception based on the error code.

Source code in src/bbstrader/metatrader/account.py
def get_terminal_info(self) -> TerminalInfo | None:
    """
    Get the connected MetaTrader 5 client terminal status and settings.

    Returns:
    -   TerminalInfo
    -   None in case of an error

    Raises:
        MT5TerminalError: A specific exception based on the error code.
    """
    try:
        terminal_info = client.terminal_info()
        if terminal_info is None:
            return None
    except Exception as e:
        raise_mt5_error(str(e))
    return terminal_info
get_symbol_info
get_symbol_info(symbol: str) -> SymbolInfo | None

Get symbol properties

Parameters:

Name Type Description Default
symbol str

Symbol name

required

Returns: - SymbolInfo. - None in case of an error.

Raises:

Type Description
MT5TerminalError

A specific exception based on the error code.

Source code in src/bbstrader/metatrader/account.py
def get_symbol_info(self, symbol: str) -> SymbolInfo | None:
    """Get symbol properties

    Args:
        symbol (str): Symbol name

    Returns:
    -   SymbolInfo.
    -   None in case of an error.

    Raises:
        MT5TerminalError: A specific exception based on the error code.

    """
    if symbol in self._symbol_cache:
        return self._symbol_cache[symbol]
    try:
        symbol_info = client.symbol_info(symbol)
        if symbol_info is None:
            return None
        self._symbol_cache[symbol] = symbol_info
        return symbol_info
    except Exception as e:
        msg = self._symbol_info_msg(symbol)
        raise_mt5_error(message=f"{str(e)} {msg}")
get_tick_info
get_tick_info(symbol: str) -> TickInfo | None

Get symbol tick properties

Parameters:

Name Type Description Default
symbol str

Symbol name

required

Returns: - TickInfo. - None in case of an error.

Raises:

Type Description
MT5TerminalError

A specific exception based on the error code.

Source code in src/bbstrader/metatrader/account.py
def get_tick_info(self, symbol: str) -> TickInfo | None:
    """Get symbol tick properties

    Args:
        symbol (str): Symbol name

    Returns:
    -   TickInfo.
    -   None in case of an error.

    Raises:
        MT5TerminalError: A specific exception based on the error code.

    """
    try:
        tick_info = client.symbol_info_tick(symbol)
        if tick_info is None:
            return None
        else:
            return tick_info
    except Exception as e:
        msg = self._symbol_info_msg(symbol)
        raise_mt5_error(message=f"{str(e)} {msg}")
get_currency_rates
get_currency_rates(symbol: str) -> dict[str, str]

Parameters:

Name Type Description Default
symbol str

The symbol for which to get currencies

required

Returns:

Type Description
dict[str, str]
  • base currency (bc)
dict[str, str]
  • margin currency (mc)
dict[str, str]
  • profit currency (pc)
dict[str, str]
  • account currency (ac)
Exemple

account = Account() account.get_currency_rates('EURUSD')

Source code in src/bbstrader/metatrader/account.py
def get_currency_rates(self, symbol: str) -> dict[str, str]:
    """
    Args:
        symbol (str): The symbol for which to get currencies

    Returns:
        - `base currency` (bc)
        - `margin currency` (mc)
        - `profit currency` (pc)
        - `account currency` (ac)

    Exemple:
        >>> account =  Account()
        >>> account.get_currency_rates('EURUSD')
        {'bc': 'EUR', 'mc': 'EUR', 'pc': 'USD', 'ac': 'USD'}
    """
    info = self.get_symbol_info(symbol)
    if info is None:
        raise_mt5_error(f"Symbol '{symbol}' not found in Market Watch")
    bc = info.currency_base
    pc = info.currency_profit
    mc = info.currency_margin
    ac = self._info.currency
    return {"bc": bc, "mc": mc, "pc": pc, "ac": ac}
get_symbols
get_symbols(symbol_type: SymbolType | str = 'ALL', check_etf=False, save=False, file_name='symbols', include_desc=False, display_total=False) -> list[str]

Get all specified financial instruments from the MetaTrader 5 terminal.

Parameters:

Name Type Description Default
symbol_type SymbolType | str

The type of financial instruments to retrieve.

'ALL'
- `ALL`

For all available symbols

required
check_etf bool

If True and symbol_type is 'etf', check if the ETF description contains 'ETF'.

False
save bool

If True, save the symbols to a file.

False
file_name str

The name of the file to save the symbols to (without the extension).

'symbols'
include_desc bool

If True, include the symbol's description in the output and saved file.

False

Returns:

Name Type Description
list list[str]

A list of symbols.

Raises:

Type Description
Exception

If there is an error connecting to MT5 or retrieving symbols.

Source code in src/bbstrader/metatrader/account.py
def get_symbols(
    self,
    symbol_type: SymbolType | str = "ALL",
    check_etf=False,
    save=False,
    file_name="symbols",
    include_desc=False,
    display_total=False,
) -> list[str]:
    """
    Get all specified financial instruments from the MetaTrader 5 terminal.

    Args:
        symbol_type (SymbolType | str): The type of financial instruments to retrieve.
        - `ALL`: For all available symbols
        - See `bbstrader.metatrader.utils.SymbolType` for more details.

        check_etf (bool): If True and symbol_type is 'etf', check if the
            ETF description contains 'ETF'.

        save (bool): If True, save the symbols to a file.

        file_name (str): The name of the file to save the symbols to
            (without the extension).

        include_desc (bool): If True, include the symbol's description
            in the output and saved file.

    Returns:
        list: A list of symbols.

    Raises:
        Exception: If there is an error connecting to MT5 or retrieving symbols.
    """
    return self.broker.get_symbols(
        symbol_type=symbol_type,
        check_etf=check_etf,
        save=save,
        file_name=file_name,
        include_desc=include_desc,
        display_total=display_total,
    )
get_symbol_type
get_symbol_type(symbol: str) -> SymbolType

Determines the type of a given financial instrument symbol.

Parameters:

Name Type Description Default
symbol str

The symbol of the financial instrument (e.g., GOOGL, EURUSD).

required

Returns:

Name Type Description
SymbolType SymbolType

The type of the financial instrument, one of the following:

SymbolType
  • SymbolType.ETFs
SymbolType
  • SymbolType.BONDS
SymbolType
  • SymbolType.FOREX
SymbolType
  • SymbolType.FUTURES
SymbolType
  • SymbolType.STOCKS
SymbolType
  • SymbolType.INDICES
SymbolType
  • SymbolType.COMMODITIES
SymbolType
  • SymbolType.CRYPTO
  • SymbolType.unknown if the type cannot be determined.
Source code in src/bbstrader/metatrader/account.py
def get_symbol_type(self, symbol: str) -> SymbolType:
    """
    Determines the type of a given financial instrument symbol.

    Args:
        symbol (str): The symbol of the financial instrument (e.g., `GOOGL`, `EURUSD`).

    Returns:
        SymbolType: The type of the financial instrument, one of the following:
        - `SymbolType.ETFs`
        - `SymbolType.BONDS`
        - `SymbolType.FOREX`
        - `SymbolType.FUTURES`
        - `SymbolType.STOCKS`
        - `SymbolType.INDICES`
        - `SymbolType.COMMODITIES`
        - `SymbolType.CRYPTO`
    - `SymbolType.unknown` if the type cannot be determined.

    """
    return self.broker.get_symbol_type(symbol)
get_stocks_from_country
get_stocks_from_country(country_code: str = 'USA', etf=False) -> list[str]

Retrieves a list of stock symbols from a specific country.

Supported countries are
  • Australia: AUS
  • Belgium: BEL
  • Denmark: DNK
  • Finland: FIN
  • France: FRA
  • Germany: DEU
  • Netherlands: NLD
  • Norway: NOR
  • Portugal: PRT
  • Spain: ESP
  • Sweden: SWE
  • United Kingdom: GBR
  • United States: USA
  • Switzerland: CHE
  • Hong Kong: HKG
  • Ireland: IRL
  • Austria: AUT

Parameters:

Name Type Description Default
country str

The country code of stocks to retrieve. Defaults to 'USA'.

required

Returns:

Name Type Description
list list[str]

A list of stock symbol names from the specified country.

Raises:

Type Description
ValueError

If an unsupported country is provided.

Notes

This mthods works primarly with brokers who specify the stock symbols type and exchanges, For other brokers use get_symbols() or this method will use it by default.

Source code in src/bbstrader/metatrader/account.py
def get_stocks_from_country(
    self, country_code: str = "USA", etf=False
) -> list[str]:
    """
    Retrieves a list of stock symbols from a specific country.

    Supported countries are:
        * **Australia:** AUS
        * **Belgium:** BEL
        * **Denmark:** DNK
        * **Finland:** FIN
        * **France:** FRA
        * **Germany:** DEU
        * **Netherlands:** NLD
        * **Norway:** NOR
        * **Portugal:** PRT
        * **Spain:** ESP
        * **Sweden:** SWE
        * **United Kingdom:** GBR
        * **United States:** USA
        * **Switzerland:** CHE
        * **Hong Kong:** HKG
        * **Ireland:** IRL
        * **Austria:** AUT

    Args:
        country (str, optional): The country code of stocks to retrieve.
                                Defaults to 'USA'.

    Returns:
        list: A list of stock symbol names from the specified country.

    Raises:
        ValueError: If an unsupported country is provided.

    Notes:
        This mthods works primarly with brokers who specify the stock symbols type and exchanges,
        For other brokers use `get_symbols()` or this method will use it by default.
    """
    stocks = self._get_symbols_by_category(
        SymbolType.STOCKS, country_code, self.broker.countries_stocks
    )
    etfs = (
        self._get_symbols_by_category(
            SymbolType.ETFs, country_code, self.broker.countries_stocks
        )
        if etf
        else []
    )
    if not stocks and not etfs:
        stocks = self.get_symbols(symbol_type=SymbolType.STOCKS)
        etfs = self.get_symbols(symbol_type=SymbolType.ETFs) if etf else []
    return stocks + etfs
get_stocks_from_exchange
get_stocks_from_exchange(exchange_code: str = 'XNYS', etf=True) -> list[str]

Get stock symbols from a specific exchange using the ISO Code for the exchange.

Supported exchanges are from Admirals Group AS products: * XASX: Australian Securities Exchange * XBRU: Euronext Brussels Exchange * XCSE: Copenhagen Stock Exchange * XHEL: NASDAQ OMX Helsinki * XPAR: Euronext Paris * XETR: Xetra Frankfurt * XOSL: Oslo Stock Exchange * XLIS: Euronext Lisbon * XMAD: Bolsa de Madrid * XSTO: NASDAQ OMX Stockholm * XLON: London Stock Exchange * NYSE: New York Stock Exchange * ARCA: NYSE ARCA * AMEX: NYSE AMEX * XNYS: New York Stock Exchange (AMEX, ARCA, NYSE) * NASDAQ: NASDAQ * BATS: BATS Exchange * XSWX: SWX Swiss Exchange * XAMS: Euronext Amsterdam

Parameters:

Name Type Description Default
exchange_code str

The ISO code of the exchange.

'XNYS'
etf bool

If True, include ETFs from the exchange. Defaults to True.

True

Returns:

Name Type Description
list list[str]

A list of stock symbol names from the specified exchange.

Raises:

Type Description
ValueError

If an unsupported exchange is provided.

Notes

This mthods works primarly with brokers who specify the stock symbols type and exchanges, For other brokers use get_symbols() or this method will use it by default.

Source code in src/bbstrader/metatrader/account.py
def get_stocks_from_exchange(
    self, exchange_code: str = "XNYS", etf=True
) -> list[str]:
    """
    Get stock symbols from a specific exchange using the ISO Code for the exchange.

    Supported exchanges are from Admirals Group AS products:
    * **XASX:**        **Australian Securities Exchange**
    * **XBRU:**        **Euronext Brussels Exchange**
    * **XCSE:**        **Copenhagen Stock Exchange**
    * **XHEL:**        **NASDAQ OMX Helsinki**
    * **XPAR:**        **Euronext Paris**
    * **XETR:**        **Xetra Frankfurt**
    * **XOSL:**        **Oslo Stock Exchange**
    * **XLIS:**        **Euronext Lisbon**
    * **XMAD:**        **Bolsa de Madrid**
    * **XSTO:**        **NASDAQ OMX Stockholm**
    * **XLON:**        **London Stock Exchange**
    * **NYSE:**        **New York Stock Exchange**
    * **ARCA:**        **NYSE ARCA**
    * **AMEX:**        **NYSE AMEX**
    * **XNYS:**        **New York Stock Exchange (AMEX, ARCA, NYSE)**
    * **NASDAQ:**      **NASDAQ**
    * **BATS:**        **BATS Exchange**
    * **XSWX:**        **SWX Swiss Exchange**
    * **XAMS:**        **Euronext Amsterdam**

    Args:
        exchange_code (str, optional): The ISO code of the exchange.
        etf (bool, optional): If True, include ETFs from the exchange. Defaults to True.

    Returns:
        list: A list of stock symbol names from the specified exchange.

    Raises:
        ValueError: If an unsupported exchange is provided.

    Notes:
        This mthods works primarly with brokers who specify the stock symbols type and exchanges,
        For other brokers use `get_symbols()` or this method will use it by default.
    """
    stocks = self._get_symbols_by_category(
        SymbolType.STOCKS, exchange_code, self.broker.exchanges
    )
    etfs = (
        self._get_symbols_by_category(
            SymbolType.ETFs, exchange_code, self.broker.exchanges
        )
        if etf
        else []
    )
    if not stocks and not etfs:
        stocks = self.get_symbols(symbol_type=SymbolType.STOCKS)
        etfs = self.get_symbols(symbol_type=SymbolType.ETFs) if etf else []
    return stocks + etfs
get_rate_info
get_rate_info(symbol: str, timeframe: str = '1m') -> RateInfo | None

Get the most recent bar for a specified symbol and timeframe.

Parameters:

Name Type Description Default
symbol str

The symbol for which to get the rate information.

required
timeframe str

The timeframe for the rate information. Default is '1m'. See bbstrader.metatrader.utils.TIMEFRAMES for supported timeframes.

'1m'

Returns: RateInfo: The most recent bar as a RateInfo named tuple. None: If no rates are found or an error occurs. Raises: MT5TerminalError: A specific exception based on the error code.

Source code in src/bbstrader/metatrader/account.py
def get_rate_info(self, symbol: str, timeframe: str = "1m") -> RateInfo | None:
    """Get the most recent bar for a specified symbol and timeframe.

    Args:
        symbol (str): The symbol for which to get the rate information.
        timeframe (str): The timeframe for the rate information. Default is '1m'.
                        See ``bbstrader.metatrader.utils.TIMEFRAMES`` for supported timeframes.
    Returns:
        RateInfo: The most recent bar as a RateInfo named tuple.
        None: If no rates are found or an error occurs.
    Raises:
        MT5TerminalError: A specific exception based on the error code.
    """
    rates = client.copy_rates_from_pos(symbol, TIMEFRAMES[timeframe], 0, 1)
    if rates is None or len(rates) == 0:
        return None
    rate = rates[0]
    return RateInfo(*rate)
get_positions
get_positions(symbol: str | None = None, group: str | None = None, ticket: int | None = None) -> list[TradePosition] | None

Get open positions with the ability to filter by symbol or ticket. There are four call options:

  • Call without parameters. Returns open positions for all symbols.
  • Call specifying a symbol. Returns open positions for the specified symbol.
  • Call specifying a group of symbols. Returns open positions for the specified group of symbols.
  • Call specifying a position ticket. Returns the position corresponding to the specified ticket.

Parameters:

Name Type Description Default
symbol Optional[str]

Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored.

None
group Optional[str]

The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only positions meeting specified criteria for a symbol name.

None
ticket Optional[int]

Position ticket. Optional named parameter. A unique number assigned to each newly opened position. It usually matches the ticket of the order used to open the position, except when the ticket is changed as a result of service operations on the server, for example, when charging swaps with position re-opening.

None

Returns:

Type Description
list[TradePosition] | None

list[TradePosition] | None:

list[TradePosition] | None
  • List of TradePosition.
Notes

The method allows receiving all open positions within a specified period.

The group parameter may contain several comma-separated conditions.

A condition can be set as a mask using '*'.

The logical negation symbol '!' can be used for exclusion.

All conditions are applied sequentially, which means conditions for inclusion in a group should be specified first, followed by an exclusion condition.

For example, group="*, !EUR" means that deals for all symbols should be selected first, and those containing "EUR" in symbol names should be excluded afterward.

Source code in src/bbstrader/metatrader/account.py
def get_positions(
    self,
    symbol: str | None = None,
    group: str | None = None,
    ticket: int | None = None,
) -> list[TradePosition] | None:
    """
    Get open positions with the ability to filter by symbol or ticket.
    There are four call options:

    - Call without parameters. Returns open positions for all symbols.
    - Call specifying a symbol. Returns open positions for the specified symbol.
    - Call specifying a group of symbols. Returns open positions for the specified group of symbols.
    - Call specifying a position ticket. Returns the position corresponding to the specified ticket.

    Args:
        symbol (Optional[str]): Symbol name. Optional named parameter.
            If a symbol is specified, the `ticket` parameter is ignored.

        group (Optional[str]): The filter for arranging a group of necessary symbols.
            Optional named parameter. If the group is specified,
            the function returns only positions meeting specified criteria
            for a symbol name.

        ticket (Optional[int]): Position ticket. Optional named parameter.
            A unique number assigned to each newly opened position.
            It usually matches the ticket of the order used to open the position,
            except when the ticket is changed as a result of service operations on the server,
            for example, when charging swaps with position re-opening.


    Returns:
        list[TradePosition] | None:
        - List of `TradePosition`.

    Notes:
        The method allows receiving all open positions within a specified period.

        The `group` parameter may contain several comma-separated conditions.

        A condition can be set as a mask using '*'.

        The logical negation symbol '!' can be used for exclusion.

        All conditions are applied sequentially, which means conditions for inclusion
        in a group should be specified first, followed by an exclusion condition.

        For example, `group="*, !EUR"` means that deals for all symbols should be selected first,
        and those containing "EUR" in symbol names should be excluded afterward.
    """

    if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
        raise ValueError(
            "Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them."
        )

    if symbol is not None:
        positions = client.positions_get(symbol)
    elif group is not None:
        positions = client.positions_get_by_group(group)
    elif ticket is not None:
        positions = client.position_get_by_ticket(ticket)
    else:
        positions = client.positions_get()

    if positions is None:
        return None
    if isinstance(positions, TradePosition):
        return [positions]
    if len(positions) == 0:
        return None

    return positions
get_orders
get_orders(symbol: str | None = None, group: str | None = None, ticket: int | None = None) -> list[TradeOrder] | None

Get active orders with the ability to filter by symbol or ticket. There are four call options:

  • Call without parameters. Returns open positions for all symbols.
  • Call specifying a symbol, open positions should be received for.
  • Call specifying a group of symbols, open positions should be received for.
  • Call specifying a position ticket.

Parameters:

Name Type Description Default
symbol Optional[str]

Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored.

None
group Optional[str]

The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only positions meeting a specified criteria for a symbol name.

None
ticket Optional[int]

Order ticket. Optional named parameter. Unique number assigned to each order.

None
to_df bool

If True, a DataFrame is returned.

required

Returns:

Type Description
list[TradeOrder] | None

[List[TradeOrder] | None]:

list[TradeOrder] | None
  • List of TradeOrder .
Notes

The method allows receiving all history orders within a specified period. The group parameter may contain several comma-separated conditions. A condition can be set as a mask using '*'.

The logical negation symbol '!' can be used for exclusion. All conditions are applied sequentially, which means conditions for inclusion in a group should be specified first, followed by an exclusion condition.

For example, group="*, !EUR" means that deals for all symbols should be selected first and the ones containing "EUR" in symbol names should be excluded afterward.

Source code in src/bbstrader/metatrader/account.py
def get_orders(
    self,
    symbol: str | None = None,
    group: str | None = None,
    ticket: int | None = None,
) -> list[TradeOrder] | None:
    """
    Get active orders with the ability to filter by symbol or ticket.
    There are four call options:

    - Call without parameters. Returns open positions for all symbols.
    - Call specifying a symbol, open positions should be received for.
    - Call specifying a group of symbols, open positions should be received for.
    - Call specifying a position ticket.

    Args:
        symbol (Optional[str]): Symbol name. Optional named parameter.
            If a symbol is specified, the ticket parameter is ignored.

        group (Optional[str]): The filter for arranging a group of necessary symbols.
            Optional named parameter. If the group is specified,
            the function returns only positions meeting a specified criteria
            for a symbol name.

        ticket (Optional[int]): Order ticket. Optional named parameter.
            Unique number assigned to each order.

        to_df (bool): If True, a DataFrame is returned.

    Returns:
        [List[TradeOrder] | None]:
        - List of `TradeOrder` .

    Notes:
        The method allows receiving all history orders within a specified period.
        The `group` parameter may contain several comma-separated conditions.
        A condition can be set as a mask using '*'.

        The logical negation symbol '!' can be used for exclusion.
        All conditions are applied sequentially, which means conditions for inclusion
        in a group should be specified first, followed by an exclusion condition.

        For example, `group="*, !EUR"` means that deals for all symbols should be selected first
        and the ones containing "EUR" in symbol names should be excluded afterward.
    """

    if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
        raise ValueError(
            "Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them."
        )

    orders = None
    if symbol is not None:
        orders = client.orders_get(symbol)
    elif group is not None:
        orders = client.orders_get_by_group(group)
    elif ticket is not None:
        orders = client.order_get_by_ticket(ticket)
    else:
        orders = client.orders_get()

    if orders is None or len(orders) == 0:
        return None
    return orders
get_trades_history
get_trades_history(date_from: datetime = datetime(2000, 1, 1), date_to: datetime | None = None, group: str | None = None, ticket: int | None = None, position: int | None = None, to_df: bool = True) -> pd.DataFrame | list[TradeDeal] | None

Get deals from trading history within the specified interval with the ability to filter by ticket or position.

This method is useful if you need panda dataframe.

You can call this method in the following ways:

  • Call with a time interval. Returns all deals falling within the specified interval.

  • Call specifying the order ticket. Returns all deals having the specified order ticket in the DEAL_ORDER property.

  • Call specifying the position ticket. Returns all deals having the specified position ticket in the DEAL_POSITION_ID property.

Parameters:

Name Type Description Default
date_from datetime

Date the bars are requested from. Set by the datetime object or as a number of seconds elapsed since 1970-01-01. Bars with the open time >= date_from are returned. Required unnamed parameter.

datetime(2000, 1, 1)
date_to Optional[datetime]

Same as date_from.

None
group Optional[str]

The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only positions meeting specified criteria for a symbol name.

None
ticket Optional[int]

Ticket of an order (stored in DEAL_ORDER) for which all deals should be received. Optional parameter. If not specified, the filter is not applied.

None
position Optional[int]

Ticket of a position (stored in DEAL_POSITION_ID) for which all deals should be received. Optional parameter. If not specified, the filter is not applied.

None
to_df bool

If True, a DataFrame is returned.

True

Returns:

Type Description
DataFrame | list[TradeDeal] | None

Union[pd.DataFrame, Tuple[TradeDeal], None]:

DataFrame | list[TradeDeal] | None
  • TradeDeal in the form of a named tuple structure (namedtuple) or pd.DataFrame().
Notes

The method allows receiving all history orders within a specified period.

The group parameter may contain several comma-separated conditions.

A condition can be set as a mask using '*'.

The logical negation symbol '!' can be used for exclusion.

All conditions are applied sequentially, which means conditions for inclusion in a group should be specified first, followed by an exclusion condition.

For example, group="*, !EUR" means that deals for all symbols should be selected first and those containing "EUR" in symbol names should be excluded afterward.

Example
Get the number of deals in history

from datetime import datetime from_date = datetime(2020, 1, 1) to_date = datetime.now() account = Account() history = account.get_trades_history(from_date, to_date)

Source code in src/bbstrader/metatrader/account.py
def get_trades_history(
    self,
    date_from: datetime = datetime(2000, 1, 1),
    date_to: datetime | None = None,
    group: str | None = None,
    ticket: int | None = None,  # TradeDeal.ticket
    position: int | None = None,  # TradePosition.ticket
    to_df: bool = True,
) -> pd.DataFrame | list[TradeDeal] | None:
    """
    Get deals from trading history within the specified interval
    with the ability to filter by `ticket` or `position`.

    This method is useful if you need panda dataframe.

    You can call this method in the following ways:

    - Call with a `time interval`. Returns all deals falling within the specified interval.

    - Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.

    - Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.

    Args:
        date_from (datetime): Date the bars are requested from.
            Set by the `datetime` object or as a number of seconds elapsed since 1970-01-01.
            Bars with the open time >= `date_from` are returned. Required unnamed parameter.

        date_to (Optional[datetime]): Same as `date_from`.

        group (Optional[str]): The filter for arranging a group of necessary symbols.
            Optional named parameter. If the group is specified,
            the function returns only positions meeting specified criteria
            for a symbol name.

        ticket (Optional[int]): Ticket of an order (stored in `DEAL_ORDER`) for which all deals should be received.
            Optional parameter. If not specified, the filter is not applied.

        position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) for which all deals should be received.
            Optional parameter. If not specified, the filter is not applied.

        to_df (bool): If True, a DataFrame is returned.

    Returns:
        Union[pd.DataFrame, Tuple[TradeDeal], None]:
        - `TradeDeal` in the form of a named tuple structure (namedtuple) or pd.DataFrame().

    Notes:
        The method allows receiving all history orders within a specified period.

        The `group` parameter may contain several comma-separated conditions.

        A condition can be set as a mask using '*'.

        The logical negation symbol '!' can be used for exclusion.

        All conditions are applied sequentially, which means conditions for inclusion
        in a group should be specified first, followed by an exclusion condition.

        For example, `group="*, !EUR"` means that deals for all symbols should be selected first
        and those containing "EUR" in symbol names should be excluded afterward.

    Example:
        >>> # Get the number of deals in history
        >>> from datetime import datetime
        >>> from_date = datetime(2020, 1, 1)
        >>> to_date = datetime.now()
        >>> account = Account()
        >>> history = account.get_trades_history(from_date, to_date)
    """
    return self._fetch_history(
        fetch_type="deals",
        drop_cols=["time_msc", "external_id"],
        time_cols=["time"],
        **dict(
            date_from=date_from,
            date_to=date_to,
            group=group,
            ticket=ticket,
            position=position,
            to_df=to_df,
        ),
    )
get_orders_history
get_orders_history(date_from: datetime = datetime(2000, 1, 1), date_to: datetime | None = None, group: str | None = None, ticket: int | None = None, position: int | None = None, to_df: bool = True) -> pd.DataFrame | list[TradeOrder] | None

Get orders from trading history within the specified interval with the ability to filter by ticket or position.

You can call this method in the following ways:

  • Call with a time interval. Returns all deals falling within the specified interval.

  • Call specifying the order ticket. Returns all deals having the specified order ticket in the DEAL_ORDER property.

  • Call specifying the position ticket. Returns all deals having the specified position ticket in the DEAL_POSITION_ID property.

Parameters:

Name Type Description Default
date_from datetime

Date the bars are requested from. Set by the datetime object or as a number of seconds elapsed since 1970-01-01. Bars with the open time >= date_from are returned. Required unnamed parameter.

datetime(2000, 1, 1)
date_to Optional[datetime]

Same as date_from.

None
group Optional[str]

The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function returns only positions meeting specified criteria for a symbol name.

None
ticket Optional[int]

Order ticket to filter results. Optional parameter. If not specified, the filter is not applied.

None
position Optional[int]

Ticket of a position (stored in DEAL_POSITION_ID) to filter results. Optional parameter. If not specified, the filter is not applied.

None
to_df bool

If True, a DataFrame is returned.

True
save bool

If True, a CSV file will be created to save the history.

required

Returns:

Type Description
DataFrame | list[TradeOrder] | None

Union[pd.DataFrame, List[TradeOrder], None]

DataFrame | list[TradeOrder] | None
  • List of TradeOrder .
Notes

The method allows receiving all history orders within a specified period.

The group parameter may contain several comma-separated conditions.

A condition can be set as a mask using '*'.

The logical negation symbol '!' can be used for exclusion.

All conditions are applied sequentially, which means conditions for inclusion in a group should be specified first, followed by an exclusion condition.

For example, group="*, !EUR" means that deals for all symbols should be selected first and those containing "EUR" in symbol names should be excluded afterward.

Example
Get the number of deals in history

from datetime import datetime from_date = datetime(2020, 1, 1) to_date = datetime.now() account = Account() history = account.get_orders_history(from_date, to_date)

Source code in src/bbstrader/metatrader/account.py
def get_orders_history(
    self,
    date_from: datetime = datetime(2000, 1, 1),
    date_to: datetime | None = None,
    group: str | None = None,
    ticket: int | None = None,  # order ticket
    position: int | None = None,  # position ticket
    to_df: bool = True,
) -> pd.DataFrame | list[TradeOrder] | None:
    """
    Get orders from trading history within the specified interval
    with the ability to filter by `ticket` or `position`.

    You can call this method in the following ways:

    - Call with a `time interval`. Returns all deals falling within the specified interval.

    - Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.

    - Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.

    Args:
        date_from (datetime): Date the bars are requested from.
            Set by the `datetime` object or as a number of seconds elapsed since 1970-01-01.
            Bars with the open time >= `date_from` are returned. Required unnamed parameter.

        date_to (Optional[datetime]): Same as `date_from`.

        group (Optional[str]): The filter for arranging a group of necessary symbols.
            Optional named parameter. If the group is specified,
            the function returns only positions meeting specified criteria
            for a symbol name.

        ticket (Optional[int]): Order ticket to filter results. Optional parameter.
            If not specified, the filter is not applied.

        position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) to filter results.
            Optional parameter. If not specified, the filter is not applied.

        to_df (bool): If True, a DataFrame is returned.

        save (bool): If True, a CSV file will be created to save the history.

    Returns:
        Union[pd.DataFrame, List[TradeOrder], None]
        - List of `TradeOrder` .

    Notes:
        The method allows receiving all history orders within a specified period.

        The `group` parameter may contain several comma-separated conditions.

        A condition can be set as a mask using '*'.

        The logical negation symbol '!' can be used for exclusion.

        All conditions are applied sequentially, which means conditions for inclusion
        in a group should be specified first, followed by an exclusion condition.

        For example, `group="*, !EUR"` means that deals for all symbols should be selected first
        and those containing "EUR" in symbol names should be excluded afterward.

    Example:
        >>> # Get the number of deals in history
        >>> from datetime import datetime
        >>> from_date = datetime(2020, 1, 1)
        >>> to_date = datetime.now()
        >>> account = Account()
        >>> history = account.get_orders_history(from_date, to_date)
    """
    return self._fetch_history(
        fetch_type="orders",
        drop_cols=[
            "time_expiration",
            "type_time",
            "state",
            "position_by_id",
            "reason",
            "volume_current",
            "price_stoplimit",
            "sl",
            "tp",
        ],
        time_cols=["time_setup", "time_done"],
        **dict(
            date_from=date_from,
            date_to=date_to,
            group=group,
            ticket=ticket,
            position=position,
            to_df=to_df,
        ),
    )
get_today_deals
get_today_deals(strategy_id: int, group: str | None = None, lookback_days: int = 3) -> list[TradeDeal]

Get all today deals for a specific strategy magic number.

Parameters:

Name Type Description Default
strategy_id int

Strategy or expert magic number.

required
group str | None

Symbol or group filter.

None
lookback_days int

How many days back to search for open positions.

3

Returns: list[TradeDeal]: Deals closed today belonging to the strategy.

Source code in src/bbstrader/metatrader/account.py
def get_today_deals(
    self,
    strategy_id: int,
    group: str | None = None,
    lookback_days: int = 3,
) -> list[TradeDeal]:
    """
    Get all today deals for a specific strategy magic number.

    Args:
        strategy_id (int): Strategy or expert magic number.
        group (str | None): Symbol or group filter.
        lookback_days (int): How many days back to search for open positions.
    Returns:
        list[TradeDeal]: Deals closed today belonging to the strategy.
    """
    from_date = datetime.now() - timedelta(days=lookback_days)
    history = (
        self.get_trades_history(date_from=from_date, group=group, to_df=False) or []
    )
    positions_ids = {
        deal.position_id for deal in history if deal.magic == strategy_id
    }
    today_deals = []
    for position in positions_ids:
        deals = self.get_trades_history(position=position, to_df=False) or []
        if not deals:
            continue
        last_deal = deals[-1]
        deal_time = datetime.fromtimestamp(last_deal.time)
        if deal_time.date() == datetime.now().date():
            today_deals.append(last_deal)
    return today_deals

broker

Broker

Broker(name: str, timezone: str | None = None, custom_patterns: dict[SymbolType, str] | None = None, custom_countries_stocks: dict[str, str] | None = None, custom_exchanges: dict[str, str] | None = None)
Source code in src/bbstrader/metatrader/broker.py
def __init__(
    self,
    name: str,
    timezone: str | None = None,
    custom_patterns: dict[SymbolType, str] | None = None,
    custom_countries_stocks: dict[str, str] | None = None,
    custom_exchanges: dict[str, str] | None = None,
):
    self._name = name
    self._timezone = timezone
    self._patterns = {**SYMBOLS_TYPE, **(custom_patterns or {})}
    self._countries_stocks = {**COUNTRIES_STOCKS, **(custom_countries_stocks or {})}
    self._exchanges = {**EXCHANGES, **(custom_exchanges or {})}
initialize_connection
initialize_connection(**kwargs) -> bool

Broker-specific connection initialization.

Source code in src/bbstrader/metatrader/broker.py
def initialize_connection(self, **kwargs) -> bool:
    """Broker-specific connection initialization."""
    return check_mt5_connection(**kwargs)
get_terminal_timezone
get_terminal_timezone() -> str

Fetch or override terminal timezone.

Source code in src/bbstrader/metatrader/broker.py
def get_terminal_timezone(self) -> str:
    """Fetch or override terminal timezone."""
    if self._timezone is not None:
        return self._timezone

    symbol = self.get_symbols()[0]
    tick = client.symbol_info_tick(symbol)

    if tick is None:
        return "Unknown (Market might be closed)"

    server_time = tick.time
    utc_now = datetime.now(timezone.utc).timestamp()

    # Check if the tick is stale (e.g., older than 10 hours).
    # This prevents calculating offsets based on weekend gaps.
    if abs(server_time - utc_now) > 3600 * 10:
        # Most Forex/CFD brokers use PLT/EEST (UTC+2 or UTC+3)
        # which maps to Europe/Nicosia or Europe/Athens.
        return "Europe/Nicosia"

    offset_hours = round((server_time - utc_now) / 3600)

    if offset_hours == 0:
        return "UTC"
    elif offset_hours in [2, 3]:
        return "Europe/Nicosia"
    elif offset_hours == 7:
        return "Asia/Bangkok"
    else:
        if -12 <= offset_hours <= 14:
            # Note: Etc/GMT signs are inverted.
            # If offset is +2 (server is ahead), we need Etc/GMT-2
            return f"Etc/GMT{-offset_hours:+d}"
        else:
            return "UTC"

check_mt5_connection

check_mt5_connection(*, path=None, login=None, password=None, server=None, timeout=60000, portable=False, **kwargs) -> bool

Initialize the connection to the MetaTrader 5 terminal.

Parameters

path : str, optional Path to the MetaTrader 5 terminal executable file. Defaults to None (e.g., "C:/Program Files/MetaTrader 5/terminal64.exe"). login : int, optional The login ID of the trading account. Defaults to None. password : str, optional The password of the trading account. Defaults to None. server : str, optional The name of the trade server to which the client terminal is connected. Defaults to None. timeout : int, optional Connection timeout in milliseconds. Defaults to 60_000. portable : bool, optional If True, the portable mode of the terminal is used. Defaults to False. See: https://www.metatrader5.com/en/terminal/help/start_advanced/start#portable

Returns

bool True if the connection is successfully established, otherwise False.

Notes

If you want to launch multiple terminal instances:

  • First, launch each terminal in portable mode.
  • See instructions: https://www.metatrader5.com/en/terminal/help/start_advanced/start#configuration_file
Source code in src/bbstrader/metatrader/broker.py
def check_mt5_connection(
    *,
    path=None,
    login=None,
    password=None,
    server=None,
    timeout=60_000,
    portable=False,
    **kwargs,
) -> bool:
    """
    Initialize the connection to the MetaTrader 5 terminal.

    Parameters
    ----------
    path : str, optional
        Path to the MetaTrader 5 terminal executable file.
        Defaults to ``None`` (e.g., ``"C:/Program Files/MetaTrader 5/terminal64.exe"``).
    login : int, optional
        The login ID of the trading account. Defaults to ``None``.
    password : str, optional
        The password of the trading account. Defaults to ``None``.
    server : str, optional
        The name of the trade server to which the client terminal is connected.
        Defaults to ``None``.
    timeout : int, optional
        Connection timeout in milliseconds. Defaults to ``60_000``.
    portable : bool, optional
        If ``True``, the portable mode of the terminal is used.
        Defaults to ``False``.
        See: https://www.metatrader5.com/en/terminal/help/start_advanced/start#portable

    Returns
    -------
    bool
        ``True`` if the connection is successfully established, otherwise ``False``.

    Notes
    -----
    If you want to launch multiple terminal instances:

    * First, launch each terminal in **portable mode**.
    * See instructions: https://www.metatrader5.com/en/terminal/help/start_advanced/start#configuration_file
    """

    if login is not None and server is not None:
        account_info = mt5.account_info()
        if account_info is not None:
            if account_info.login == login and account_info.server == server:
                return True

    init = False
    if path is None and (login or password or server):
        raise ValueError(
            "You must provide a path to the terminal executable file"
            "when providing login, password or server"
        )
    try:
        if path is not None:
            if login is not None and password is not None and server is not None:
                init = mt5.initialize(
                    path=path,
                    login=login,
                    password=password,
                    server=server,
                    timeout=timeout,
                    portable=portable,
                )
            else:
                init = mt5.initialize(path=path)
        else:
            init = mt5.initialize()
        if not init:
            raise_mt5_error(str(mt5.last_error()) + INIT_MSG)
    except Exception:
        raise_mt5_error(str(mt5.last_error()) + INIT_MSG)
    return init

copier

TradeCopier

TradeCopier(source: dict, destinations: list[dict], /, sleeptime: float = 0.1, start_time: str = None, end_time: str = None, *, custom_logger=None, shutdown_event=None, log_queue=None)

Bases: object

TradeCopier responsible for copying trading orders and positions from a source account to multiple destination accounts.

This class facilitates the synchronization of trades between a source account and multiple destination accounts. It handles copying new orders, modifying existing orders, updating and closing positions based on updates from the source account.

Initializes the TradeCopier instance, setting up the source and destination trading accounts for trade copying.

Parameters:

Name Type Description Default
source dict

A dictionary containing the connection details for the source trading account. This dictionary must include all parameters required to successfully connect to the source account. Refer to the bbstrader.metatrader.check_mt5_connection function for a comprehensive list of required keys and their expected values. Common parameters include, but are not limited to

- `login`:  The account login ID (integer).
- `password`: The account password (string).
- `server`:  The server address (string), e.g., "Broker-Demo".
- `path`:  The path to the MetaTrader 5 installation directory (string).
- `portable`:  A boolean indicating whether to open MetaTrader 5 installation in portable mode.
- `id`: A unique identifier for all trades opened buy the source source account.
    This Must be a positive number greater than 0 and less than 2^32 / 2.
- `unique`: A boolean indication whehter to allow destination accounts to copy from other sources.
    If Set to True, all destination accounts won't be allow to accept trades from other accounts even
    manually opened positions or orders will be removed.
required
destinations list[dict]

A list of dictionaries, where each dictionary represents a destination trading account to which trades will be copied. Each destination dictionary must contain the following keys

- Authentication details (e.g., `login`, `password`, `server`)
Identical in structure and requirements to the `source` dictionary,
ensuring a connection can be established to the destination account.
Refer to ``bbstrader.metatrader.check_mt5_connection``.

- `symbols` (Union[list[str], Dict[str, str], str])
Specifies which symbols should be copied from the source
account to this destination account.  Possible values include
`list[str]` A list of strings, where each string is a symbol to be copied.
    The same symbol will be traded on the destination account.  Example `["EURUSD", "GBPUSD"]`
`Dict[str, str]` A dictionary mapping source symbols to destination symbols.
    This allows for trading a different symbol on the destination account than the one traded on the source.
    Example `{"EURUSD": "EURUSD_i", "GBPUSD": "GBPUSD_i"}`.
`"all"` or `"*"`  Indicates that all symbols traded on the source account should be
    copied to this destination account, using the same symbol name.

- `mode` (str) The risk management mode to use.  Valid options are
`"fix"` Use a fixed lot size.  The `value` key must specify the fixed lot size.
`"multiply"` Multiply the source account's lot size by a factor.
    The `value` key must specify the multiplier.
`"percentage"`  Trade a percentage of the source account's lot size.
    The `value` key must specify the percentage (as a decimal, e.g., 50 for 50%).
`"dynamic"` Calculate the lot size dynamically based on account equity and risk parameters.
    The `value` key is ignored.
`"replicate"` Copy the exact lot size from the source account. The `value` key is ignored.
`"specific"` Use a specific lot size defined in the `value` key for each symbol.

- `value` (float or dict,  optional)  A numerical value or dict used in conjunction with the selected `mode`.
    Its meaning depends on the chosen `mode` (see above). Required for "fix", "multiply", specific
    and "percentage" modes; optional for "dynamic".

- `slippage` (float, optional) The maximum allowed slippage in percentage when opening trades on the destination account,
defaults to 0.1% (0.1), if the slippage exceeds this value, the trade will not be copied.

- `comment` (str, optional) An optional comment to be added to trades opened on the destination account,
defaults to an empty string.

- ``copy_what`` (str, optional)
Specifies what to copy from the source account to the destination accounts.  Valid options are
`"orders"` Copy only orders from the source account to the destination accounts.
`"positions"` Copy only positions from the source account to the destination accounts.
`"all"` Copy both orders and positions from the source account to the destination accounts.
Defaults to `"all"`.
required
sleeptime float

The time interval in seconds between each iteration of the trade copying process. Defaults to 0.1 seconds. It can be useful if you know the frequency of new trades on the source account.

0.1
start_time str

The time (HH:MM) from which the copier start copying from the source.

None
end_time str

The time (HH:MM) from which the copier stop copying from the source.

None
custom_logger (Any, Optional)

Used to set a cutum logger (default is loguru.logger)

None
shutdown_event (Any, Otional)

Use to terminate the copy process when runs in a custum environment like web App or GUI.

None
log_queue (Queue, Optional)

Use to send log to an external program, usefule in GUI apps

None
Note

The source account and the destination accounts must be connected to different MetaTrader 5 platforms. you can copy the initial installation of MetaTrader 5 to a different directory and rename it to create a new instance Then you can connect destination accounts to the new instance while the source account is connected to the original instance.

Source code in src/bbstrader/metatrader/copier.py
def __init__(
    self,
    source: dict,
    destinations: list[dict],
    /,
    sleeptime: float = 0.1,
    start_time: str = None,
    end_time: str = None,
    *,
    custom_logger=None,
    shutdown_event=None,
    log_queue=None,
):
    """
    Initializes the ``TradeCopier`` instance, setting up the source and destination trading accounts for trade copying.

    Args:
        source (dict):
            A dictionary containing the connection details for the source trading account. This dictionary
            **must** include all parameters required to successfully connect to the source account.
            Refer to the ``bbstrader.metatrader.check_mt5_connection`` function for a comprehensive list
            of required keys and their expected values.  Common parameters include, but are not limited to

                - `login`:  The account login ID (integer).
                - `password`: The account password (string).
                - `server`:  The server address (string), e.g., "Broker-Demo".
                - `path`:  The path to the MetaTrader 5 installation directory (string).
                - `portable`:  A boolean indicating whether to open MetaTrader 5 installation in portable mode.
                - `id`: A unique identifier for all trades opened buy the source source account.
                    This Must be a positive number greater than 0 and less than 2^32 / 2.
                - `unique`: A boolean indication whehter to allow destination accounts to copy from other sources.
                    If Set to True, all destination accounts won't be allow to accept trades from other accounts even
                    manually opened positions or orders will be removed.

        destinations (list[dict]):
            A list of dictionaries, where each dictionary represents a destination trading account to which
            trades will be copied.  Each destination dictionary **must** contain the following keys

                - Authentication details (e.g., `login`, `password`, `server`)
                Identical in structure and requirements to the `source` dictionary,
                ensuring a connection can be established to the destination account.
                Refer to ``bbstrader.metatrader.check_mt5_connection``.

                - `symbols` (Union[list[str], Dict[str, str], str])
                Specifies which symbols should be copied from the source
                account to this destination account.  Possible values include
                `list[str]` A list of strings, where each string is a symbol to be copied.
                    The same symbol will be traded on the destination account.  Example `["EURUSD", "GBPUSD"]`
                `Dict[str, str]` A dictionary mapping source symbols to destination symbols.
                    This allows for trading a different symbol on the destination account than the one traded on the source.
                    Example `{"EURUSD": "EURUSD_i", "GBPUSD": "GBPUSD_i"}`.
                `"all"` or `"*"`  Indicates that all symbols traded on the source account should be
                    copied to this destination account, using the same symbol name.

                - `mode` (str) The risk management mode to use.  Valid options are
                `"fix"` Use a fixed lot size.  The `value` key must specify the fixed lot size.
                `"multiply"` Multiply the source account's lot size by a factor.
                    The `value` key must specify the multiplier.
                `"percentage"`  Trade a percentage of the source account's lot size.
                    The `value` key must specify the percentage (as a decimal, e.g., 50 for 50%).
                `"dynamic"` Calculate the lot size dynamically based on account equity and risk parameters.
                    The `value` key is ignored.
                `"replicate"` Copy the exact lot size from the source account. The `value` key is ignored.
                `"specific"` Use a specific lot size defined in the `value` key for each symbol.

                - `value` (float or dict,  optional)  A numerical value or dict used in conjunction with the selected `mode`.
                    Its meaning depends on the chosen `mode` (see above). Required for "fix", "multiply", specific
                    and "percentage" modes; optional for "dynamic".

                - `slippage` (float, optional) The maximum allowed slippage in percentage when opening trades on the destination account,
                defaults to 0.1% (0.1), if the slippage exceeds this value, the trade will not be copied.

                - `comment` (str, optional) An optional comment to be added to trades opened on the destination account,
                defaults to an empty string.

                - ``copy_what`` (str, optional)
                Specifies what to copy from the source account to the destination accounts.  Valid options are
                `"orders"` Copy only orders from the source account to the destination accounts.
                `"positions"` Copy only positions from the source account to the destination accounts.
                `"all"` Copy both orders and positions from the source account to the destination accounts.
                Defaults to `"all"`.

        sleeptime (float, optional):
            The time interval in seconds between each iteration of the trade copying process.
            Defaults to 0.1 seconds. It can be useful if you know the frequency of new trades on the source account.

        start_time (str, optional): The time (HH:MM) from which the copier start copying from the source.
        end_time (str, optional): The time (HH:MM) from which the copier stop copying from the source.
        custom_logger (Any, Optional): Used to set a cutum logger (default is ``loguru.logger``)
        shutdown_event (Any, Otional): Use to terminate the copy process when runs in a custum environment like web App or GUI.
        log_queue (multiprocessing.Queue, Optional): Use to send log to an external program, usefule in GUI apps

    Note:
        The source account and the destination accounts must be connected to different MetaTrader 5 platforms.
        you can copy the initial installation of MetaTrader 5 to a different directory and rename it to create a new instance
        Then you can connect destination accounts to the new instance while the source account is connected to the original instance.
    """
    self.source = source
    self.source_id = source.get("id", 0)
    self.source_isunique = source.get("unique", True)
    self.destinations = destinations
    self.sleeptime = sleeptime
    self.start_time = start_time
    self.end_time = end_time
    self.errors = set()
    self.log_queue = log_queue
    self._add_logger(custom_logger)
    self._validate_source()
    self.shutdown_event = (
        shutdown_event if shutdown_event is not None else mp.Event()
    )
    self._last_session = datetime.now().date()
    self._running = True
running property
running

Check if the Trade Copier is running.

start_copy_process
start_copy_process(destination: dict)

Worker process: copies orders and positions concurrently for a single destination account.

Source code in src/bbstrader/metatrader/copier.py
def start_copy_process(self, destination: dict):
    """
    Worker process: copies orders and positions concurrently for a single destination account.
    """
    if destination.get("path") == self.source.get("path"):
        self.log_message(
            f"Source and destination accounts are on the same MetaTrader 5 "
            f"installation ({self.source.get('path')}), which is not allowed."
        )
        return

    self.log_message(
        f"Copy process started for source @{self.source.get('login')} "
        f"and destination @{destination.get('login')}"
    )
    while not self.shutdown_event.is_set():
        try:
            self.copy_positions(destination)
            self.copy_orders(destination)
        except KeyboardInterrupt:
            self.log_message(
                "KeyboardInterrupt received, stopping the Trade Copier..."
            )
            self.stop()
        except Exception as e:
            self.log_error(f"An error occurred during the sync cycle: {e}")
        time.sleep(self.sleeptime)

    self.log_message(
        f"Process exiting for destination @{destination.get('login')} due to shutdown event."
    )
run
run()

Entry point: Starts a dedicated worker thread for EACH destination account to run concurrently.

Source code in src/bbstrader/metatrader/copier.py
def run(self):
    """
    Entry point: Starts a dedicated worker thread for EACH destination account to run concurrently.
    """
    self.log_message(
        f"Main Copier instance starting for source @{self.source.get('login')}."
    )
    self.log_message(
        f"Found {len(self.destinations)} destination accounts to process in parallel."
    )
    if len(set([d.get("path") for d in self.destinations])) < len(
        self.destinations
    ):
        self.log_message(
            "Two or more destination accounts have the same Terminal path, which is not allowed.",
            type="error",
        )
        return

    worker_threads = []

    for destination in self.destinations:
        self.log_message(
            f"Creating worker thread for destination @{destination.get('login')}"
        )
        try:
            thread = threading.Thread(
                target=self.start_copy_process,
                args=(destination,),
                name=f"Worker-{destination.get('login')}",
            )
            worker_threads.append(thread)
            thread.start()
        except Exception as e:
            self.log_error(
                f"Error executing thread Worker-{destination.get('login')} : {e}"
            )

    self.log_message(f"All {len(worker_threads)} worker threads have been started.")
    try:
        while not self.shutdown_event.is_set():
            time.sleep(1)
    except KeyboardInterrupt:
        self.log_message(
            "\nKeyboardInterrupt detected by main thread. Initiating shutdown..."
        )
    finally:
        self.stop()
        self.log_message("Waiting for all worker threads to complete...")
        for thread in worker_threads:
            thread.join()

        self.log_message("All worker threads have shut down. Copier exiting.")
stop
stop()

Stop the Trade Copier gracefully by setting the shutdown event.

Source code in src/bbstrader/metatrader/copier.py
def stop(self):
    """
    Stop the Trade Copier gracefully by setting the shutdown event.
    """
    if self._running:
        self.log_message(
            f"Signaling stop for Trade Copier on source account @{self.source.get('login')}..."
        )
        self._running = False
        self.shutdown_event.set()
    self.log_message("Trade Copier stopped successfully.")

copier_worker_process

copier_worker_process(source_config: dict, destination_config: dict, sleeptime: float, start_time: str, end_time: str, /, custom_logger=None, shutdown_event=None, log_queue=None)

A top-level worker function for handling a single source-to-destination copy task.

This function is the cornerstone of the robust, multi-process architecture. It is designed to be the target of a multiprocessing.Process. By being a top-level function, it avoids pickling issues on Windows and ensures that each copy task runs in a completely isolated process.

A controller (like a GUI or a master script) should spawn one process with this target for each destination account it needs to manage.

Parameters:

Name Type Description Default
source_config dict

Configuration dictionary for the source account. Must contain 'login', 'password', 'server', and 'path'.

required
destination_config dict

Configuration dictionary for a single destination account.

required
sleeptime float

The time in seconds to wait between copy cycles.

required
start_time str

The time of day to start copying (e.g., "08:00").

required
end_time str

The time of day to stop copying (e.g., "22:00").

required
custom_logger

An optional custom logger instance.

None
shutdown_event Event

An event object that, when set, will signal this process to terminate gracefully.

None
log_queue Queue

A queue for sending log messages back to the parent process in a thread-safe manner.

None
Source code in src/bbstrader/metatrader/copier.py
def copier_worker_process(
    source_config: dict,
    destination_config: dict,
    sleeptime: float,
    start_time: str,
    end_time: str,
    /,
    custom_logger=None,
    shutdown_event=None,
    log_queue=None,
):
    """A top-level worker function for handling a single source-to-destination copy task.

    This function is the cornerstone of the robust, multi-process architecture. It is
    designed to be the `target` of a `multiprocessing.Process`. By being a top-level
    function, it avoids pickling issues on Windows and ensures that each copy task
    runs in a completely isolated process.

    A controller (like a GUI or a master script) should spawn one process with this
    target for each destination account it needs to manage.

    Args:
        source_config (dict): Configuration dictionary for the source account.
            Must contain 'login', 'password', 'server', and 'path'.
        destination_config (dict): Configuration dictionary for a *single*
            destination account.
        sleeptime (float): The time in seconds to wait between copy cycles.
        start_time (str): The time of day to start copying (e.g., "08:00").
        end_time (str): The time of day to stop copying (e.g., "22:00").
        custom_logger: An optional custom logger instance.
        shutdown_event (multiprocessing.Event): An event object that, when set,
            will signal this process to terminate gracefully.
        log_queue (multiprocessing.Queue): A queue for sending log messages back
            to the parent process in a thread-safe manner.
    """
    copier = TradeCopier(
        source_config,
        [destination_config],
        sleeptime=sleeptime,
        start_time=start_time,
        end_time=end_time,
        custom_logger=custom_logger,
        shutdown_event=shutdown_event,
        log_queue=log_queue,
    )
    copier.start_copy_process(destination_config)

RunCopier

RunCopier(source: dict, destinations: list, sleeptime: float, start_time: str, end_time: str, /, custom_logger=None, shutdown_event=None, log_queue=None)

Initialize and run a TradeCopier instance in a single process.

This function serves as a straightforward wrapper to start a copying session that handles one source account and one or more destination accounts sequentially within the same thread. It does not create any new processes itself.

Use Cases
  • Simpler, command-line based use cases.
  • Scenarios where parallelism is not required.
  • As the target for RunMultipleCopier, where each process handles a full source-to-destinations session.
Parameters

source : dict Configuration dictionary for the source account. destinations : list A list of configuration dictionaries, one for each destination account to be processed sequentially. sleeptime : float The time in seconds to wait after completing a full cycle through all destinations. start_time : str The time of day to start copying (e.g., "08:00"). end_time : str The time of day to stop copying (e.g., "22:00"). custom_logger : logging.Logger, optional An optional custom logger instance. shutdown_event : multiprocessing.Event, optional An event to signal shutdown. log_queue : multiprocessing.Queue, optional A queue for log messages.

Returns

None Runs until stopped via shutdown_event or external interruption.

Source code in src/bbstrader/metatrader/copier.py
def RunCopier(
    source: dict,
    destinations: list,
    sleeptime: float,
    start_time: str,
    end_time: str,
    /,
    custom_logger=None,
    shutdown_event=None,
    log_queue=None,
):
    """
    Initialize and run a TradeCopier instance in a single process.

    This function serves as a straightforward wrapper to start a copying session
    that handles one source account and one or more destination accounts
    sequentially within the same thread. It does not create any new processes itself.

    Use Cases
    ---------
    * Simpler, command-line based use cases.
    * Scenarios where parallelism is not required.
    * As the target for ``RunMultipleCopier``, where each process handles a
      full source-to-destinations session.

    Parameters
    ----------
    source : dict
        Configuration dictionary for the source account.
    destinations : list
        A list of configuration dictionaries, one for each
        destination account to be processed sequentially.
    sleeptime : float
        The time in seconds to wait after completing a full
        cycle through all destinations.
    start_time : str
        The time of day to start copying (e.g., ``"08:00"``).
    end_time : str
        The time of day to stop copying (e.g., ``"22:00"``).
    custom_logger : logging.Logger, optional
        An optional custom logger instance.
    shutdown_event : multiprocessing.Event, optional
        An event to signal shutdown.
    log_queue : multiprocessing.Queue, optional
        A queue for log messages.

    Returns
    -------
    None
        Runs until stopped via ``shutdown_event`` or external interruption.
    """
    copier = TradeCopier(
        source,
        destinations,
        sleeptime=sleeptime,
        start_time=start_time,
        end_time=end_time,
        custom_logger=custom_logger,
        shutdown_event=shutdown_event,
        log_queue=log_queue,
    )
    copier.run()

RunMultipleCopier

RunMultipleCopier(accounts: list[dict], sleeptime: float = 0.01, start_delay: float = 1.0, start_time: str = None, end_time: str = None, shutdown_event=None, custom_logger=None, log_queue=None)

Manage multiple, independent trade copying sessions in parallel.

This function acts as a high-level manager that takes a list of account setups and creates a separate, dedicated process for each one. Each process is responsible for copying from one source account to its associated list of destination accounts.

The parallelism occurs at the source account level. Within each spawned process, the destinations for that source are handled sequentially by RunCopier.

Example

An example accounts structure:

.. code-block:: python

accounts = [
    {"source": {...}, "destinations": [{...}, {...}]},  # -> Process 1
    {"source": {...}, "destinations": [{...}]}          # -> Process 2
]
Parameters

accounts : list of dict A list of account configurations. Each item must be a dictionary with a source key and a destinations key. sleeptime : float, optional The sleep time passed down to each RunCopier process. start_delay : float, optional A delay in seconds between starting each new process. Helps prevent resource contention by staggering the initialization of multiple MetaTrader 5 terminals. start_time : str, optional The start time passed down to each RunCopier process. end_time : str, optional The end time passed down to each RunCopier process. shutdown_event : multiprocessing.Event, optional An event to signal shutdown to all child processes. custom_logger : logging.Logger, optional An optional custom logger instance. log_queue : multiprocessing.Queue, optional A queue for aggregating log messages from all child processes.

Returns

None Runs until stopped via shutdown_event or external interruption.

Source code in src/bbstrader/metatrader/copier.py
def RunMultipleCopier(
    accounts: list[dict],
    sleeptime: float = 0.01,
    start_delay: float = 1.0,
    start_time: str = None,
    end_time: str = None,
    shutdown_event=None,
    custom_logger=None,
    log_queue=None,
):
    """
    Manage multiple, independent trade copying sessions in parallel.

    This function acts as a high-level manager that takes a list of account
    setups and creates a separate, dedicated process for each one. Each process
    is responsible for copying from one source account to its associated list of
    destination accounts.

    The parallelism occurs at the **source account level**. Within each spawned
    process, the destinations for that source are handled sequentially by
    ``RunCopier``.

    Example
    -------
    An example ``accounts`` structure:

    .. code-block:: python

        accounts = [
            {"source": {...}, "destinations": [{...}, {...}]},  # -> Process 1
            {"source": {...}, "destinations": [{...}]}          # -> Process 2
        ]

    Parameters
    ----------
    accounts : list of dict
        A list of account configurations. Each item must be a dictionary with
        a ``source`` key and a ``destinations`` key.
    sleeptime : float, optional
        The sleep time passed down to each ``RunCopier`` process.
    start_delay : float, optional
        A delay in seconds between starting each new process.
        Helps prevent resource contention by staggering the initialization of
        multiple MetaTrader 5 terminals.
    start_time : str, optional
        The start time passed down to each ``RunCopier`` process.
    end_time : str, optional
        The end time passed down to each ``RunCopier`` process.
    shutdown_event : multiprocessing.Event, optional
        An event to signal shutdown to all child processes.
    custom_logger : logging.Logger, optional
        An optional custom logger instance.
    log_queue : multiprocessing.Queue, optional
        A queue for aggregating log messages from all child processes.

    Returns
    -------
    None
        Runs until stopped via ``shutdown_event`` or external interruption.
    """
    processes = []

    for account in accounts:
        source = account.get("source")
        destinations = account.get("destinations")

        if not source or not destinations:
            logger.warning("Skipping account due to missing source or destinations.")
            continue
        paths = set([source.get("path")] + [dest.get("path") for dest in destinations])
        if len(paths) == 1 and len(destinations) >= 1:
            logger.warning(
                "Skipping account: source and destination cannot share the same MetaTrader 5 terminal path."
            )
            continue
        logger.info(f"Starting process for source account @{source.get('login')}")
        process = mp.Process(
            target=RunCopier,
            args=(
                source,
                destinations,
                sleeptime,
                start_time,
                end_time,
            ),
            kwargs=dict(
                custom_logger=custom_logger,
                shutdown_event=shutdown_event,
                log_queue=log_queue,
            ),
        )
        processes.append(process)
        process.start()

        if start_delay:
            time.sleep(start_delay)

    for process in processes:
        process.join()

auto_convert

auto_convert(value: str) -> bool | None | int | float | str

Convert string values to appropriate data types

Source code in src/bbstrader/metatrader/copier.py
def auto_convert(value: str) -> bool | None | int | float | str:
    """Convert string values to appropriate data types"""
    if value.lower() in {"true", "false"}:  # Boolean
        return value.lower() == "true"
    elif value.lower() in {"none", "null"}:  # None
        return None
    elif value.isdigit():
        return int(value)
    try:
        return float(value)
    except ValueError:
        return value

dict_from_ini

dict_from_ini(file_path: str, sections: str | list[str] | None = None) -> dict[str, Any]

Reads an INI file and converts it to a dictionary with proper data types. Args: file_path: Path to the INI file to read. sections: Optional list of sections to read from the INI file. Returns: A dictionary containing the INI file contents with proper data types.

Source code in src/bbstrader/metatrader/copier.py
def dict_from_ini(
    file_path: str, sections: str | list[str] | None = None
) -> dict[str, Any]:
    """Reads an INI file and converts it to a dictionary with proper data types.
    Args:
        file_path: Path to the INI file to read.
        sections: Optional list of sections to read from the INI file.
    Returns:
        A dictionary containing the INI file contents with proper data types.
    """
    config = configparser.ConfigParser(interpolation=None)
    config.read(file_path)
    ini_dict: dict[str, Any] = {}
    for section in config.sections():
        ini_dict[section] = {
            key: auto_convert(value) for key, value in config.items(section)
        }

    if isinstance(sections, str):
        try:
            return ini_dict[sections]
        except KeyError:
            raise KeyError(f"{sections} not found in the {file_path} file")
    if isinstance(sections, list):
        sect_dict: dict[str, Any] = {}
        for section in sections:
            try:
                sect_dict[section] = ini_dict[section]
            except KeyError:
                raise KeyError(f"{section} not found in the {file_path} file")
        return sect_dict
    return ini_dict

config_copier

config_copier(source_section: str = None, dest_sections: str | list[str] = None, inifile: str | Path = None) -> tuple[dict, list[dict]]

Read the configuration file and return the source and destination account details.

Parameters:

Name Type Description Default
inifile str | Path

The path to the INI configuration file.

None
source_section str

The section name of the source account, defaults to "SOURCE".

None
dest_sections str | list[str]

The section name(s) of the destination account(s).

None

Returns:

Type Description
tuple[dict, list[dict]]

tuple[dict, list[dict]]: A tuple containing the source account and a list of destination accounts.

Example
from pathlib import Path
config_file = ~/.bbstrader/copier/copier.ini
source, destinations = config_copier(config_file, "SOURCE", ["DEST1", "DEST2"])
Source code in src/bbstrader/metatrader/copier.py
def config_copier(
    source_section: str = None,
    dest_sections: str | list[str] = None,
    inifile: str | Path = None,
) -> tuple[dict, list[dict]]:
    """
    Read the configuration file and return the source and destination account details.

    Args:
        inifile (str | Path): The path to the INI configuration file.
        source_section (str): The section name of the source account, defaults to "SOURCE".
        dest_sections (str | list[str]): The section name(s) of the destination account(s).

    Returns:
        tuple[dict, list[dict]]: A tuple containing the source account and a list of destination accounts.

    Example:
        ```python
        from pathlib import Path
        config_file = ~/.bbstrader/copier/copier.ini
        source, destinations = config_copier(config_file, "SOURCE", ["DEST1", "DEST2"])
        ```
    """

    if not inifile:
        inifile = Path().home() / ".bbstrader" / "copier" / "copier.ini"
        if not inifile.exists() or not inifile.is_file():
            raise FileNotFoundError(f"{inifile} not found")

    if not source_section:
        source_section = "SOURCE"

    config = dict_from_ini(inifile)
    try:
        source = config.pop(source_section)
    except KeyError:
        raise ValueError(f"Source section {source_section} not found in {inifile}")
    dest_sections = dest_sections or config.keys()
    if not dest_sections:
        raise ValueError("No destination sections found in the configuration file")

    destinations = []

    if isinstance(dest_sections, str):
        dest_sections = [dest_sections]

    for dest_section in dest_sections:
        try:
            section = config[dest_section]
        except KeyError:
            raise ValueError(
                f"Destination section {dest_section} not found in {inifile}"
            )
        _parse_symbols(section)
        _parse_lots(section)
        destinations.append(section)

    return source, destinations

rates

Rates

Rates(symbol: str, timeframe: str = 'D1', start_pos: int = 0, count: int | None = MAX_BARS, **kwargs)

Provides methods to retrieve historical financial data from MetaTrader 5.

This class encapsulates interactions with the MetaTrader 5 (MT5) terminal to fetch historical price data for a given symbol and timeframe. It offers flexibility in retrieving data either by specifying a starting position and count of bars or by providing a specific date range .

Notes

All data is rerturn as pandas.DataFrame

  1. Befor using this class, ensure that the Max bars in chart in your terminal is set to a value that is greater than the number of bars you want to retrieve or just set it to Unlimited. In your MT5 terminal, go to Tools -> Options -> Charts -> Max bars in chart.

  2. The open, high, low, close, adjclose, returns, volume properties returns data in Broker's timezone by default.

See bbstrader.metatrader.broker.check_mt5_connection() for more details on how to connect to MT5 terminal.

Example

rates = Rates("EURUSD", "1h") df = rates.get_historical_data( ... date_from=datetime(2023, 1, 1), ... date_to=datetime(2023, 1, 10), ... ) print(df.head())

Initializes a new Rates instance.

Parameters:

Name Type Description Default
symbol str

Financial instrument symbol (e.g., "EURUSD").

required
timeframe str

Timeframe string (e.g., "D1", "1h", "5m").

'D1'
start_pos int

Starting index (int) for data retrieval.

0
count int

Number of bars to retrieve default is the maximum bars availble in the MT5 terminal.

MAX_BARS

Raises: ValueError: If the provided timeframe is invalid.

Source code in src/bbstrader/metatrader/rates.py
def __init__(
    self,
    symbol: str,
    timeframe: str = "D1",
    start_pos: int = 0,
    count: int | None = MAX_BARS,
    **kwargs,
):
    """
    Initializes a new Rates instance.

    Args:
        symbol (str): Financial instrument symbol (e.g., "EURUSD").
        timeframe (str): Timeframe string (e.g., "D1", "1h", "5m").
        start_pos (int): Starting index (int) for data retrieval.
        count (int, optional): Number of bars to retrieve default is
            the maximum bars availble in the MT5 terminal.
    Raises:
        ValueError: If the provided timeframe is invalid.
    """
    self.symbol = symbol
    self.start_pos = start_pos
    self.count = count
    self.time_frame = self._validate_time_frame(timeframe)
    self.__account = Account(**kwargs)
    self.__data = self.get_rates_from_pos
returns property
returns

Fractional change between the current and a prior element.

Computes the fractional change from the immediately previous row by default. This is useful in comparing the fraction of change in a time series of elements.

Note

It calculates fractional change (also known as per unit change or relative change) and not percentage change. If you need the percentage change, multiply these values by 100.

get_rates_from_pos
get_rates_from_pos(filter=False, fill_na=False, lower_colnames=False, utc=False) -> pd.DataFrame | None

Retrieves historical data starting from a specific position.

Uses the start_pos and count attributes specified during initialization to fetch data.

Parameters:

Name Type Description Default
filter

See Rates.get_historical_data for more details.

required
fill_na

See Rates.get_historical_data for more details.

required
lower_colnames

If True, the column names will be converted to lowercase.

required
utc bool

If True, the data will be in UTC timezone. Defaults to False.

False

Returns:

Type Description
DataFrame | None

Union[pd.DataFrame, None]: A DataFrame containing historical

DataFrame | None

data if successful, otherwise None.

Raises:

Type Description
ValueError

If start_pos or count is not provided during initialization.

Notes

The Datetime for this method is in Broker's timezone.

Source code in src/bbstrader/metatrader/rates.py
def get_rates_from_pos(
    self, filter=False, fill_na=False, lower_colnames=False, utc=False
) -> pd.DataFrame | None:
    """
    Retrieves historical data starting from a specific position.

    Uses the `start_pos` and `count` attributes specified during
    initialization to fetch data.

    Args:
        filter : See `Rates.get_historical_data` for more details.
        fill_na : See `Rates.get_historical_data` for more details.
        lower_colnames : If True, the column names will be converted to lowercase.
        utc (bool, optional): If True, the data will be in UTC timezone.
            Defaults to False.

    Returns:
        Union[pd.DataFrame, None]: A DataFrame containing historical
        data if successful, otherwise None.

    Raises:
        ValueError: If `start_pos` or `count` is not provided during
            initialization.

    Notes:
        The Datetime for this method is in Broker's timezone.
    """
    if self.start_pos is None or self.count is None:
        raise ValueError(
            "Both 'start_pos' and 'count' must be provided "
            "when calling 'get_rates_from_pos'."
        )
    utc = self._check_filter(filter, utc)
    df = self._fetch_data(
        self.start_pos, self.count, lower_colnames=lower_colnames, utc=utc
    )
    if df is None:
        return None
    if filter:
        return self._filter_data(df, fill_na=fill_na)
    return df
get_rates_from
get_rates_from(date_from: datetime | Timestamp, count: int = MAX_BARS, filter=False, fill_na=False, lower_colnames=False, utc=False) -> pd.DataFrame | None

Retrieves historical data within a specified date range.

Parameters:

Name Type Description Default
date_from

Starting date for data retrieval. The data will be retrieved from this date going to the past.

required
count

Number of bars to retrieve.

required
filter

See Rates.get_historical_data for more details.

required
fill_na

See Rates.get_historical_data for more details.

required
lower_colnames

If True, the column names will be converted to lowercase.

required
utc bool

If True, the data will be in UTC timezone. Defaults to False.

False

Returns:

Type Description
DataFrame | None

Union[pd.DataFrame, None]: A DataFrame containing historical

DataFrame | None

data if successful, otherwise None.

Source code in src/bbstrader/metatrader/rates.py
def get_rates_from(
    self,
    date_from: datetime | pd.Timestamp,
    count: int = MAX_BARS,
    filter=False,
    fill_na=False,
    lower_colnames=False,
    utc=False,
) -> pd.DataFrame | None:
    """
    Retrieves historical data within a specified date range.

    Args:
        date_from : Starting date for data retrieval.
            The data will be retrieved from this date going to the past.

        count : Number of bars to retrieve.

        filter : See `Rates.get_historical_data` for more details.
        fill_na : See `Rates.get_historical_data` for more details.
        lower_colnames : If True, the column names will be converted to lowercase.
        utc (bool, optional): If True, the data will be in UTC timezone.
            Defaults to False.

    Returns:
        Union[pd.DataFrame, None]: A DataFrame containing historical
        data if successful, otherwise None.
    """
    utc = self._check_filter(filter, utc)
    df = self._fetch_data(date_from, count, lower_colnames=lower_colnames, utc=utc)
    if df is None:
        return None
    if filter:
        return self._filter_data(df, fill_na=fill_na)
    return df
get_historical_data
get_historical_data(date_from: datetime | Timestamp, date_to: datetime | Timestamp = pd.Timestamp.now(), utc: bool = False, filter: bool | None = False, fill_na: bool | str | None = False, lower_colnames: bool | None = True, save_csv: bool | None = False) -> pd.DataFrame | None

Retrieves historical data within a specified date range.

Parameters:

Name Type Description Default
date_from

Starting date for data retrieval.

required
date_to

Ending date for data retrieval. Defaults to the current time.

required
utc

If True, the data will be in UTC timezone. Defaults to False.

required
filter

If True, the data will be filtered based on the trading sessions for the symbol. This is use when we want to use the data for backtesting using Zipline.

required
fill_na

If True, the data will be filled with the nearest value. This is use only when filter is True and time frame is "1m" or "D1", this is because we use calendar.minutes_in_range or calendar.sessions_in_range where calendar is the ExchangeCalendar from exchange_calendars package. So, for "1m" or "D1" time frame, the data will be filled with the nearest value because the data from MT5 will have approximately the same number of rows as the number of trading days or minute in the exchange calendar, so we can fill the missing data with the nearest value.

But for other time frames, the data will be reindexed with the exchange calendar because the data from MT5 will have more rows than the number of trading days or minute in the exchange calendar. So we only take the data that is in the range of the exchange calendar sessions or minutes.

required
lower_colnames

If True, the column names will be converted to lowercase.

required
save_csv

File path to save the data as a CSV. If None, the data won't be saved.

required

Returns:

Type Description
DataFrame | None

Union[pd.DataFrame, None]: A DataFrame containing historical data if successful, otherwise None.

Raises:

Type Description
ValueError

If the starting date is greater than the ending date.

Notes

The filter for this method can be use only for Admira Markets Group (AMG) symbols. The Datetime for this method is in Local timezone by default. All STK symbols are filtered based on the the exchange calendar. All FX symbols are filtered based on the us_futures calendar. All IDX symbols are filtered based on the exchange calendar of margin currency. All COMD symbols are filtered based on the exchange calendar of the commodity.

Source code in src/bbstrader/metatrader/rates.py
def get_historical_data(
    self,
    date_from: datetime | pd.Timestamp,
    date_to: datetime | pd.Timestamp = pd.Timestamp.now(),
    utc: bool = False,
    filter: bool | None = False,
    fill_na: bool | str | None = False,
    lower_colnames: bool | None = True,
    save_csv: bool | None = False,
) -> pd.DataFrame | None:
    """
    Retrieves historical data within a specified date range.

    Args:
        date_from : Starting date for data retrieval.

        date_to : Ending date for data retrieval.
            Defaults to the current time.

        utc : If True, the data will be in UTC timezone.
            Defaults to False.

        filter : If True, the data will be filtered based
            on the trading sessions for the symbol.
            This is use when we want to use the data for backtesting using Zipline.

        fill_na : If True, the data will be filled with the nearest value.
            This is use only when `filter` is True and time frame is "1m" or "D1",
            this is because we use ``calendar.minutes_in_range`` or ``calendar.sessions_in_range``
            where calendar is the ``ExchangeCalendar`` from `exchange_calendars` package.
            So, for "1m" or "D1" time frame, the data will be filled with the nearest value
            because the data from MT5 will have approximately the same number of rows as the
            number of trading days or minute in the exchange calendar, so we can fill the missing
            data with the nearest value.

            But for other time frames, the data will be reindexed with the exchange calendar
            because the data from MT5 will have more rows than the number of trading days or minute
            in the exchange calendar. So we only take the data that is in the range of the exchange
            calendar sessions or minutes.

        lower_colnames : If True, the column names will be converted to lowercase.

        save_csv : File path to save the data as a CSV.
            If None, the data won't be saved.

    Returns:
        Union[pd.DataFrame, None]: A DataFrame containing historical data
            if successful, otherwise None.

    Raises:
        ValueError: If the starting date is greater than the ending date.

    Notes:
        The `filter` for this method can be use only for Admira Markets Group (AMG) symbols.
        The Datetime for this method is in Local timezone by default.
        All STK symbols are filtered based on the the exchange calendar.
        All FX symbols are filtered based on the ``us_futures`` calendar.
        All IDX symbols are filtered based on the exchange calendar of margin currency.
        All COMD symbols are filtered based on the exchange calendar of the commodity.
    """
    utc = self._check_filter(filter, utc)
    df = self._fetch_data(
        date_from, date_to, lower_colnames=lower_colnames, utc=utc
    )
    if df is None:
        return None
    if filter:
        df = self._filter_data(
            df, date_from=date_from, date_to=date_to, fill_na=fill_na
        )
    if save_csv:
        df.to_csv(f"{self.symbol}.csv")
    return df

download_historical_data

download_historical_data(symbol, timeframe, date_from, date_to=pd.Timestamp.now(), lower_colnames=True, utc=False, filter=False, fill_na=False, save_csv=False, **kwargs)

Download historical data from MetaTrader 5 terminal. See Rates.get_historical_data for more details.

Source code in src/bbstrader/metatrader/rates.py
def download_historical_data(
    symbol,
    timeframe,
    date_from,
    date_to=pd.Timestamp.now(),
    lower_colnames=True,
    utc=False,
    filter=False,
    fill_na=False,
    save_csv=False,
    **kwargs,
):
    """Download historical data from MetaTrader 5 terminal.
    See `Rates.get_historical_data` for more details.
    """
    rates = Rates(symbol, timeframe, **kwargs)
    data = rates.get_historical_data(
        date_from=date_from,
        date_to=date_to,
        save_csv=save_csv,
        utc=utc,
        filter=filter,
        lower_colnames=lower_colnames,
    )
    return data

get_data_from_pos

get_data_from_pos(symbol, timeframe, start_pos=0, fill_na=False, count=MAX_BARS, lower_colnames=False, utc=False, filter=False, session_duration=23.0, **kwargs)

Get historical data from a specific position. See Rates.get_rates_from_pos for more details.

Source code in src/bbstrader/metatrader/rates.py
def get_data_from_pos(
    symbol,
    timeframe,
    start_pos=0,
    fill_na=False,
    count=MAX_BARS,
    lower_colnames=False,
    utc=False,
    filter=False,
    session_duration=23.0,
    **kwargs,
):
    """Get historical data from a specific position.
    See `Rates.get_rates_from_pos` for more details.
    """
    rates = Rates(symbol, timeframe, start_pos, count, **kwargs)
    data = rates.get_rates_from_pos(
        filter=filter, fill_na=fill_na, lower_colnames=lower_colnames, utc=utc
    )
    return data

get_data_from_date

get_data_from_date(symbol, timeframe, date_from, count=MAX_BARS, fill_na=False, lower_colnames=False, utc=False, filter=False, **kwargs)

Get historical data from a specific date. See Rates.get_rates_from for more details.

Source code in src/bbstrader/metatrader/rates.py
def get_data_from_date(
    symbol,
    timeframe,
    date_from,
    count=MAX_BARS,
    fill_na=False,
    lower_colnames=False,
    utc=False,
    filter=False,
    **kwargs,
):
    """Get historical data from a specific date.
    See `Rates.get_rates_from` for more details.
    """
    rates = Rates(symbol, timeframe, **kwargs)
    data = rates.get_rates_from(
        date_from,
        count,
        filter=filter,
        fill_na=fill_na,
        lower_colnames=lower_colnames,
        utc=utc,
    )
    return data

risk

RiskManagement

RiskManagement(symbol: str, max_risk: float = 10.0, daily_risk: float | None = None, max_trades: int | None = None, std_stop: bool = False, pchange_sl: float | None = None, account_leverage: bool = True, time_frame: TimeFrame = 'D1', start_time: str = '1:00', finishing_time: str = '23:00', broker_tz: bool = False, sl: int | None = None, tp: int | None = None, be: int | None = None, rr: float = 3.0, **kwargs)

The RiskManagement class provides foundational risk management functionalities for trading activities. It calculates risk levels, determines stop loss and take profit levels, and ensures trading activities align with predefined risk parameters.

Exemple

risk_manager = RiskManagement( ... symbol="EURUSD", ... max_risk=5.0, ... daily_risk=2.0, ... max_trades=10, ... std_stop=True, ... act_leverage=True, ... start_time="09:00", ... finishing_time="17:00", ... time_frame="1h" ... )

Calculate risk level

risk_level = risk_manager.risk_level()

Get appropriate lot size for a trade

lot_size = risk_manager.get_lot()

Determine stop loss and take profit levels

stop_loss = risk_manager.get_stop_loss() take_profit = risk_manager.get_take_profit()

Check if current risk is acceptable

is_risk_acceptable = risk_manager.is_risk_ok()

Initialize the RiskManagement class to manage risk in trading activities.

Parameters:

Name Type Description Default
symbol str

The symbol of the financial instrument to trade.

required
max_risk float

The maximum risk allowed on the trading account.

10.0
daily_risk float

Daily Max risk allowed. If Set to None it will be determine based on Maximum risk. The day is based on the start and the ending time

None
max_trades int

Maximum number of trades at any point in time. If set to None it will be determine based on the timeframe of trading.

None
std_stop bool

If set to True, the Stop loss is calculated based On historical volatility of the trading instrument. Defaults to False.

False
pchange_sl float

If set, the Stop loss is calculated based On percentage change of the trading instrument.

None
act_leverage bool

If set to True the account leverage will be used In risk management setting. Defaults to False.

required
time_frame str

The time frame on which the program is working (1m, 3m, 5m, 10m, 15m, 30m, 1h, 2h, 4h, D1). Defaults to 'D1'.

'D1'
start_time str

The starting time for the trading session (HH:MM, H and M do not star with 0). Defaults to "1:00".

'1:00'
finishing_time str

The finishing time for the trading strategy (HH:MM, H and M do not star with 0). Defaults to "23:00".

'23:00'
sl int

Stop Loss in points, Must be a positive number.

None
tp int

Take Profit in points, Must be a positive number.

None
be int

Break Even in points, Must be a positive number.

None
rr float

Risk reward ratio, Must be a positive number. Defaults to 1.5.

3.0
Source code in src/bbstrader/metatrader/risk.py
def __init__(
    self,
    symbol: str,
    max_risk: float = 10.0,
    daily_risk: float | None = None,
    max_trades: int | None = None,
    std_stop: bool = False,
    pchange_sl: float | None = None,
    account_leverage: bool = True,
    time_frame: TimeFrame = "D1",
    start_time: str = "1:00",
    finishing_time: str = "23:00",
    broker_tz: bool = False,
    sl: int | None = None,
    tp: int | None = None,
    be: int | None = None,
    rr: float = 3.0,
    **kwargs,
):
    """
    Initialize the RiskManagement class to manage risk in trading activities.

    Args:
        symbol (str): The symbol of the financial instrument to trade.
        max_risk (float): The `maximum risk allowed` on the trading account.
        daily_risk (float, optional): `Daily Max risk allowed`.
            If Set to None it will be determine based on Maximum risk.
            The day is based on the start and the ending time
        max_trades (int, optional): Maximum number of trades at any point in time.
            If set to None it will be determine based on the timeframe of trading.
        std_stop (bool, optional): If set to True, the Stop loss is calculated based
            On `historical volatility` of the trading instrument. Defaults to False.
        pchange_sl (float, optional): If set, the Stop loss is calculated based
            On `percentage change` of the trading instrument.
        act_leverage (bool, optional): If set to True the account leverage will be used
            In risk management setting. Defaults to False.
        time_frame (str, optional): The time frame on which the program is working
            `(1m, 3m, 5m, 10m, 15m, 30m, 1h, 2h, 4h, D1)`. Defaults to 'D1'.
        start_time (str, optional): The starting time for the trading session
            `(HH:MM, H and M do not star with 0)`. Defaults to "1:00".
        finishing_time (str, optional): The finishing time for the trading strategy
            `(HH:MM, H and M do not star with 0)`. Defaults to "23:00".
        sl (int, optional): Stop Loss in points, Must be a positive number.
        tp (int, optional): Take Profit in points, Must be a positive number.
        be (int, optional): Break Even in points, Must be a positive number.
        rr (float, optional): Risk reward ratio, Must be a positive number. Defaults to 1.5.
    """

    assert max_risk > 0
    assert daily_risk > 0 if daily_risk is not None else ...
    daily_risk = round(daily_risk, 5) if daily_risk is not None else None
    assert all(isinstance(v, int) and v > 0 for v in [sl, tp] if v is not None)
    assert isinstance(be, (int, float)) and be > 0 if be else ...
    assert time_frame in TIMEFRAMES

    self.kwargs = kwargs
    self.symbol = symbol
    self.timeframe = time_frame
    self.start_time = start_time
    self.finishing_time = finishing_time
    self.max_trades = max_trades
    self.std_stop = std_stop
    self.pchange = pchange_sl
    self.act_leverage = account_leverage
    self.daily_dd = daily_risk
    self.max_risk = max_risk
    self.broker_tz = broker_tz
    self.rr = rr
    self.sl = sl
    self.tp = tp
    self.be = be

    self.account = Account(**kwargs)
    self.symbol_info = client.symbol_info(self.symbol)
get_minutes
get_minutes() -> int

calculates the number of minutes between the starting of the session and the end of the session

Source code in src/bbstrader/metatrader/risk.py
def get_minutes(self) -> int:
    """calculates the number of minutes between
    the starting of the session and the end of the session"""

    fmt = "%H:%M"
    start = datetime.strptime(self.start_time, fmt)
    end = datetime.strptime(self.finishing_time, fmt)
    if self.broker_tz:
        start = self.account.broker.get_broker_time(self.start_time, fmt)
        end = self.account.broker.get_broker_time(self.finishing_time, fmt)
    diff = (end - start).total_seconds()
    diff += 86400 if diff < 0 else diff
    return int(diff // 60)
get_hours
get_hours() -> int

Calculates the number of hours between the starting of the session and the end of the session

Source code in src/bbstrader/metatrader/risk.py
def get_hours(self) -> int:
    """Calculates the number of hours between
    the starting of the session and the end of the session"""
    return self.get_minutes() // 60
risk_level
risk_level(balance_value=False) -> float | tuple[float, float]

Calculates the risk level of a trade

Returns: - Risk level in the form of a float percentage.

Source code in src/bbstrader/metatrader/risk.py
def risk_level(self, balance_value=False) -> float | tuple[float, float]:
    """
    Calculates the risk level of a trade

    Returns:
    -   Risk level in the form of a float percentage.
    """
    account_info = self.account.get_account_info()
    balance = account_info.balance
    equity = account_info.equity
    if equity == 0:
        return 0.0
    trades_history = self.account.get_trades_history()

    realized_profit = None
    if trades_history is None or len(trades_history) == 1:
        realized_profit = 0
    else:
        profit_df = trades_history.iloc[1:]
        profit = profit_df["profit"].sum()
        commisions = trades_history["commission"].sum()
        fees = trades_history["fee"].sum()
        swap = trades_history["swap"].sum()
        realized_profit = commisions + fees + swap + profit

    initial_balance = balance - realized_profit
    dd_percent = ((equity - initial_balance) / equity) * 100
    dd_percent = round(abs(dd_percent) if dd_percent < 0 else 0.0, 2)
    if balance_value:
        return (initial_balance, equity)
    return dd_percent
max_trade
max_trade() -> int

calculates the maximum number of trades allowed

Source code in src/bbstrader/metatrader/risk.py
def max_trade(self) -> int:
    """calculates the maximum number of trades allowed"""
    minutes = self.get_minutes()
    tf_int = self._convert_time_frame(self.timeframe)
    max_trades = self.max_trades or round(minutes / tf_int)
    return max(max_trades, 1)
get_std_stop
get_std_stop() -> int

Calculate the standard deviation-based stop loss level for a given financial instrument.

Returns: - Standard deviation-based stop loss level, rounded to the nearest point. - 0 if the calculated stop loss is less than or equal to 0.

Source code in src/bbstrader/metatrader/risk.py
def get_std_stop(self) -> int:
    """
    Calculate the standard deviation-based stop loss level
    for a given financial instrument.

    Returns:
    -   Standard deviation-based stop loss level, rounded to the nearest point.
    -   0 if the calculated stop loss is less than or equal to 0.
    """
    std = np.std(self._get_returns())
    return self._get_stop(std)
get_pchange_stop
get_pchange_stop(pchange: float | None) -> int

Calculate the percentage change-based stop loss level for a given financial instrument.

Parameters:

Name Type Description Default
pchange float

Percentage change in price to use for calculating stop loss level. If pchange is set to None, the stop loss is calculate using std.

required

Returns: - Percentage change-based stop loss level, rounded to the nearest point. - 0 if the calculated stop loss is <= 0.

Source code in src/bbstrader/metatrader/risk.py
def get_pchange_stop(self, pchange: float | None) -> int:
    """
    Calculate the percentage change-based stop loss level
    for a given financial instrument.

    Args:
        pchange (float): Percentage change in price to use for calculating stop loss level.
            If pchange is set to None, the stop loss is calculate using std.

    Returns:
    -   Percentage change-based stop loss level, rounded to the nearest point.
    -   0 if the calculated stop loss is <= 0.
    """
    if pchange is not None:
        return self._get_stop(pchange)
    else:
        # Use std as default pchange
        return self.get_std_stop()
calculate_var
calculate_var(tf: TimeFrame = 'D1', c=0.95) -> float

Calculate Value at Risk (VaR) for a given portfolio.

Parameters:

Name Type Description Default
tf str

Time frame to use to calculate volatility.

'D1'
c float

Confidence level for VaR calculation (default is 95%).

0.95

Returns: - VaR value

Source code in src/bbstrader/metatrader/risk.py
def calculate_var(self, tf: TimeFrame = "D1", c=0.95) -> float:
    """
    Calculate Value at Risk (VaR) for a given portfolio.

    Args:
        tf (str): Time frame to use to calculate volatility.
        c (float): Confidence level for VaR calculation (default is 95%).

    Returns:
    -   VaR value
    """
    returns = self._get_returns()
    P = self.account.get_account_info().margin_free
    mu = returns.mean()
    sigma = returns.std()
    alpha = norm.ppf(1 - c, mu, sigma)
    return P - P * (alpha + 1)
get_trade_risk
get_trade_risk() -> float

Calculate risk per trade as percentage

Source code in src/bbstrader/metatrader/risk.py
def get_trade_risk(self) -> float:
    """Calculate risk per trade as percentage"""
    total_risk = self.risk_level()
    max_trades = self.max_trade()
    if total_risk < self.max_risk:
        if self.daily_dd is not None:
            trade_risk = self.daily_dd / max_trades
        else:
            trade_risk = (self.max_risk - total_risk) / max_trades
        return trade_risk
    else:
        return 0
var_loss_value
var_loss_value() -> float

Calculate the stop-loss level based on VaR.

Notes

The Var is Estimated using the Variance-Covariance method on the daily returns. If you want to use the VaR for a different time frame .

Source code in src/bbstrader/metatrader/risk.py
def var_loss_value(self) -> float:
    """
    Calculate the stop-loss level based on VaR.

    Notes:
        The Var is Estimated using the Variance-Covariance method on the daily returns.
        If you want to use the VaR for a different time frame .
    """
    P = self.account.get_account_info().margin_free
    trade_risk = self.get_trade_risk()
    loss_allowed = P * trade_risk / 100
    var = self.calculate_var()
    return min(var, loss_allowed)
get_take_profit
get_take_profit() -> int

calculates the take profit of a trade in points

Source code in src/bbstrader/metatrader/risk.py
def get_take_profit(self) -> int:
    """calculates the take profit of a trade in points"""
    deviation = self.get_deviation()
    if self.tp is not None:
        return self.tp + deviation
    else:
        return round(self.get_stop_loss() * self.rr)
get_currency_risk
get_currency_risk() -> float

calculates the currency risk of a trade

Source code in src/bbstrader/metatrader/risk.py
def get_currency_risk(self) -> float:
    """calculates the currency risk of a trade"""
    return round(self.currency_risk()["currency_risk"], 2)
expected_profit
expected_profit()

Calculate the expected profit per trade

Source code in src/bbstrader/metatrader/risk.py
def expected_profit(self):
    """Calculate the expected profit per trade"""
    risk = self.get_currency_risk()
    return round(risk * self.rr, 2)
volume
volume()

Volume per trade

Source code in src/bbstrader/metatrader/risk.py
def volume(self):
    """Volume per trade"""

    return self.currency_risk()["volume"]
currency_risk
currency_risk() -> dict[str, int | float | Any]

Calculates the currency risk of a trade.

Returns:

Type Description
dict[str, int | float | Any]

Dict[str, Union[int, float, Any]]: A dictionary containing the following keys:

dict[str, int | float | Any]
  • 'currency_risk': Dollar amount risk on a single trade.
dict[str, int | float | Any]
  • 'trade_loss': Loss value per tick in dollars.
dict[str, int | float | Any]
  • 'trade_profit': Profit value per tick in dollars.
dict[str, int | float | Any]
  • 'volume': Contract size multiplied by the average price.
dict[str, int | float | Any]
  • 'lot': Lot size per trade.
Source code in src/bbstrader/metatrader/risk.py
def currency_risk(self) -> dict[str, int | float | Any]:
    """
    Calculates the currency risk of a trade.

    Returns:
        Dict[str, Union[int, float, Any]]: A dictionary containing the following keys:

        - `'currency_risk'`: Dollar amount risk on a single trade.
        - `'trade_loss'`: Loss value per tick in dollars.
        - `'trade_profit'`: Profit value per tick in dollars.
        - `'volume'`: Contract size multiplied by the average price.
        - `'lot'`: Lot size per trade.
    """
    s_info = self.account.get_symbol_info(self.symbol)
    leverage = self.account.broker.get_leverage_for_symbol(
        self.symbol, self.act_leverage
    )
    contract_size = s_info.trade_contract_size
    av_price = (s_info.bid + s_info.ask) / 2
    trade_risk = self.get_trade_risk()
    symbol_type = self.account.get_symbol_type(self.symbol)

    tick_value_loss, tick_value_profit = self.account.broker.adjust_tick_values(
        self.symbol,
        s_info.trade_tick_value_loss,
        s_info.trade_tick_value_profit,
        contract_size,
    )
    tick_value = s_info.trade_tick_value  # For checks

    if tick_value == 0 or tick_value_loss == 0 or tick_value_profit == 0:
        logger.error(
            f"The Tick Values for {self.symbol} is 0.0. Check broker conditions for {self.symbol}."
        )
        return {
            "currency_risk": 0.0,
            "trade_loss": 0.0,
            "trade_profit": 0.0,
            "volume": 0,
            "lot": 0.01,
        }

    if trade_risk > 0:
        currency_risk = round(self.var_loss_value(), 5)
        volume = round(currency_risk * leverage)
        lot = (
            round(volume / (contract_size * av_price), 2)
            if contract_size * av_price != 0
            else 0.0
        )
        lot = self.account.broker.validate_lot_size(self.symbol, lot)

        if symbol_type == SymbolType.COMMODITIES and contract_size > 1:
            lot = (
                volume / (av_price * contract_size)
                if av_price * contract_size != 0
                else 0.0
            )
            lot = self.account.broker.validate_lot_size(self.symbol, lot)
        if symbol_type == SymbolType.FOREX:
            lot = round(volume / contract_size, 2) if contract_size != 0 else 0.0
            lot = self.account.broker.validate_lot_size(self.symbol, lot)

        if self.sl is not None:
            trade_loss = currency_risk / self.sl if self.sl != 0 else 0.0
            trade_profit = (
                (currency_risk * (self.tp // self.sl if self.tp else self.rr))
                / (self.tp or (self.sl * self.rr))
                if self.sl != 0
                else 0.0
            )
            lot = (
                round(trade_loss / (contract_size * tick_value_loss), 2)
                if contract_size * tick_value_loss != 0
                else 0.0
            )
            lot = self.account.broker.validate_lot_size(self.symbol, lot)
            volume = round(lot * contract_size * av_price)

            if (
                symbol_type in [SymbolType.COMMODITIES, SymbolType.CRYPTO]
            ) and contract_size > 1:
                lot = (
                    currency_risk / (self.sl * tick_value_loss * contract_size)
                    if self.sl * tick_value_loss * contract_size != 0
                    else 0.0
                )
                lot = self.account.broker.validate_lot_size(self.symbol, lot)
                trade_loss = lot * contract_size * tick_value_loss

            if symbol_type == SymbolType.FOREX:
                volume = (
                    round(trade_loss * contract_size / tick_value_loss)
                    if tick_value_loss != 0
                    else 0
                )
                lot = (
                    round(volume / contract_size, 2) if contract_size != 0 else 0.0
                )
                lot = self.account.broker.validate_lot_size(self.symbol, lot)

        elif self.std_stop and self.pchange is None and self.sl is None:
            sl = self.get_std_stop()
            trade_loss, trade_profit, lot, volume = self._std_pchange_stop(
                currency_risk, sl, contract_size, tick_value_loss
            )

        elif self.pchange is not None and not self.std_stop and self.sl is None:
            sl = self.get_pchange_stop(self.pchange)
            trade_loss, trade_profit, lot, volume = self._std_pchange_stop(
                currency_risk, sl, contract_size, tick_value_loss
            )

        else:
            if symbol_type == SymbolType.FOREX:
                trade_loss = (
                    tick_value_loss * (volume / contract_size)
                    if contract_size != 0
                    else 0.0
                )
                trade_profit = (
                    tick_value_profit * (volume / contract_size)
                    if contract_size != 0
                    else 0.0
                )
            else:
                trade_loss = (lot * contract_size) * tick_value_loss
                trade_profit = (lot * contract_size) * tick_value_profit

        # Apply currency conversion
        rates = self.account.get_currency_rates(self.symbol)
        factor = self.account.broker.get_currency_conversion_factor(
            self.symbol, rates.get("pc", ""), self.account.currency
        )
        trade_profit *= factor
        trade_loss *= factor
        currency_risk *= factor

        return {
            "currency_risk": currency_risk,
            "trade_loss": trade_loss,
            "trade_profit": trade_profit,
            "volume": round(volume),
            "lot": lot,
        }
    else:
        return {
            "currency_risk": 0.0,
            "trade_loss": 0.0,
            "trade_profit": 0.0,
            "volume": 0,
            "lot": 0.01,
        }
get_break_even
get_break_even(thresholds: list[tuple[int, float]] = None) -> int

Calculates the break-even price level based on stop-loss tiers.

The function determines the break-even point by applying a multiplier to the sum of the current stop-loss and market spread. If an explicit break-even value (self.be) is already set, it returns that value (converting percentage-based floats to absolute points if necessary).

Parameters:

Name Type Description Default
thresholds list[tuple[int, float]]

A list of tiers defined as (threshold_limit, multiplier). Example: [(150, 0.25), (100, 0.35), (0, 0.5)]. If None, defaults to standard conservative tiers.

None

Returns:

Name Type Description
int int

The calculated break-even value in points/pips.

Note

The function automatically sorts thresholds in descending order to ensure the 'stop' value is matched against the highest possible tier first.

Source code in src/bbstrader/metatrader/risk.py
def get_break_even(self, thresholds: list[tuple[int, float]] = None) -> int:
    """
    Calculates the break-even price level based on stop-loss tiers.

    The function determines the break-even point by applying a multiplier to the
    sum of the current stop-loss and market spread. If an explicit break-even
    value (`self.be`) is already set, it returns that value (converting
    percentage-based floats to absolute points if necessary).

    Args:
        thresholds (list[tuple[int, float]], optional): A list of tiers defined
            as (threshold_limit, multiplier).
            Example: [(150, 0.25), (100, 0.35), (0, 0.5)].
            If None, defaults to standard conservative tiers.

    Returns:
        int: The calculated break-even value in points/pips.

    Note:
        The function automatically sorts thresholds in descending order to
        ensure the 'stop' value is matched against the highest possible tier first.
    """
    if self.be is not None:
        return (
            self.be if isinstance(self.be, int) else self.get_pchange_stop(self.be)
        )

    if thresholds is None:
        thresholds = [(150, 0.25), (100, 0.35), (0, 0.50)]

    stop = self.get_stop_loss()
    spread = client.symbol_info(self.symbol).spread
    sorted_thresholds = sorted(thresholds, key=lambda x: x[0], reverse=True)

    for limit, multiplier in sorted_thresholds:
        if stop > limit:
            return round((stop + spread) * multiplier)
    return 0

trade

Trade

Trade(symbol: str = 'EURUSD', expert_name: str = 'bbstrader', expert_id: int = EXPERT_ID, version: str = '3.0', target: float = 5.0, start_time: str = '1:00', finishing_time: str = '23:00', ending_time: str = '23:30', time_frame: str = 'D1', broker_tz=False, verbose: bool = False, console_log: bool = False, logger: Logger | str = 'bbstrader.log', **kwargs)

Extends the RiskManagement class to include specific trading operations, incorporating risk management strategies directly into trade executions. It offers functionalities to execute trades while managing risks.

Exemple

import time

Initialize the Trade class with parameters

trade = Trade( ... symbol="EURUSD", # Symbol to trade ... expert_name="bbstrader", # Name of the expert advisor ... expert_id=12345, # Unique ID for the expert advisor ... version="1.0", # Version of the expert advisor ... target=5.0, # Daily profit target in percentage ... start_time="09:00", # Start time for trading ... finishing_time="17:00", # Time to stop opening new positions ... ending_time="17:30", # Time to close any open positions ... max_risk=2.0, # Maximum risk allowed on the account in percentage ... daily_risk=1.0, # Daily risk allowed in percentage ... max_trades=5, # Maximum number of trades per session ... rr=2.0, # Risk-reward ratio ... account_leverage=True, # Use account leverage in calculations ... std_stop=True, # Use standard deviation for stop loss calculation ... sl=20, # Stop loss in points (optional) ... tp=30, # Take profit in points (optional) ... be=10 # Break-even in points (optional) ... )

Example to open a buy position

trade.open_buy_position(mm=True, comment="Opening Buy Position")

Example to open a sell position

trade.open_sell_position(mm=True, comment="Opening Sell Position")

Check current open positions

opened_positions = trade.get_opened_positions if opened_positions is not None: ... print(f"Current open positions: {opened_positions}")

Close all open positions at the end of the trading session

if trade.days_end(): ... trade.close_all_positions(comment="Closing all positions at day's end")

Print trading session statistics

trade.statistics(save=True, dir="my_trading_stats")

Sleep until the next trading session if needed (example usage)

sleep_time = trade.sleep_time() print(f"Sleeping for {sleep_time} minutes until the next trading session.") time.sleep(sleep_time * 60)

Initializes the Trade class with the specified parameters.

Parameters:

Name Type Description Default
symbol str

The symbol that the expert advisor will trade.

'EURUSD'
expert_name str

The name of the expert advisor.

'bbstrader'
expert_id int

The unique ID used to identify the expert advisor or the strategy used on the symbol.

EXPERT_ID
version str

The version of the expert advisor.

'3.0'
target float

Trading period (day, week, month) profit target in percentage.

5.0
start_time str

Thehour and minutes that the expert advisor is able to start to run.

'1:00'
finishing_time str

The time after which no new position can be opened.

'23:00'
ending_time str

The time after which any open position will be closed.

'23:30'
verbose bool | None

If set to None (default), account summary and risk managment parameters are printed in the terminal.

False
console_log bool

If set to True, log messages are displayed in the console.

False
logger Logger | str

The logger object to use for logging messages could be a string or a logger object.

'bbstrader.log'
**kwargs

Params for the RiskManagement and Account See the bbstrader.metatrader.risk.RiskManagement class for more details on these parameters. See bbstrader.metatrader.broker.check_mt5_connection() for more details on how to connect to MT5 terminal.

{}
Source code in src/bbstrader/metatrader/trade.py
def __init__(
    self,
    symbol: str = "EURUSD",
    expert_name: str = "bbstrader",
    expert_id: int = EXPERT_ID,
    version: str = "3.0",
    target: float = 5.0,
    start_time: str = "1:00",
    finishing_time: str = "23:00",
    ending_time: str = "23:30",
    time_frame: str = "D1",
    broker_tz=False,
    verbose: bool = False,
    console_log: bool = False,
    logger: Logger | str = "bbstrader.log",
    **kwargs,
):
    """
    Initializes the Trade class with the specified parameters.

    Args:
        symbol (str): The `symbol` that the expert advisor will trade.
        expert_name (str): The name of the `expert advisor`.
        expert_id (int): The `unique ID` used to identify the expert advisor
            or the strategy used on the symbol.
        version (str): The `version` of the expert advisor.
        target (float): `Trading period (day, week, month) profit target` in percentage.
        start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
        finishing_time (str): The time after which no new position can be opened.
        ending_time (str): The time after which any open position will be closed.
        verbose (bool | None): If set to None (default), account summary and risk managment
            parameters are printed in the terminal.
        console_log (bool): If set to True, log messages are displayed in the console.
        logger (Logger | str): The logger object to use for logging messages could be a string or a logger object.
        **kwargs: Params for the RiskManagement and Account
            See the ``bbstrader.metatrader.risk.RiskManagement`` class for more details on these parameters.
            See `bbstrader.metatrader.broker.check_mt5_connection()` for more details on how to connect to MT5 terminal.
    """

    self.symbol = symbol
    self.expert_name = expert_name
    self.expert_id = expert_id
    self.version = version
    self.target = target
    self.verbose = verbose
    self.start = start_time
    self.end = ending_time
    self.finishing = finishing_time
    self.broker_tz = broker_tz
    self.console_log = console_log
    self.timeframe = time_frame
    self.kwargs = kwargs

    self.account = Account(**kwargs)
    self.rm = RiskManagement(
        symbol=symbol,
        start_time=start_time,
        finishing_time=finishing_time,
        time_frame=time_frame,
        broker_tz=broker_tz,
        **kwargs,
    )

    self.buy_positions = []
    self.sell_positions = []
    self.opened_positions = []
    self.opened_orders = []
    self.break_even_status = []
    self.break_even_points = {}
    self.trail_after_points = []
    self._retcodes = []

    self._get_logger(logger, console_log)
    self.initialize(**kwargs)
    self.select_symbol(**kwargs)
    self.prepare_symbol()

    if self.verbose:
        self.summary()
        print()
        self.risk_managment()
        print(f">>> Everything is OK, @{self.expert_name} is Running ...>>>\n")
retcodes property
retcodes: list[int]

Return all the retcodes

orders property
orders

Return all opened order's tickets

positions property
positions

Return all opened position's tickets

buypos property
buypos

Return all buy opened position's tickets

sellpos property
sellpos

Return all sell opened position's tickets

bepos property
bepos

Return All positon's tickets for which a break even has been set

initialize
initialize(**kwargs)

Initializes the MetaTrader 5 (MT5) terminal for trading operations. This method attempts to establish a connection with the MT5 terminal. If the initial connection attempt fails due to a timeout, it retries after a specified delay. Successful initialization is crucial for the execution of trading operations.

Raises:

Type Description
MT5TerminalError

If initialization fails.

Source code in src/bbstrader/metatrader/trade.py
def initialize(self, **kwargs):
    """
    Initializes the MetaTrader 5 (MT5) terminal for trading operations.
    This method attempts to establish a connection with the MT5 terminal.
    If the initial connection attempt fails due to a timeout, it retries after a specified delay.
    Successful initialization is crucial for the execution of trading operations.

    Raises:
        MT5TerminalError: If initialization fails.
    """
    try:
        if self.verbose:
            print("\nInitializing the basics.")
        check_mt5_connection(**kwargs)
        if self.verbose:
            print(
                f"You are running the @{self.expert_name} Expert advisor,"
                f" Version @{self.version}, on {self.symbol}."
            )
    except Exception as e:
        LOGGER.error(f"During initialization: {e}")
select_symbol
select_symbol(**kwargs)

Selects the trading symbol in the MetaTrader 5 (MT5) terminal. This method ensures that the specified trading symbol is selected and visible in the MT5 terminal, allowing subsequent trading operations such as opening and closing positions on this symbol.

Raises:

Type Description
MT5TerminalError

If symbole selection fails.

Source code in src/bbstrader/metatrader/trade.py
def select_symbol(self, **kwargs):
    """
    Selects the trading symbol in the MetaTrader 5 (MT5) terminal.
    This method ensures that the specified trading
    symbol is selected and visible in the MT5 terminal,
    allowing subsequent trading operations such as opening and
    closing positions on this symbol.

    Raises:
        MT5TerminalError: If symbole selection fails.
    """
    try:
        check_mt5_connection(**kwargs)
        if not client.symbol_select(self.symbol, True):
            raise_mt5_error(message=INIT_MSG)
    except Exception as e:
        LOGGER.error(f"Selecting symbol '{self.symbol}': {e}")
prepare_symbol
prepare_symbol()

Prepares the selected symbol for trading. This method checks if the symbol is available and visible in the MT5 terminal. If the symbol is not visible, it attempts to select the symbol again. This step ensures that trading operations can be performed on the selected symbol without issues.

Raises:

Type Description
MT5TerminalError

If the symbol cannot be made visible for trading operations.

Source code in src/bbstrader/metatrader/trade.py
def prepare_symbol(self):
    """
    Prepares the selected symbol for trading.
    This method checks if the symbol is available and visible in the
    MT5 terminal. If the symbol is not visible, it attempts to select the symbol again.
    This step ensures that trading operations can be performed on the selected symbol without issues.

    Raises:
        MT5TerminalError: If the symbol cannot be made visible for trading operations.
    """
    try:
        symbol_info = client.symbol_info(self.symbol)
        if symbol_info is None:
            raise_mt5_error(message=INIT_MSG)

        if not symbol_info.visible:
            raise_mt5_error(message=INIT_MSG)
        if self.verbose:
            print("Initialization successfully completed.")
    except Exception as e:
        LOGGER.error(f"Preparing symbol '{self.symbol}': {e}")
summary
summary()

Show a brief description about the trading program

Source code in src/bbstrader/metatrader/trade.py
def summary(self):
    """Show a brief description about the trading program"""
    fmt = "%H:%M"
    start = datetime.strptime(self.start, fmt).time()
    finish = datetime.strptime(self.finishing, fmt).time()
    end = datetime.strptime(self.end, fmt).time()
    if self.broker_tz:
        start = self.account.broker.get_broker_time(self.start, fmt).time()
        finish = self.account.broker.get_broker_time(self.finishing, fmt).time()
        end = self.account.broker.get_broker_time(self.end, fmt).time()
    summary_data = [
        ["Expert Advisor Name", f"@{self.expert_name}"],
        ["Expert Advisor Version", f"@{self.version}"],
        ["Expert | Strategy ID", self.expert_id],
        ["Trading Symbol", self.symbol],
        ["Trading Time Frame", self.timeframe],
        ["Start Trading Time", f"{start}"],
        ["Finishing Trading Time", f"{finish}"],
        ["Closing Position After", f"{end}"],
    ]
    # Custom table format
    summary_table = tabulate(
        summary_data, headers=["Summary", "Values"], tablefmt="outline"
    )

    # Print the table
    print("\n[============ Trade Account Summary ==============]")
    print(summary_table)
risk_managment
risk_managment()

Show the risk management parameters

Source code in src/bbstrader/metatrader/trade.py
def risk_managment(self):
    """Show the risk management parameters"""

    loss = self.rm.currency_risk()["trade_loss"]
    trade_profit = self.rm.currency_risk()["trade_profit"]
    ok = "OK" if self.rm.is_risk_ok() else "Not OK"
    account_info = self.account.get_account_info()
    total_profit = round(self.get_stats()[1]["total_profit"], 2)
    currency = account_info.currency
    rates = self.account.get_currency_rates(self.symbol)

    account_data = [
        ["Account Name", account_info.name],
        ["Account Number", account_info.login],
        ["Account Server", account_info.server],
        ["Account Balance", f"{account_info.balance} {currency}"],
        ["Account Profit", f"{total_profit} {currency}"],
        ["Account Equity", f"{account_info.equity} {currency}"],
        ["Account Leverage", account_info.leverage],
        ["Account Margin", f"{round(account_info.margin, 2)} {currency}"],
        ["Account Free Margin", f"{account_info.margin_free} {currency}"],
        ["Maximum Drawdown", f"{self.rm.max_risk}%"],
        ["Risk Allowed", f"{round((self.rm.max_risk - self.rm.risk_level()), 2)}%"],
        ["Volume", f"{self.rm.volume()} {rates.get('pc')}"],
        ["Risk Per trade", f"{-self.rm.get_currency_risk()} {currency}"],
        ["Profit Expected Per trade", f"{self.rm.expected_profit()} {currency}"],
        ["Lot Size", f"{self.rm.get_lot()} Lots"],
        ["Stop Loss", f"{self.rm.get_stop_loss()} Points"],
        ["Loss Value Per Tick", f"{round(loss, 5)} {currency}"],
        ["Take Profit", f"{self.rm.get_take_profit()} Points"],
        ["Profit Value Per Tick", f"{round(trade_profit, 5)} {currency}"],
        ["Break Even", f"{self.rm.get_break_even()} Points"],
        ["Deviation", f"{self.rm.get_deviation()} Points"],
        ["Trading Time Interval", f"{self.rm.get_minutes()} Minutes"],
        ["Risk Level", ok],
        ["Maximum Trades", self.rm.max_trade()],
    ]
    # Custom table format
    print("\n[======= Account Risk Management Overview =======]")
    table = tabulate(
        account_data, headers=["Risk Metrics", "Values"], tablefmt="outline"
    )

    # Print the table
    print(table)
statistics
statistics(save=True, dir=None)

Print some statistics for the trading session and save to CSV if specified.

Parameters:

Name Type Description Default
save bool

Whether to save the statistics to a CSV file.

True
dir str

The directory to save the CSV file.

None
Source code in src/bbstrader/metatrader/trade.py
def statistics(self, save=True, dir=None):
    """
    Print some statistics for the trading session and save to CSV if specified.

    Args:
        save (bool, optional): Whether to save the statistics to a CSV file.
        dir (str, optional): The directory to save the CSV file.
    """
    stats, additional_stats = self.get_stats()

    profit = round(stats["profit"], 2)
    win_rate = stats["win_rate"]
    total_fees = round(stats["total_fees"], 3)
    average_fee = round(stats["average_fee"], 3)
    currency = self.account.info.currency
    net_profit = round((profit + total_fees), 2)
    trade_risk = round(self.rm.get_currency_risk() * -1, 2)

    # Formatting the statistics output
    session_data = [
        ["Total Trades", stats["deals"]],
        ["Winning Trades", stats["win_trades"]],
        ["Losing Trades", stats["loss_trades"]],
        ["Session Profit", f"{profit} {currency}"],
        ["Total Fees", f"{total_fees} {currency}"],
        ["Average Fees", f"{average_fee} {currency}"],
        ["Net Profit", f"{net_profit} {currency}"],
        ["Risk per Trade", f"{trade_risk} {currency}"],
        ["Expected Profit per Trade", f"{self.rm.expected_profit()} {currency}"],
        ["Risk Reward Ratio", self.rm.rr],
        ["Win Rate", f"{win_rate}%"],
        ["Sharpe Ratio", self.sharpe()],
        ["Trade Profitability", additional_stats["profitability"]],
    ]
    session_table = tabulate(
        session_data, headers=["Statistics", "Values"], tablefmt="outline"
    )

    if self.verbose:
        print("\n[========== Trading Session Statistics ===========]")
        print(session_table)

    if save and stats["deals"] > 0:
        today_date = datetime.now().strftime("%Y%m%d%H%M%S")
        statistics_dict = {item[0]: item[1] for item in session_data}
        stats_df = pd.DataFrame(statistics_dict, index=[0])

        dir = dir or ".sessions"
        os.makedirs(dir, exist_ok=True)
        symbol = self.symbol.split(".")[0] if "." in self.symbol else self.symbol

        filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
        filepath = os.path.join(dir, filename)
        stats_df.to_csv(filepath, index=False)
        LOGGER.info(f"Session statistics saved to {filepath}")
open_position
open_position(action: Buys | Sells, price: float | None = None, stoplimit: float | None = None, id: int | None = None, mm: bool = True, trail: bool = True, comment: str | None = None, symbol: str | None = None, volume: float | None = None, sl: float | None = None, tp: float | None = None) -> bool

Opens a Buy or Sell position (Market or Pending).

Parameters:

Name Type Description Default
action str

('BMKT', 'SMKT') for Market orders or ('BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT') for pending orders

required
price float

The price at which to open an order

None
stoplimit float

A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory). The pending order is not passed to the trading system until that moment

None
id int

The strategy id or expert Id

None
mm bool

Weither to put stop loss and tp or not

True
trail bool

Weither to trail the stop loss or not

True
comment str

The comment for the closing position

None
symbol str

The symbol to trade

None
volume float

The volume (lot) to trade

None
sl float

The stop loss price

None
tp float

The take profit price

None
Source code in src/bbstrader/metatrader/trade.py
def open_position(
    self,
    action: Buys | Sells,
    price: float | None = None,
    stoplimit: float | None = None,
    id: int | None = None,
    mm: bool = True,
    trail: bool = True,
    comment: str | None = None,
    symbol: str | None = None,
    volume: float | None = None,
    sl: float | None = None,
    tp: float | None = None,
) -> bool:
    """Opens a Buy or Sell position (Market or Pending).

    Args:
        action (str): (`'BMKT'`, `'SMKT'`) for Market orders
            or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
        price (float): The price at which to open an order
        stoplimit (float): A price a pending Limit order is set at
            when the price reaches the 'price' value (this condition is mandatory).
            The pending order is not passed to the trading system until that moment
        id (int): The strategy id or expert Id
        mm (bool): Weither to put stop loss and tp or not
        trail (bool): Weither to trail the stop loss or not
        comment (str): The comment for the closing position
        symbol (str): The symbol to trade
        volume (float): The volume (lot) to trade
        sl (float): The stop loss price
        tp (float): The take profit price
    """
    is_buy = action.startswith("B")
    symbol = symbol or self.symbol
    expert_id = id if id is not None else self.expert_id
    point = client.symbol_info(symbol).point
    tick = client.symbol_info_tick(symbol)

    req_price = None
    if "MKT" in action:
        req_price = tick.bid if is_buy else tick.ask
    else:
        if price is None:
            raise ValueError(f"Price is required for pending order: {action}")
        req_price = price

    mm_price = req_price
    if "TPLMT" in action:
        if stoplimit is None:
            raise ValueError(f"StopLimit price required for {action}")
        if (is_buy and stoplimit > req_price) or (
            not is_buy and stoplimit < req_price
        ):
            raise ValueError("Invalid StopLimit relationship to Price.")
        mm_price = stoplimit

    order_type, _ = self._order_type()[action]
    trade_action = (
        Mt5.TRADE_ACTION_DEAL if "MKT" in action else Mt5.TRADE_ACTION_PENDING
    )
    request = {
        "action": trade_action,
        "symbol": symbol,
        "volume": float(volume or self.rm.get_lot()),
        "type": order_type,
        "price": req_price,
        "deviation": self.rm.get_deviation(),
        "magic": expert_id,
        "comment": comment or f"@{self.expert_name}",
        "type_time": Mt5.ORDER_TIME_GTC,
        "type_filling": Mt5.ORDER_FILLING_FOK,
    }

    if "TPLMT" in action:
        request["stoplimit"] = stoplimit

    if mm:
        direction = 1 if is_buy else -1
        request["sl"] = sl or (
            mm_price - (direction * self.rm.get_stop_loss() * point)
        )
        request["tp"] = tp or (
            mm_price + (direction * self.rm.get_take_profit() * point)
        )

    self.break_even(mm=mm, id=expert_id, trail=trail)

    if self.check(comment):
        final_price = stoplimit if "TPLMT" in action else req_price
        return self.request_result(final_price, request, action)

    return False
open_buy_position
open_buy_position(**kwargs)

Open a buy position or order.

See Trade.open_position for the kwargs parameters.

Source code in src/bbstrader/metatrader/trade.py
def open_buy_position(self, **kwargs):
    """
    Open a buy position or order.

    See Trade.open_position for the ``kwargs`` parameters.
    """
    return self.open_position(action=kwargs.pop("action", "BMKT"), **kwargs)
open_sell_position
open_sell_position(**kwargs)

Open a sell position or order.

See Trade.open_position for the kwargs parameters.

Source code in src/bbstrader/metatrader/trade.py
def open_sell_position(self, **kwargs):
    """
    Open a sell position or order.

    See Trade.open_position for the ``kwargs`` parameters.
    """
    return self.open_position(action=kwargs.pop("action", "SMKT"), **kwargs)
check
check(comment)

Verify if all conditions for taking a position are valide, These conditions are based on the Maximum risk ,daily risk, the starting, the finishing, and ending trading time.

Parameters:

Name Type Description Default
comment str

The comment for the closing position

required
Source code in src/bbstrader/metatrader/trade.py
def check(self, comment):
    """
    Verify if all conditions for taking a position are valide,
    These conditions are based on the Maximum risk ,daily risk,
    the starting, the finishing, and ending trading time.

    Args:
        comment (str): The comment for the closing position
    """

    def _check(txt: str = ""):
        if (
            self.positive_profit(id=self.expert_id)
            or self.get_current_positions() is None
        ):
            self.close_positions(position_type="all")
            LOGGER.info(txt)
            self.statistics(save=True)

    if self.days_end():
        LOGGER.warning(f"End of the trading Day, SYMBOL={self.symbol}")
        return False
    elif not self.trading_time():
        LOGGER.warning(f"Not Trading time, SYMBOL={self.symbol}")
        return False
    elif not self.rm.is_risk_ok():
        LOGGER.warning(f"Account Risk not allowed, SYMBOL={self.symbol}")
        _check(comment)
        return False
    elif self.is_max_trades_reached():
        LOGGER.warning(f"Maximum trades reached for Today, SYMBOL={self.symbol}")
        return False
    elif self.profit_target():
        _check(f"Profit target Reached !!! SYMBOL={self.symbol}")
    return True
request_result
request_result(price: float, request: dict[str, Any], type: Buys | Sells)

Check if a trading order has been sent correctly

Parameters:

Name Type Description Default
price float

Price for opening the position

required
request Dict[str, Any]

A trade request to sent to Mt5.order_sent()

required
all detail in request can be found here https

//www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py

required
type str

The type of the order (BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)

required
Source code in src/bbstrader/metatrader/trade.py
def request_result(self, price: float, request: dict[str, Any], type: Buys | Sells):
    """
    Check if a trading order has been sent correctly

    Args:
        price (float): Price for opening the position
        request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
        all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py

        type (str): The type of the order `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
    """
    # Send a trading request
    # Check the execution result
    pos = self._order_type()[type][1]
    addtionnal = f", SYMBOL={self.symbol}"
    result = None
    try:
        client.order_check(request)
        result = client.order_send(request)
    except Exception as e:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
        return False
    if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
        if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL:  # 10030
            for fill in FILLING_TYPE:
                request["type_filling"] = fill
                result = client.order_send(request)
                if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
                    break
        elif result and result.retcode == Mt5.TRADE_RETCODE_INVALID_VOLUME:  # 10014
            new_volume = int(request["volume"])
            if new_volume >= 1:
                request["volume"] = new_volume
                result = client.order_send(request)
        elif result and result.retcode not in self._retcodes:
            self._retcodes.append(result.retcode)
            msg = trade_retcode_message(result.retcode) if result else "N/A"
            retcode = result.retcode if result else None
            LOGGER.error(
                f"Trade Order Request, RETCODE={retcode}: {msg}{addtionnal}"
            )
        elif result and result.retcode in [
            Mt5.TRADE_RETCODE_CONNECTION,
            Mt5.TRADE_RETCODE_TIMEOUT,
        ]:
            tries = 0
            while result and result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
                try:
                    client.order_check(request)
                    result = client.order_send(request)
                except Exception as e:
                    msg = trade_retcode_message(result.retcode) if result else "N/A"
                    LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
                    return False
                if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
                    break
                tries += 1
    # Print the result
    if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
        msg = trade_retcode_message(result.retcode)
        LOGGER.info(f"Trade Order {msg}{addtionnal}")
        if type != "BMKT" and type != "SMKT":
            self.opened_orders.append(result.order)
        long_msg = (
            f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{round(price, 5)}, "
            f"Lot(s): {result.volume}, Sl: {self.rm.get_stop_loss()}, "
            f"Tp: {self.rm.get_take_profit()}"
        )
        LOGGER.info(long_msg)
        if type == "BMKT" or type == "SMKT":
            self.opened_positions.append(result.order)
            positions = self.account.get_positions(symbol=self.symbol)
            if positions is not None:
                for position in positions:
                    if position.ticket == result.order:
                        if position.type == 0:
                            order_type = "BUY"
                            self.buy_positions.append(position.ticket)
                        else:
                            order_type = "SELL"
                            self.sell_positions.append(position.ticket)
                        profit = round(client.account_info().profit, 5)
                        order_info = (
                            f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open, 5)}, "
                            f"Sl: @{round(position.sl, 5)} Tp: @{round(position.tp, 5)}"
                        )
                        LOGGER.info(order_info)
                        pos_info = (
                            f"3. [OPEN POSITIONS ON {self.symbol} = {len(positions)}, ACCOUNT OPEN PnL = {profit} "
                            f"{client.account_info().currency}]\n"
                        )
                        LOGGER.info(pos_info)
        return True
    else:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        retcode = result.retcode if result else None
        LOGGER.error(
            f"Unable to Open Position, RETCODE={retcode}: {msg}{addtionnal}"
        )
        return False
get_filtered_tickets
get_filtered_tickets(id: int | None = None, filter_type: str | None = None, th=None) -> list[int] | None

Get tickets for positions or orders based on filters.

Parameters:

Name Type Description Default
id int

The strategy id or expert Id

None
filter_type str

Filter type to apply on the tickets, - orders are current open orders - buy_stops are current buy stop orders - sell_stops are current sell stop orders - buy_limits are current buy limit orders - sell_limits are current sell limit orders - buy_stop_limits are current buy stop limit orders - sell_stop_limits are current sell stop limit orders - positions are all current open positions - buys and sells are current buy or sell open positions - profitables are current open position that have a profit greater than a threshold - losings are current open position that have a negative profit

None
th bool

the minimum treshold for winning position (only relevant when filter_type is 'profitables')

None

Returns:

Type Description
list[int] | None

List[int] | None: A list of filtered tickets or None if no tickets match the criteria.

Source code in src/bbstrader/metatrader/trade.py
def get_filtered_tickets(
    self, id: int | None = None, filter_type: str | None = None, th=None
) -> list[int] | None:
    """
    Get tickets for positions or orders based on filters.

    Args:
        id (int): The strategy id or expert Id
        filter_type (str): Filter type to apply on the tickets,
            - `orders` are current open orders
            - `buy_stops` are current buy stop orders
            - `sell_stops` are current sell stop orders
            - `buy_limits` are current buy limit orders
            - `sell_limits` are current sell limit orders
            - `buy_stop_limits` are current buy stop limit orders
            - `sell_stop_limits` are current sell stop limit orders
            - `positions` are all current open positions
            - `buys` and `sells` are current buy or sell open positions
            - `profitables` are current open position that have a profit greater than a threshold
            - `losings` are current open position that have a negative profit
        th (bool): the minimum treshold for winning position
            (only relevant when filter_type is 'profitables')

    Returns:
        List[int] | None: A list of filtered tickets
            or None if no tickets match the criteria.
    """
    Id = id if id is not None else self.expert_id
    POSITIONS = ["positions", "buys", "sells", "profitables", "losings"]

    if filter_type not in POSITIONS:
        items = self.account.get_orders(symbol=self.symbol)
    else:
        items = self.account.get_positions(symbol=self.symbol)

    filtered_tickets = []

    if items is None:
        return []
    for item in items:
        if item.magic == Id:
            if filter_type == "buys" and item.type != 0:
                continue
            if filter_type == "sells" and item.type != 1:
                continue
            if filter_type == "losings" and item.profit > 0:
                continue
            if filter_type == "profitables" and not self.win_trade(item, th=th):
                continue
            if (
                filter_type == "buy_stops"
                and item.type != self._order_type()["BSTP"][0]
            ):
                continue
            if (
                filter_type == "sell_stops"
                and item.type != self._order_type()["SSTP"][0]
            ):
                continue
            if (
                filter_type == "buy_limits"
                and item.type != self._order_type()["BLMT"][0]
            ):
                continue
            if (
                filter_type == "sell_limits"
                and item.type != self._order_type()["SLMT"][0]
            ):
                continue
            if (
                filter_type == "buy_stop_limits"
                and item.type != self._order_type()["BSTPLMT"][0]
            ):
                continue
            if (
                filter_type == "sell_stop_limits"
                and item.type != self._order_type()["SSTPLMT"][0]
            ):
                continue
            filtered_tickets.append(item.ticket)
    return filtered_tickets
positive_profit
positive_profit(th: float | None = None, id: int | None = None, account: bool = True) -> bool

Check is the total profit on current open positions Is greater than a minimum profit express as percentage of the profit target.

Parameters:

Name Type Description Default
th float

The minimum profit target on current positions

None
id int

The strategy id or expert Id

None
account bool

Weither to check positions on the account or on the symbol

True
Source code in src/bbstrader/metatrader/trade.py
def positive_profit(
    self, th: float | None = None, id: int | None = None, account: bool = True
) -> bool:
    """
    Check is the total profit on current open positions
    Is greater than a minimum profit express as percentage
    of the profit target.

    Args:
        th (float): The minimum profit target on current positions
        id (int): The strategy id or expert Id
        account (bool): Weither to check positions on the account or on the symbol
    """
    if account and id is None:
        # All open positions no matter the symbol or strategy or expert
        positions = self.account.get_positions()
    elif account and id is not None:
        # All open positions for a specific strategy or expert no matter the symbol
        positions = self.account.get_positions()
        if positions is not None:
            positions = [position for position in positions if position.magic == id]
    elif not account and id is None:
        # All open positions for the current symbol no matter the strategy or expert
        positions = self.account.get_positions(symbol=self.symbol)
    elif not account and id is not None:
        # All open positions for the current symbol and a specific strategy or expert
        positions = self.account.get_positions(symbol=self.symbol)
        if positions is not None:
            positions = [position for position in positions if position.magic == id]

    if positions is not None:
        profit = 0.0
        balance = client.account_info().balance
        target = round((balance * self.target) / 100, 2)
        for position in positions:
            profit += position.profit
        fees = self.get_average_fees()
        current_profit = profit + fees
        th_profit = (target * th) / 100 if th is not None else (target * 0.01)
        return current_profit >= th_profit
    return False
break_even
break_even(mm=True, id: int | None = None, trail: bool | None = True, stop_trail: int | str = None, trail_after_points: int | str = None, be_plus_points: int | None = None)

Manages the break-even level of a trading position.

This function checks whether it is time to set a break-even stop loss for an open position. If the break-even level is already set, it monitors price movement and updates the stop loss accordingly if the trail parameter is enabled.

When trail is enabled, the function dynamically adjusts the break-even level based on the trail_after_points and stop_trail parameters.

Parameters:

Name Type Description Default
id int

The strategy ID or expert ID.

None
mm bool

Whether to manage the position or not.

True
trail bool

Whether to trail the stop loss or not.

True
stop_trail int

Number of points to trail the stop loss by. It represent the distance from the current price to the stop loss.

None
trail_after_points (int, str)

Number of points in profit from where the strategy will start to trail the stop loss. If set to str, it must be one of the following values: - 'SL' to trail the stop loss after the profit reaches the stop loss level in points. - 'TP' to trail the stop loss after the profit reaches the take profit level in points. - 'BE' to trail the stop loss after the profit reaches the break-even level in points.

None
be_plus_points int

Number of points to add to the break-even level. Represents the minimum profit to secure.

None
Source code in src/bbstrader/metatrader/trade.py
def break_even(
    self,
    mm=True,
    id: int | None = None,
    trail: bool | None = True,
    stop_trail: int | str = None,
    trail_after_points: int | str = None,
    be_plus_points: int | None = None,
):
    """
    Manages the break-even level of a trading position.

    This function checks whether it is time to set a break-even stop loss for an open position.
    If the break-even level is already set, it monitors price movement and updates the stop loss
    accordingly if the `trail` parameter is enabled.

    When `trail` is enabled, the function dynamically adjusts the break-even level based on the
    `trail_after_points` and `stop_trail` parameters.

    Args:
        id (int): The strategy ID or expert ID.
        mm (bool): Whether to manage the position or not.
        trail (bool): Whether to trail the stop loss or not.
        stop_trail (int): Number of points to trail the stop loss by.
            It represent the distance from the current price to the stop loss.
        trail_after_points (int, str): Number of points in profit
            from where the strategy will start to trail the stop loss.
            If set to str, it must be one of the following values:
            - 'SL' to trail the stop loss after the profit reaches the stop loss level in points.
            - 'TP' to trail the stop loss after the profit reaches the take profit level in points.
            - 'BE' to trail the stop loss after the profit reaches the break-even level in points.
        be_plus_points (int): Number of points to add to the break-even level.
            Represents the minimum profit to secure.
    """

    if not mm:
        return False

    Id = id if id is not None else self.expert_id
    positions = self.account.get_positions(symbol=self.symbol)
    be = self.rm.get_break_even()
    if trail_after_points is not None:
        if isinstance(trail_after_points, int):
            assert trail_after_points > be, (
                "trail_after_points must be greater than break even or set to None"
            )
        trail_after_points = self._get_trail_after_points(trail_after_points)

    if not positions:
        return False

    for position in positions:
        if position.magic == Id:
            symbol_info = client.symbol_info(self.symbol)

            point = symbol_info.point
            digits = symbol_info.digits

            points = position.profit * (
                symbol_info.trade_tick_size
                / symbol_info.trade_tick_value
                / position.volume
            )
            break_even = float(points / point) >= be
            if not break_even:
                continue
            # Check if break-even has already been set for this position
            if position.ticket not in self.break_even_status:
                price = None
                if be_plus_points is not None:
                    price = position.price_open + (be_plus_points * point)
                self.set_break_even(position, be, price=price)
                self.break_even_status.append(position.ticket)
                self.break_even_points[position.ticket] = be
            else:
                # Skip this if the trail is not set to True
                if not trail:
                    continue
                # Check if the price has moved favorably
                new_be = (
                    round(be * 0.10) if be_plus_points is None else be_plus_points
                )
                if trail_after_points is not None:
                    if position.ticket not in self.trail_after_points:
                        # This ensures that the position rich the minimum points required
                        # before the trail can be set
                        new_be = trail_after_points - be
                        self.trail_after_points.append(position.ticket)
                new_be_points = self.break_even_points[position.ticket] + new_be
                favorable_move = float(points / point) >= new_be_points
                if not favorable_move:
                    continue
                # This allows the position to go to take profit in case of a swing trade
                # If is a scalping position, we can set the stop_trail close to the current price.
                trail_points = (
                    round(be * 0.50) if stop_trail is None else stop_trail
                )
                # Calculate the new break-even level and price
                if position.type == 0:
                    # This level validate the favorable move of the price
                    new_level = round(
                        position.price_open + (new_be_points * point),
                        digits,
                    )
                    # This price is set away from the current price by the trail_points
                    new_price = round(
                        position.price_current - (trail_points * point),
                        digits,
                    )
                    if new_price < position.sl:
                        new_price = position.sl
                elif position.type == 1:
                    new_level = round(
                        position.price_open - (new_be_points * point),
                        digits,
                    )
                    new_price = round(
                        position.price_current + (trail_points * point),
                        digits,
                    )
                    if new_price > position.sl:
                        new_price = position.sl
                return self.set_break_even(
                    position, be, price=new_price, level=new_level
                )
    return False
set_break_even
set_break_even(position: TradePosition, be: int, price: float | None = None, level: float | None = None)

Sets the break-even level for a given trading position.

Parameters:

Name Type Description Default
position TradePosition

The trading position for which the break-even is to be set. This is the value return by mt5.positions_get().

required
be int

The break-even level in points.

required
level float

The break-even level in price, if set to None , it will be calated automaticaly.

None
price float

The break-even price, if set to None , it will be calated automaticaly.

None
Source code in src/bbstrader/metatrader/trade.py
def set_break_even(
    self,
    position: TradePosition,
    be: int,
    price: float | None = None,
    level: float | None = None,
):
    """
    Sets the break-even level for a given trading position.

    Args:
        position (TradePosition): The trading position for which the break-even is to be set.
            This is the value return by `mt5.positions_get()`.
        be (int): The break-even level in points.
        level (float): The break-even level in price, if set to None , it will be calated automaticaly.
        price (float): The break-even price, if set to None , it will be calated automaticaly.
    """

    symbol_info = client.symbol_info(self.symbol)
    average_fee = abs(self.get_average_fees())
    point_value = self.rm.currency_risk().get("trade_profit", 1)
    fees_points = round((average_fee / point_value), 3) if point_value != 0 else 0

    is_buy = position.type == 0
    direction = 1 if is_buy else -1
    if not position.profit > 0:
        return False
    calc_be_level = position.price_open + (direction * be * symbol_info.point)
    calc_be_price = position.price_open + (
        direction * (fees_points + symbol_info.spread) * symbol_info.point
    )
    if price is None:
        be_price = calc_be_price
    else:
        be_price = (
            max(price, calc_be_price) if is_buy else min(price, calc_be_price)
        )
    be_level = calc_be_level if level is None else level
    tick = client.symbol_info_tick(self.symbol)
    send_request = (tick.ask > be_level) if is_buy else (tick.bid < be_level)

    if send_request:
        request = {
            "action": Mt5.TRADE_ACTION_SLTP,
            "position": position.ticket,
            "sl": round(be_price, symbol_info.digits),
            "tp": position.tp,
        }
        return self.break_even_request(
            position.ticket, round(be_price, symbol_info.digits), request
        )
    return False
break_even_request
break_even_request(tiket, price, request)

Send a request to set the stop loss to break even for a given trading position.

Parameters:

Name Type Description Default
tiket int

The ticket number of the trading position.

required
price float

The price at which the stop loss is to be set.

required
request dict

The request to set the stop loss to break even.

required
Source code in src/bbstrader/metatrader/trade.py
def break_even_request(self, tiket, price, request):
    """
    Send a request to set the stop loss to break even for a given trading position.

    Args:
        tiket (int): The ticket number of the trading position.
        price (float): The price at which the stop loss is to be set.
        request (dict): The request to set the stop loss to break even.
    """
    addtionnal = f", SYMBOL={self.symbol}"
    result = None
    try:
        client.order_check(request)
        result = client.order_send(request)
    except Exception as e:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        LOGGER.error(f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}")
        return False
    if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
        msg = trade_retcode_message(result.retcode)
        if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
            LOGGER.error(
                f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}"
            )
        tries = 0
        while result and result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
            if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
                break
            else:
                try:
                    client.order_check(request)
                    result = client.order_send(request)
                except Exception as e:
                    msg = trade_retcode_message(result.retcode) if result else "N/A"
                    LOGGER.error(
                        f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}"
                    )
                    return False
                if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
                    break
            tries += 1
    if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
        msg = trade_retcode_message(result.retcode)
        LOGGER.info(f"Break-Even Order {msg}{addtionnal}")
        info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{round(price, 5)}"
        LOGGER.info(info)
        self.break_even_status.append(tiket)
        return True
    return False
win_trade
win_trade(position: TradePosition, th: int | None = None) -> bool

Determines if a position has met the minimum 'win' threshold in points.

Source code in src/bbstrader/metatrader/trade.py
def win_trade(self, position: TradePosition, th: int | None = None) -> bool:
    """
    Determines if a position has met the minimum 'win' threshold in points.
    """
    points = self._convert_profit_to_points(position)
    if th is not None:
        win_threshold = th
    else:
        win_threshold = self._calculate_dynamic_threshold()

    is_profitable = points >= win_threshold
    not_processed = position.ticket not in self.break_even_status

    return is_profitable and not_processed
profit_target
profit_target() -> bool

Checks if the net profit for today's deals has reached the percentage target.

Source code in src/bbstrader/metatrader/trade.py
def profit_target(self) -> bool:
    """Checks if the net profit for today's deals has reached the percentage target."""
    from bbstrader.api import trade_object_to_df

    balance = client.account_info().balance
    target_amount = (balance * self.target) / 100

    opened_positions = self.get_today_deals(group=self.symbol)
    history_df = trade_object_to_df(opened_positions)

    if history_df.empty:
        return False
    net_profit = history_df[["profit", "commission", "swap", "fee"]].sum().sum()

    return net_profit >= target_amount
close_request
close_request(request: dict, type: str)

Close a trading order or position

Parameters:

Name Type Description Default
request dict

The request to close a trading order or position

required
type str

Type of the request ('order', 'position')

required
Source code in src/bbstrader/metatrader/trade.py
def close_request(self, request: dict, type: str):
    """
    Close a trading order or position

    Args:
        request (dict): The request to close a trading order or position
        type (str): Type of the request ('order', 'position')
    """
    ticket = request[type]
    addtionnal = f", SYMBOL={self.symbol}"
    result = None
    try:
        client.order_check(request)
        result = client.order_send(request)
    except Exception as e:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        LOGGER.error(
            f"Closing {type.capitalize()} Request, RETCODE={msg}{addtionnal}, Error: {e}"
        )
        return False

    if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
        if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL:  # 10030
            for fill in FILLING_TYPE:
                request["type_filling"] = fill
                result = client.order_send(request)
                if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
                    break
        elif result and result.retcode not in self._retcodes:
            self._retcodes.append(result.retcode)
            msg = trade_retcode_message(result.retcode)
            LOGGER.error(
                f"Closing Order Request, {type.capitalize()}: #{ticket}, "
                f"RETCODE={result.retcode}: {msg}{addtionnal}"
            )
        else:
            tries = 0
            while result and result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
                try:
                    client.order_check(request)
                    result = client.order_send(request)
                except Exception as e:
                    msg = trade_retcode_message(result.retcode) if result else "N/A"
                    LOGGER.error(
                        f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}"
                    )
                    return False
                if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
                    break
                tries += 1
    if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
        msg = trade_retcode_message(result.retcode)
        LOGGER.info(f"Closing Order {msg}{addtionnal}")
        info = (
            f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol},"
            f"Price: @{round(request.get('price', 0.0), 5)}"
        )
        LOGGER.info(info)
        return True
    else:
        return False
modify_order
modify_order(ticket: int, price: float | None = None, stoplimit: float | None = None, sl: float | None = None, tp: float | None = None)

Modify an open order by it ticket

Parameters:

Name Type Description Default
ticket int

Order ticket to modify (e.g TradeOrder.ticket)

required
price float

The price at which to modify the order

None
stoplimit float

A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory). The pending order is not passed to the trading system until that moment

None
sl float

The stop loss in points

None
tp float

The take profit in points

None
Source code in src/bbstrader/metatrader/trade.py
def modify_order(
    self,
    ticket: int,
    price: float | None = None,
    stoplimit: float | None = None,
    sl: float | None = None,
    tp: float | None = None,
):
    """
    Modify an open order by it ticket

    Args:
        ticket (int): Order ticket to modify (e.g TradeOrder.ticket)
        price (float): The price at which to modify the order
        stoplimit (float): A price a pending Limit order is set at
            when the price reaches the 'price' value (this condition is mandatory).
            The pending order is not passed to the trading system until that moment
        sl (float): The stop loss in points
        tp (float): The take profit in points
    """
    orders = self.account.get_orders(ticket=ticket) or []
    if len(orders) == 0:
        LOGGER.error(
            f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={round(price, 5) if price else 'N/A'}"
        )
        return False
    order = orders[0]
    request = {
        "action": Mt5.TRADE_ACTION_MODIFY,
        "order": ticket,
        "price": price or order.price_open,
        "sl": sl or order.sl,
        "tp": tp or order.tp,
        "stoplimit": stoplimit or order.price_stoplimit,
    }
    try:
        client.order_check(request)
        result = client.order_send(request)
    except Exception as e:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        LOGGER.error(f"Unable to modify Order #{ticket}, RETCODE={msg}, Error: {e}")
        return False
    if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
        LOGGER.info(
            f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={round(request['price'], 5)},"
            f"SL={round(request['sl'], 5)}, TP={round(request['tp'], 5)}, STOP_LIMIT={round(request['stoplimit'], 5)}"
        )
        return True
    else:
        msg = trade_retcode_message(result.retcode) if result else "N/A"
        retcode = result.retcode if result else None
        LOGGER.error(
            f"Unable to modify Order #{ticket}, RETCODE={retcode}: {msg}, SYMBOL={self.symbol}"
        )
        return False
close_order
close_order(ticket: int, id: int | None = None, comment: str | None = None)

Close an open order by it ticket

Parameters:

Name Type Description Default
ticket int

Order ticket to close (e.g TradeOrder.ticket)

required
id int

The unique ID of the Expert or Strategy

None
comment str

Comment for the closing position

None

Returns: - True if order closed, False otherwise

Source code in src/bbstrader/metatrader/trade.py
def close_order(
    self, ticket: int, id: int | None = None, comment: str | None = None
):
    """
    Close an open order by it ticket

    Args:
        ticket (int): Order ticket to close (e.g TradeOrder.ticket)
        id (int): The unique ID of the Expert or Strategy
        comment (str): Comment for the closing position

    Returns:
    -   True if order closed, False otherwise
    """
    request = {
        "action": Mt5.TRADE_ACTION_REMOVE,
        "symbol": self.symbol,
        "order": ticket,
        "magic": id if id is not None else self.expert_id,
        "comment": f"@{self.expert_name}" if comment is None else comment,
    }
    return self.close_request(request, type="order")
close_position
close_position(ticket: int, id: int | None = None, pct: float | None = 1.0, comment: str | None = None, symbol: str | None = None) -> bool

Close an open position by it ticket

Parameters:

Name Type Description Default
ticket int

Positon ticket to close (e.g TradePosition.ticket)

required
id int

The unique ID of the Expert or Strategy

None
pct float

Percentage of the position to close

1.0
comment str

Comment for the closing position

None

Returns: - True if position closed, False otherwise

Source code in src/bbstrader/metatrader/trade.py
def close_position(
    self,
    ticket: int,
    id: int | None = None,
    pct: float | None = 1.0,
    comment: str | None = None,
    symbol: str | None = None,
) -> bool:
    """
    Close an open position by it ticket

    Args:
        ticket (int): Positon ticket to close (e.g TradePosition.ticket)
        id (int): The unique ID of the Expert or Strategy
        pct (float): Percentage of the position to close
        comment (str): Comment for the closing position

    Returns:
    -   True if position closed, False otherwise
    """
    symbol = symbol or self.symbol
    Id = id if id is not None else self.expert_id
    positions = self.account.get_positions(ticket=ticket)
    deviation = self.rm.get_deviation()
    if positions is not None and len(positions) == 1:
        position = positions[0]
        if position.ticket == ticket and position.magic == Id:
            buy = position.type == 0
            request = {
                "action": Mt5.TRADE_ACTION_DEAL,
                "symbol": symbol,
                "volume": (position.volume * pct),
                "type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
                "position": ticket,
                "price": position.price_current,
                "deviation": deviation,
                "comment": f"@{self.expert_name}" if comment is None else comment,
                "type_time": Mt5.ORDER_TIME_GTC,
                "type_filling": Mt5.ORDER_FILLING_FOK,
            }
            return self.close_request(request, type="position")
    return False
bulk_close
bulk_close(tickets: list, tikets_type: Literal['positions', 'orders'], close_func: Callable, order_type: str, id: int | None = None, comment: str | None = None)

Close multiple orders or positions at once.

Parameters:

Name Type Description Default
tickets List

List of tickets to close

required
tikets_type str

Type of tickets to close ('positions', 'orders')

required
close_func Callable

The function to close the tickets

required
order_type str

Type of orders or positions to close

required
id int

The unique ID of the Expert or Strategy

None
comment str

Comment for the closing position

None
Source code in src/bbstrader/metatrader/trade.py
def bulk_close(
    self,
    tickets: list,
    tikets_type: Literal["positions", "orders"],
    close_func: Callable,
    order_type: str,
    id: int | None = None,
    comment: str | None = None,
):
    """
    Close multiple orders or positions at once.

    Args:
        tickets (List): List of tickets to close
        tikets_type (str): Type of tickets to close ('positions', 'orders')
        close_func (Callable): The function to close the tickets
        order_type (str): Type of orders or positions to close
        id (int): The unique ID of the Expert or Strategy
        comment (str): Comment for the closing position
    """
    if order_type == "all":
        order_type = "open"

    if not tickets:
        return
    failed_tickets = []
    with ThreadPoolExecutor(max_workers=min(len(tickets), 20)) as executor:
        future_to_ticket = {
            executor.submit(close_func, ticket, id=id, comment=comment): ticket
            for ticket in tickets
        }
        for future in as_completed(future_to_ticket):
            ticket = future_to_ticket[future]
            try:
                success = future.result()
                if not success:
                    failed_tickets.append(ticket)
            except Exception as exc:
                LOGGER.error(f"Ticket {ticket} generated an exception: {exc}")
                failed_tickets.append(ticket)
    if not failed_tickets:
        LOGGER.info(
            f"ALL {order_type.upper()} {tikets_type.upper()} closed, SYMBOL={self.symbol}."
        )
    else:
        LOGGER.info(
            f"{len(failed_tickets)}/{len(tickets)} {order_type.upper()} {tikets_type.upper()} NOT closed, SYMBOL={self.symbol}"
        )
close_orders
close_orders(order_type: Orders, id: int | None = None, comment: str | None = None)

Parameters:

Name Type Description Default
order_type str

Type of orders to close ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')

required
id int

The unique ID of the Expert or Strategy

None
comment str

Comment for the closing position

None
Source code in src/bbstrader/metatrader/trade.py
def close_orders(
    self,
    order_type: Orders,
    id: int | None = None,
    comment: str | None = None,
):
    """
    Args:
        order_type (str): Type of orders to close
            ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
        id (int): The unique ID of the Expert or Strategy
        comment (str): Comment for the closing position
    """
    id = id if id is not None else self.expert_id
    if order_type == "all":
        orders = self.get_current_orders(id=id)
    elif order_type == "buy_stops":
        orders = self.get_current_buy_stops(id=id)
    elif order_type == "sell_stops":
        orders = self.get_current_sell_stops(id=id)
    elif order_type == "buy_limits":
        orders = self.get_current_buy_limits(id=id)
    elif order_type == "sell_limits":
        orders = self.get_current_sell_limits(id=id)
    elif order_type == "buy_stop_limits":
        orders = self.get_current_buy_stop_limits(id=id)
    elif order_type == "sell_stop_limits":
        orders = self.get_current_sell_stop_limits(id=id)
    else:
        LOGGER.error(f"Invalid order type: {order_type}")
        return
    self.bulk_close(
        orders, "orders", self.close_order, order_type, id=id, comment=comment
    )
close_positions
close_positions(position_type: Positions, id: int | None = None, comment: str | None = None)

Parameters:

Name Type Description Default
position_type str

Type of positions to close ('all', 'buy', 'sell', 'profitable', 'losing')

required
id int

The unique ID of the Expert or Strategy

None
comment str

Comment for the closing position

None
Source code in src/bbstrader/metatrader/trade.py
def close_positions(
    self,
    position_type: Positions,
    id: int | None = None,
    comment: str | None = None,
):
    """
    Args:
        position_type (str): Type of positions to close ('all', 'buy', 'sell', 'profitable', 'losing')
        id (int): The unique ID of the Expert or Strategy
        comment (str): Comment for the closing position
    """
    id = id if id is not None else self.expert_id
    if position_type == "all":
        positions = self.get_current_positions(id=id)
    elif position_type == "buy":
        positions = self.get_current_buys(id=id)
    elif position_type == "sell":
        positions = self.get_current_sells(id=id)
    elif position_type == "profitable":
        positions = self.get_current_profitables(id=id)
    elif position_type == "losing":
        positions = self.get_current_losings(id=id)
    else:
        LOGGER.error(f"Invalid position type: {position_type}")
        return
    self.bulk_close(
        positions,
        "positions",
        self.close_position,
        position_type,
        id=id,
        comment=comment,
    )
is_max_trades_reached
is_max_trades_reached() -> bool

Check if the maximum number of trades for the day has been reached.

:return: bool

Source code in src/bbstrader/metatrader/trade.py
def is_max_trades_reached(self) -> bool:
    """
    Check if the maximum number of trades for the day has been reached.

    :return: bool
    """
    max_trades = self.rm.max_trade()
    today_deals = self.get_today_deals(group=self.symbol)
    negative_deals = [deal for deal in today_deals if deal.profit < 0]
    return len(negative_deals) >= max_trades
get_stats
get_stats() -> tuple[dict[str, Any], dict[str, Any]]

Retrieves aggregated session and historical trading performance.

Source code in src/bbstrader/metatrader/trade.py
def get_stats(self) -> tuple[dict[str, Any], dict[str, Any]]:
    """Retrieves aggregated session and historical trading performance."""
    today_deals = self.get_today_deals(group=self.symbol)
    stats1 = self._calculate_session_stats(today_deals)
    stats2 = self._calculate_historical_stats()

    return stats1, stats2
sharpe
sharpe()

Calculate the Sharpe ratio of a returns stream based on a number of trading periods. The function assumes that the returns are the excess of those compared to a benchmark.

Source code in src/bbstrader/metatrader/trade.py
def sharpe(self):
    """
    Calculate the Sharpe ratio of a returns stream
    based on a number of trading periods.
    The function assumes that the returns are the excess of
    those compared to a benchmark.
    """
    import warnings

    warnings.filterwarnings("ignore")
    history = self.account.get_trades_history()
    if history is None or len(history) < 2:
        return 0.0
    df = history.iloc[1:]
    profit = df[["profit", "commission", "fee", "swap"]].sum(axis=1)
    returns = profit.pct_change(fill_method=None)
    periods = self.rm.max_trade() * 252
    sharpe = qs.stats.sharpe(returns, periods=periods)

    return round(sharpe, 3)
days_end
days_end() -> bool

Check if it is the end of the trading day.

Source code in src/bbstrader/metatrader/trade.py
def days_end(self) -> bool:
    """Check if it is the end of the trading day."""
    fmt = "%H:%M"
    now = datetime.now().time()
    end = datetime.strptime(self.end, fmt).time()
    if self.broker_tz:
        now = self.account.broker.get_broker_time(self.current_time(), fmt).time()
        end = self.account.broker.get_broker_time(self.end, fmt).time()
    if now >= end:
        return True
    return False
trading_time
trading_time()

Check if it is time to trade.

Source code in src/bbstrader/metatrader/trade.py
def trading_time(self):
    """Check if it is time to trade."""
    fmt = "%H:%M"
    now = datetime.now()
    start = datetime.strptime(self.start, fmt).time()
    end = datetime.strptime(self.finishing, fmt).time()
    if self.broker_tz:
        now = self.account.broker.get_broker_time(self.current_time(), fmt).time()
        start = self.account.broker.get_broker_time(self.start, fmt).time()
        now = self.account.broker.get_broker_time(self.finishing, fmt).time()
    if start <= now.time() <= end:
        return True
    return False

create_trade_instance

create_trade_instance(symbols: list[str], params: dict[str, Any], daily_risk: dict[str, float] | None = None, max_risk: dict[str, float] | None = None, pchange_sl: dict[str, float] | float | None = None, **kwargs) -> dict[str, Trade]

Creates Trade instances for each symbol provided.

Parameters:

Name Type Description Default
symbols list[str]

A list of trading symbols (e.g., ['AAPL', 'MSFT']).

required
params dict[str, Any]

A dictionary containing parameters for the Trade instance.

required
daily_risk dict[str, float] | None

A dictionary containing daily risk weight for each symbol.

None
max_risk dict[str, float] | None

A dictionary containing maximum risk weight for each symbol.

None

Returns:

Type Description
dict[str, Trade]

A dictionary where keys are symbols and values are corresponding Trade instances.

Raises:

Type Description
ValueError

If the 'symbols' list is empty or the 'params' dictionary is missing required keys.

Note

daily_risk and max_risk can be used to manage the risk of each symbol based on the importance of the symbol in the portfolio or strategy. See bbstrader.metatrader.risk.RiskManagement for more details.

Source code in src/bbstrader/metatrader/trade.py
def create_trade_instance(
    symbols: list[str],
    params: dict[str, Any],
    daily_risk: dict[str, float] | None = None,
    max_risk: dict[str, float] | None = None,
    pchange_sl: dict[str, float] | float | None = None,
    **kwargs,
) -> dict[str, Trade]:
    """
    Creates Trade instances for each symbol provided.

    Args:
        symbols: A list of trading symbols (e.g., ['AAPL', 'MSFT']).
        params: A dictionary containing parameters for the Trade instance.
        daily_risk: A dictionary containing daily risk weight for each symbol.
        max_risk: A dictionary containing maximum risk weight for each symbol.

    Returns:
        A dictionary where keys are symbols and values are corresponding Trade instances.

    Raises:
        ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.

    Note:
        `daily_risk` and `max_risk`  can be used to manage the risk of each symbol
        based on the importance of the symbol in the portfolio or strategy.
        See bbstrader.metatrader.risk.RiskManagement for more details.
    """
    if not symbols or not params:
        raise ValueError("Symbols and params are required.")

    logger = params.get("logger") if isinstance(params.get("logger"), Logger) else log
    base_id = params.get("expert_id", EXPERT_ID)

    def get_val(source, symbol, default=None):
        if isinstance(source, dict):
            if symbol not in source:
                raise ValueError(f"Missing key '{symbol}' in configuration.")
            return source[symbol]
        return source if source is not None else default

    trades = {}
    for sym in symbols:
        try:
            conf = {
                **params,
                "symbol": sym,
                "expert_id": get_val(base_id, sym),
                "daily_risk": get_val(daily_risk, sym, params.get("daily_risk")),
                "max_risk": get_val(max_risk, sym, params.get("max_risk", 10.0)),
                "pchange_sl": get_val(pchange_sl, sym, params.get("pchange_sl")),
            }
            trades[sym] = Trade(**conf)

        except Exception as e:
            logger.error(f"Failed trade init: SYMBOL={sym} | ERR={e}")

    # Final Audit
    if len(trades) < len(symbols):
        missing = set(symbols) - set(trades.keys())
        logger.warning(f"Partial success. Missing symbols: {missing}")

    logger.info(f"Initialized {len(trades)} trade instances.")
    return trades

utils

TimeFrame

Bases: Enum

Rrepresent a time frame object

SymbolType

Bases: Enum

Represents the type of a symbol.

RateInfo

Bases: NamedTuple

Reprents a candle (bar) for a specified period. * time: Time in seconds since 1970.01.01 00:00 * open: Open price * high: High price * low: Low price * close: Close price * tick_volume: Tick volume * spread: Spread value * real_volume: Real volume

InvalidBroker

InvalidBroker(message='Invalid broker.')

Bases: Exception

Exception raised for invalid broker errors.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Invalid broker."):
    super().__init__(message)

MT5TerminalError

MT5TerminalError(code, message)

Bases: Exception

Base exception class for trading-related errors.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, code, message):
    super().__init__(message)
    self.code = code
    self.message = message

GenericFail

GenericFail(message='Generic fail')

Bases: MT5TerminalError

Exception raised for generic failure.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Generic fail"):
    super().__init__(MT5.RES_E_FAIL, message)

InvalidParams

InvalidParams(message='Invalid arguments or parameters.')

Bases: MT5TerminalError

Exception raised for invalid arguments or parameters.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Invalid arguments or parameters."):
    super().__init__(MT5.RES_E_INVALID_PARAMS, message)

HistoryNotFound

HistoryNotFound(message='No history found.')

Bases: MT5TerminalError

Exception raised when no history is found.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="No history found."):
    super().__init__(MT5.RES_E_NOT_FOUND, message)

InvalidVersion

InvalidVersion(message='Invalid version.')

Bases: MT5TerminalError

Exception raised for an invalid version.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Invalid version."):
    super().__init__(MT5.RES_E_INVALID_VERSION, message)

AuthFailed

AuthFailed(message='Authorization failed.')

Bases: MT5TerminalError

Exception raised for authorization failure.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Authorization failed."):
    super().__init__(MT5.RES_E_AUTH_FAILED, message)

UnsupportedMethod

UnsupportedMethod(message='Unsupported method.')

Bases: MT5TerminalError

Exception raised for an unsupported method.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Unsupported method."):
    super().__init__(MT5.RES_E_UNSUPPORTED, message)

AutoTradingDisabled

AutoTradingDisabled(message='Auto-trading is disabled.')

Bases: MT5TerminalError

Exception raised when auto-trading is disabled.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Auto-trading is disabled."):
    super().__init__(MT5.RES_E_AUTO_TRADING_DISABLED, message)

InternalFailError

InternalFailError(code, message)

Bases: MT5TerminalError

Base exception class for internal IPC errors.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, code, message):
    super().__init__(code, message)

InternalFailSend

InternalFailSend(message='Internal IPC send failed.')

Bases: InternalFailError

Exception raised for internal IPC send failure.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Internal IPC send failed."):
    super().__init__(MT5.RES_E_INTERNAL_FAIL_SEND, message)

InternalFailReceive

InternalFailReceive(message='Internal IPC receive failed.')

Bases: InternalFailError

Exception raised for internal IPC receive failure.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Internal IPC receive failed."):
    super().__init__(MT5.RES_E_INTERNAL_FAIL_RECEIVE, message)

InternalFailInit

InternalFailInit(message='Internal IPC initialization failed.')

Bases: InternalFailError

Exception raised for internal IPC initialization failure.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Internal IPC initialization failed."):
    super().__init__(MT5.RES_E_INTERNAL_FAIL_INIT, message)

InternalFailConnect

InternalFailConnect(message='No IPC connection.')

Bases: InternalFailError

Exception raised for no IPC connection.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="No IPC connection."):
    super().__init__(MT5.RES_E_INTERNAL_FAIL_CONNECT, message)

InternalFailTimeout

InternalFailTimeout(message='Internal timeout.')

Bases: InternalFailError

Exception raised for an internal timeout.

Source code in src/bbstrader/metatrader/utils.py
def __init__(self, message="Internal timeout."):
    super().__init__(MT5.RES_E_INTERNAL_FAIL_TIMEOUT, message)

raise_mt5_error

raise_mt5_error(message: Optional[str] = None)

Raises an exception based on the given error code.

Parameters:

Name Type Description Default
message Optional[str]

An optional custom error message.

None

Raises:

Type Description
MT5TerminalError

A specific exception based on the error code.

Source code in src/bbstrader/metatrader/utils.py
def raise_mt5_error(message: Optional[str] = None):
    """Raises an exception based on the given error code.

    Args:
        message: An optional custom error message.

    Raises:
        MT5TerminalError: A specific exception based on the error code.
    """
    if message and isinstance(message, Exception):
        message = str(message)
    exception = _ERROR_CODE_TO_EXCEPTION_.get(MT5.last_error()[0])
    if exception is not None:
        raise exception(f"{message or MT5.last_error()[1]}")
    else:
        raise Exception(f"{message or MT5.last_error()[1]}")

retry_on_disconnect

retry_on_disconnect(max_retries: int = 3, delay: float = 1.0) -> Callable[[_F], _F]

Decorator that retries a function on MT5 connection errors with exponential back-off.

Catches InternalFailConnect and InternalFailTimeout, waits delay * 2**attempt seconds between tries, then re-raises on the last attempt.

Source code in src/bbstrader/metatrader/utils.py
def retry_on_disconnect(max_retries: int = 3, delay: float = 1.0) -> Callable[[_F], _F]:
    """Decorator that retries a function on MT5 connection errors with exponential back-off.

    Catches ``InternalFailConnect`` and ``InternalFailTimeout``, waits
    ``delay * 2**attempt`` seconds between tries, then re-raises on the last attempt.
    """

    def decorator(func: _F) -> _F:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except (InternalFailConnect, InternalFailTimeout):
                    if attempt == max_retries - 1:
                        raise
                    time.sleep(delay * (2**attempt))

        return wrapper  # type: ignore[return-value]

    return decorator  # type: ignore[return-value]

trade_retcode_message

trade_retcode_message(code, display=False, add_msg='')

Retrieves a user-friendly message corresponding to a given trade return code.

Parameters:

Name Type Description Default
code int

The trade return code to look up.

required
display bool

Whether to print the message to the console. Defaults to False.

False

Returns:

Name Type Description
str

The message associated with the provided trade return code. If the code is not found, it returns "Unknown trade error.".

Source code in src/bbstrader/metatrader/utils.py
def trade_retcode_message(code, display=False, add_msg=""):
    """
    Retrieves a user-friendly message corresponding to a given trade return code.

    Args:
        code (int): The trade return code to look up.
        display (bool, optional): Whether to print the message to the console. Defaults to False.

    Returns:
        str: The message associated with the provided trade return code. If the code is not found,
             it returns "Unknown trade error.".
    """
    message = _TRADE_RETCODE_MESSAGES_.get(code, "Unknown trade error")
    if display:
        print(message + add_msg)
    return message