@app.command()
-deflast_error(ctx:typer.Context)->None:
-"""Export the last error information."""
-_export_command(ctx,lambdaclient:client.last_error())
+546
+547
@app.command()
+deflast_error(ctx:typer.Context)->None:
+"""Export the last error information."""
+_export_command(ctx,lambdaclient:client.last_error())
@@ -855,8 +875,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
-
@app.command()
-defmt5_summary(ctx:typer.Context)->None:
-"""Export a compact terminal/account status summary."""
-_export_command(ctx,lambdaclient:client.mt5_summary_as_df())
+534
+535
@app.command()
+defmt5_summary(ctx:typer.Context)->None:
+"""Export a compact terminal/account status summary."""
+_export_command(ctx,lambdaclient:client.mt5_summary_as_df())
@@ -1086,8 +1106,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
-
@app.command()
-deforder_send(
-ctx:typer.Context,
-request:Annotated[
-dict[str,Any],
-typer.Option(click_type=REQUEST_TYPE,help=_REQUEST_OPTION_HELP),
-],
-yes:Annotated[
-bool,
-typer.Option("--yes",help="Confirm the live trade request."),
-]=False,
-)->None:
-"""Send a trading operation request to the trade server.
-
- Raises:
- typer.BadParameter: If --yes is not provided.
- """
-ifnotyes:
-msg="Pass --yes to send a live trade request."
-raisetyper.BadParameter(msg,param_hint="--yes")
-export_ctx=_get_export_context(ctx)
-
-def_fetch()->pd.DataFrame:
-returnsdk._run_with_client(# noqa: SLF001 # pyright: ignore[reportPrivateUsage]
-export_ctx.config,
-lambdac:c.order_send_as_df(request=request),
-)
-
-_execute_export(ctx,_fetch)
+600
@app.command()
+deforder_send(
+ctx:typer.Context,
+request:Annotated[
+dict[str,Any],
+typer.Option(click_type=REQUEST_TYPE,help=_REQUEST_OPTION_HELP),
+],
+yes:Annotated[
+bool,
+typer.Option("--yes",help="Confirm the live trade request."),
+]=False,
+)->None:
+"""Send a trading operation request to the trade server.
+
+ Raises:
+ typer.BadParameter: If --yes is not provided.
+ """
+ifnotyes:
+msg="Pass --yes to send a live trade request."
+raisetyper.BadParameter(msg,param_hint="--yes")
+_export_command(ctx,lambdaclient:client.order_send(request))
@@ -1280,8 +1268,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
-
@app.command()
-defsymbol_info_tick(
-ctx:typer.Context,
-symbol:Annotated[str,typer.Option(help="Symbol name.")],
-)->None:
-"""Export the last tick for a symbol."""
-_export_command(ctx,lambdaclient:client.symbol_info_tick(symbol))
+555
+556
@app.command()
+defsymbol_info_tick(
+ctx:typer.Context,
+symbol:Annotated[str,typer.Option(help="Symbol name.")],
+)->None:
+"""Export the last tick for a symbol."""
+_export_command(ctx,lambdaclient:client.symbol_info_tick(symbol))
@@ -1861,8 +1849,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
-
Public client for generic MT5 data access and order primitives.
+
Extends the read-only SDK client with optional order check/send helpers and
+exposes the same connection lifecycle as :class:~mt5cli.sdk.Mt5CliClient.
+Downstream applications such as private trading packages should prefer this
+type over the legacy Mt5CliClient name.
+
mt5cli intentionally exposes minimal execution primitives only. Trading
+decisions, signals, strategies, backtests, and optimization remain the
+responsibility of downstream applications.
@classmethod
+deffrom_connected_client(cls,client:Mt5DataClient)->Self:
+"""Bind to an already-connected ``Mt5DataClient`` without owning it.
+
+ Returns:
+ Client wrapper bound to the injected connection.
+ """
+returncls(client=client)
+
Send a live trade request to the MT5 trade server.
+
+
+
+ Warning
+
This is a live execution primitive. A successful call can place,
+modify, or close real trades on the connected account. Downstream
+applications must gate usage explicitly (for example behind manual
+confirmation or application-specific risk controls). mt5cli does
+not implement strategy logic, signal generation, or trade sizing.
deforder_send(self,request:dict[str,Any])->pd.DataFrame:
+"""Send a live trade request to the MT5 trade server.
+
+ Warning:
+ This is a live execution primitive. A successful call can place,
+ modify, or close real trades on the connected account. Downstream
+ applications must gate usage explicitly (for example behind manual
+ confirmation or application-specific risk controls). mt5cli does
+ not implement strategy logic, signal generation, or trade sizing.
+
+ Args:
+ request: MT5 order request dictionary.
+
+ Returns:
+ One-row DataFrame with the order-send result.
+ """
+returnself._fetch(lambdaclient:client.order_send_as_df(request=request))
+
@contextmanager
+defmt5_session(config:Mt5Config|None=None)->Iterator[MT5Client]:
+"""Open an MT5 terminal session and yield a connected :class:`MT5Client`.
+
+ Args:
+ config: MT5 connection configuration. Defaults to an empty config that
+ attaches to a running terminal.
+
+ Yields:
+ Connected :class:`MT5Client` bound to the session.
+ """
+mt5_config=configorbuild_config()
+withconnected_client(mt5_config)asclient:
+yieldMT5Client.from_connected_client(client)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Search
+
+
+
+
From here you can search these documents. Enter your search terms below.
defgranularity_name(timeframe:int|str)->str:
+"""Return a short granularity label for a timeframe integer or name.
+
+ Args:
+ timeframe: MT5 timeframe as integer or name (for example ``M1``).
+
+ Returns:
+ Short name such as ``M1`` or the stringified integer when unknown.
+ """
+tf=parse_timeframe(timeframe)
+try:
+name=_get_timeframe_name(tf)
+exceptValueError:
+returnstr(tf)
+returnname.removeprefix("TIMEFRAME_")
+
defnormalize_symbol(symbol:str)->str:
+"""Normalize a broker symbol name for MT5 API calls.
+
+ Strips surrounding whitespace while preserving broker-specific casing and
+ suffixes (for example ``XAUUSDm``, ``US500.cash``, or ``EURUSD.r``).
+
+ Args:
+ symbol: Raw symbol name.
+
+ Returns:
+ Normalized symbol string.
+
+ Raises:
+ ValueError: If the symbol is empty after normalization.
+ """
+normalized=symbol.strip()
+ifnotnormalized:
+msg="Symbol must not be empty."
+raiseValueError(msg)
+returnnormalized
+
defparse_date_range(
+date_from:datetime|str,
+date_to:datetime|str,
+)->tuple[datetime,datetime]:
+"""Parse and validate an inclusive UTC date range.
+
+ Args:
+ date_from: Range start as datetime or ISO 8601 string.
+ date_to: Range end as datetime or ISO 8601 string.
+
+ Returns:
+ Tuple of UTC-aware ``(start, end)`` datetimes.
+
+ Raises:
+ ValueError: If ``date_from`` is after ``date_to``.
+ """
+start=ensure_utc(date_from)
+end=ensure_utc(date_to)
+ifstart>end:
+msg=(
+f"date_from ({start.isoformat()}) must not be after "
+f"date_to ({end.isoformat()})."
+)
+raiseValueError(msg)
+returnstart,end
+
defparse_datetime(value:str)->datetime:
+"""Parse an ISO 8601 datetime string to a timezone-aware datetime.
+
+ Args:
+ value: ISO 8601 datetime string (e.g., '2024-01-01' or
+ '2024-01-01T12:00:00+00:00').
+
+ Returns:
+ Parsed datetime with UTC timezone if no timezone is specified.
+
+ Raises:
+ ValueError: If the string cannot be parsed.
+ """
+try:
+dt=datetime.fromisoformat(value)
+exceptValueError:
+msg=f"Invalid datetime format: '{value}'. Use ISO 8601 format."
+raiseValueError(msg)fromNone
+ifdt.tzinfoisNone:
+dt=dt.replace(tzinfo=UTC)
+returndt
+
defrecent_window(
+*,
+hours:float|None=None,
+seconds:float|None=None,
+date_to:datetime|str|None=None,
+)->tuple[datetime,datetime]:
+"""Build a trailing UTC window ending at ``date_to`` or now.
+
+ Exactly one of ``hours`` or ``seconds`` must be provided.
+
+ Args:
+ hours: Trailing window length in hours.
+ seconds: Trailing window length in seconds.
+ date_to: Window end. Defaults to current UTC time.
+
+ Returns:
+ Tuple of UTC-aware ``(start, end)`` datetimes.
+
+ Raises:
+ ValueError: If neither or both window lengths are provided, or if a
+ length is not positive.
+ """
+if(hoursisNone)==(secondsisNone):
+msg="Provide exactly one of hours or seconds."
+raiseValueError(msg)
+ifhoursisnotNone:
+length=timedelta(hours=hours)
+else:
+length=timedelta(seconds=secondsifsecondsisnotNoneelse0)
+iflength.total_seconds()<=0:
+msg="Window length must be positive."
+raiseValueError(msg)
+end=ensure_utc(date_to)ifdate_toisnotNoneelsedatetime.now(UTC)
+returnend-length,end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Search
+
+
+
+
From here you can search these documents. Enter your search terms below.
defnormalize_mt5_exception(exc:BaseException)->Mt5CliError:
+"""Map pdmt5/MT5 exceptions to stable mt5cli exception types.
+
+ Args:
+ exc: Original exception from MT5 or pdmt5.
+
+ Returns:
+ ``Mt5ConnectionError`` for runtime failures, ``Mt5OperationError`` for
+ trading failures, or the original exception when it is not recognized.
+ """
+ifisinstance(exc,Mt5TradingError):
+returnMt5OperationError(str(exc))
+ifisinstance(exc,Mt5RuntimeError):
+returnMt5ConnectionError(str(exc))
+ifisinstance(exc,Mt5CliError):
+returnexc
+returnMt5CliError(str(exc))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Search
+
+
+
+
From here you can search these documents. Enter your search terms below.
950951952953
@@ -682,28 +701,29 @@ by an explicit table (for example a custom SQLite view).
967968969
-970
defappend_dataframe(
-conn:sqlite3.Connection,
-frame:pd.DataFrame,
-table_name:str,
-if_exists:IfExists,
-)->bool:
-"""Append a DataFrame to SQLite when it has a schema.
-
- Returns:
- True if a table was written, False if the frame had no columns.
- """
-iflen(frame.columns)==0:
-logger.warning("Skipping %s: dataset returned no columns",table_name)
-returnFalse
-frame.to_sql(# type: ignore[reportUnknownMemberType]
-table_name,
-conn,
-if_exists=if_exists.value,
-index=False,
-chunksize=50_000,
-)
-returnTrue
+970
+971
defappend_dataframe(
+conn:sqlite3.Connection,
+frame:pd.DataFrame,
+table_name:str,
+if_exists:IfExists,
+)->bool:
+"""Append a DataFrame to SQLite when it has a schema.
+
+ Returns:
+ True if a table was written, False if the frame had no columns.
+ """
+iflen(frame.columns)==0:
+logger.warning("Skipping %s: dataset returned no columns",table_name)
+returnFalse
+frame.to_sql(# type: ignore[reportUnknownMemberType]
+table_name,
+conn,
+if_exists=if_exists.value,
+index=False,
+chunksize=50_000,
+)
+returnTrue
@@ -732,33 +752,33 @@ by an explicit table (for example a custom SQLite view).
Source code in mt5cli/history.py
-
535536537538
@@ -958,41 +977,42 @@ with symbol=None for each timeframe instead of raising.
565566567
-568
defbuild_rate_targets(
-symbols:Sequence[str],
-timeframes:Sequence[int|str],
-*,
-allow_missing_symbol:bool=False,
-)->list[RateTarget]:
-"""Build rate targets for every symbol and timeframe combination.
-
- Args:
- symbols: MT5 symbol names. May be empty when ``allow_missing_symbol``.
- timeframes: MT5 timeframes as integers or names (for example ``M1``).
- allow_missing_symbol: When True and ``symbols`` is empty, build targets
- with ``symbol=None`` for each timeframe instead of raising.
-
- Returns:
- Targets in row-major order: every timeframe for the first symbol, then
- every timeframe for the next symbol, and so on.
-
- Raises:
- ValueError: If ``timeframes`` is empty, or ``symbols`` is empty and
- ``allow_missing_symbol`` is False.
- """
-ifnottimeframes:
-msg="At least one timeframe is required."
-raiseValueError(msg)
-ifnotsymbols:
-ifnotallow_missing_symbol:
-msg="At least one symbol is required."
-raiseValueError(msg)
-return[RateTarget(symbol=None,timeframe=tf)fortfintimeframes]
-return[
-RateTarget(symbol=symbol,timeframe=tf)
-forsymbolinsymbols
-fortfintimeframes
-]
+568
+569
defbuild_rate_targets(
+symbols:Sequence[str],
+timeframes:Sequence[int|str],
+*,
+allow_missing_symbol:bool=False,
+)->list[RateTarget]:
+"""Build rate targets for every symbol and timeframe combination.
+
+ Args:
+ symbols: MT5 symbol names. May be empty when ``allow_missing_symbol``.
+ timeframes: MT5 timeframes as integers or names (for example ``M1``).
+ allow_missing_symbol: When True and ``symbols`` is empty, build targets
+ with ``symbol=None`` for each timeframe instead of raising.
+
+ Returns:
+ Targets in row-major order: every timeframe for the first symbol, then
+ every timeframe for the next symbol, and so on.
+
+ Raises:
+ ValueError: If ``timeframes`` is empty, or ``symbols`` is empty and
+ ``allow_missing_symbol`` is False.
+ """
+ifnottimeframes:
+msg="At least one timeframe is required."
+raiseValueError(msg)
+ifnotsymbols:
+ifnotallow_missing_symbol:
+msg="At least one symbol is required."
+raiseValueError(msg)
+return[RateTarget(symbol=None,timeframe=tf)fortfintimeframes]
+return[
+RateTarget(symbol=symbol,timeframe=tf)
+forsymbolinsymbols
+fortfintimeframes
+]
@@ -1026,8 +1046,7 @@ a symbol such as EURUSD_M1 cannot collide with EURUSDSource code in mt5cli/history.py
-
127128129130
@@ -1041,22 +1060,23 @@ a symbol such as EURUSD_M1 cannot collide with EURUSD138139140
-141
defbuild_rate_view_name(
-*,
-symbol:str,
-granularity:str,
-granularity_count:int,
-timeframe:int,
-)->str:
-"""Return a collision-free offline optimize view name.
-
- View names always include the timeframe integer after a ``__`` separator so
- a symbol such as ``EURUSD_M1`` cannot collide with ``EURUSD`` at timeframe
- ``M1``.
- """
-ifgranularity_count==1:
-returnf"rate_{symbol}__{timeframe}"
-returnf"rate_{symbol}__{granularity}_{timeframe}"
+141
+142
defbuild_rate_view_name(
+*,
+symbol:str,
+granularity:str,
+granularity_count:int,
+timeframe:int,
+)->str:
+"""Return a collision-free offline optimize view name.
+
+ View names always include the timeframe integer after a ``__`` separator so
+ a symbol such as ``EURUSD_M1`` cannot collide with ``EURUSD`` at timeframe
+ ``M1``.
+ """
+ifgranularity_count==1:
+returnf"rate_{symbol}__{timeframe}"
+returnf"rate_{symbol}__{granularity}_{timeframe}"
@@ -1106,8 +1126,7 @@ a symbol such as EURUSD_M1 cannot collide with EURUSDSource code in mt5cli/history.py
-
defcreate_cash_events_view(
-conn:sqlite3.Connection,
-deals_columns:set[str],
-)->bool:
-"""Create the cash_events SQLite view derived from history_deals.
-
- Returns:
- True if the view was created, False if required columns are missing.
- """
-if"type"notindeals_columns:
-logger.warning("Skipping cash_events view: history_deals.type is missing")
-returnFalse
-conn.execute("DROP VIEW IF EXISTS cash_events")
-conn.execute(
-"CREATE VIEW cash_events AS"# noqa: S608
-f" SELECT * FROM history_deals WHERE type NOT IN {_TRADE_DEAL_TYPES_SQL}",
-)
-returnTrue
+1240
+1241
defcreate_cash_events_view(
+conn:sqlite3.Connection,
+deals_columns:set[str],
+)->bool:
+"""Create the cash_events SQLite view derived from history_deals.
+
+ Returns:
+ True if the view was created, False if required columns are missing.
+ """
+if"type"notindeals_columns:
+logger.warning("Skipping cash_events view: history_deals.type is missing")
+returnFalse
+conn.execute("DROP VIEW IF EXISTS cash_events")
+conn.execute(
+"CREATE VIEW cash_events AS"# noqa: S608
+f" SELECT * FROM history_deals WHERE type NOT IN {_TRADE_DEAL_TYPES_SQL}",
+)
+returnTrue
@@ -1168,8 +1188,7 @@ a symbol such as EURUSD_M1 cannot collide with EURUSDSource code in mt5cli/history.py
-
defcreate_history_indexes(
-conn:sqlite3.Connection,
-written_columns:dict[Dataset,set[str]],
-)->None:
-"""Create useful indexes for collected history tables when present."""
-if{"symbol","timeframe","time"}.issubset(
-written_columns.get(Dataset.rates,set()),
-):
-conn.execute(
-"CREATE INDEX IF NOT EXISTS idx_rates_symbol_timeframe_time"
-" ON rates(symbol, timeframe, time)",
-)
-if{"symbol","time"}.issubset(written_columns.get(Dataset.ticks,set())):
-conn.execute(
-"CREATE INDEX IF NOT EXISTS idx_ticks_symbol_time ON ticks(symbol, time)",
-)
-if{"position_id","symbol"}.issubset(
-written_columns.get(Dataset.history_deals,set()),
-):
-conn.execute(
-"CREATE INDEX IF NOT EXISTS idx_history_deals_position_symbol"
-" ON history_deals(position_id, symbol)",
-)
+1156
+1157
defcreate_history_indexes(
+conn:sqlite3.Connection,
+written_columns:dict[Dataset,set[str]],
+)->None:
+"""Create useful indexes for collected history tables when present."""
+if{"symbol","timeframe","time"}.issubset(
+written_columns.get(Dataset.rates,set()),
+):
+conn.execute(
+"CREATE INDEX IF NOT EXISTS idx_rates_symbol_timeframe_time"
+" ON rates(symbol, timeframe, time)",
+)
+if{"symbol","time"}.issubset(written_columns.get(Dataset.ticks,set())):
+conn.execute(
+"CREATE INDEX IF NOT EXISTS idx_ticks_symbol_time ON ticks(symbol, time)",
+)
+if{"position_id","symbol"}.issubset(
+written_columns.get(Dataset.history_deals,set()),
+):
+conn.execute(
+"CREATE INDEX IF NOT EXISTS idx_history_deals_position_symbol"
+" ON history_deals(position_id, symbol)",
+)
@@ -1262,8 +1282,7 @@ a symbol such as EURUSD_M1 cannot collide with EURUSDSource code in mt5cli/history.py
-
defcreate_positions_reconstructed_view(
-conn:sqlite3.Connection,
-deals_columns:set[str],
-)->bool:
-"""Create the positions_reconstructed SQLite view derived from history_deals.
-
- Returns:
- True if the view was created, False if required columns are missing.
- """
-ifnot_POSITIONS_VIEW_REQUIRED_COLUMNS.issubset(deals_columns):
-missing=", ".join(sorted(_POSITIONS_VIEW_REQUIRED_COLUMNS-deals_columns))
-logger.warning(
-"Skipping positions_reconstructed view: history_deals missing columns: %s",
-missing,
-)
-returnFalse
-conn.execute("DROP VIEW IF EXISTS positions_reconstructed")
-conn.execute(
-"CREATE VIEW positions_reconstructed AS"# noqa: S608
-" SELECT"
-" position_id,"
-" symbol,"
-" MIN(CASE WHEN entry = 0 THEN time END) AS open_time,"
-" MAX(CASE WHEN entry IN (1, 2, 3) THEN time END) AS close_time,"
-" MIN(CASE WHEN entry = 0 THEN type END) AS direction,"
-" SUM(CASE WHEN entry = 0 THEN volume ELSE 0 END) AS volume_open,"
-" SUM(CASE WHEN entry IN (1, 2, 3) THEN volume ELSE 0 END) AS volume_close,"
-" SUM(CASE WHEN entry = 2 THEN volume ELSE 0 END) AS volume_reversal,"
-" CASE"
-" WHEN SUM(CASE WHEN entry = 0 THEN volume ELSE 0 END) > 0"
-" THEN SUM(CASE WHEN entry = 0 THEN price * volume ELSE 0 END)"
-" / SUM(CASE WHEN entry = 0 THEN volume ELSE 0 END)"
-" END AS open_price,"
-" CASE"
-" WHEN SUM(CASE WHEN entry IN (1, 2, 3) THEN volume ELSE 0 END) > 0"
-" THEN SUM(CASE WHEN entry IN (1, 2, 3) THEN price * volume ELSE 0 END)"
-" / SUM(CASE WHEN entry IN (1, 2, 3) THEN volume ELSE 0 END)"
-" END AS close_price,"
-" SUM(profit) AS total_profit,"
-" SUM(CASE WHEN entry = 2 THEN 1 ELSE 0 END) AS reversal_count,"
-" COUNT(*) AS deals_count"
-" FROM history_deals"
-f" WHERE type IN {_TRADE_DEAL_TYPES_SQL} AND position_id != 0"
-" GROUP BY position_id, symbol"
-" HAVING SUM(CASE WHEN entry IN (1, 2, 3) THEN 1 ELSE 0 END) > 0",
-)
-returnTrue
+1289
+1290
defcreate_positions_reconstructed_view(
+conn:sqlite3.Connection,
+deals_columns:set[str],
+)->bool:
+"""Create the positions_reconstructed SQLite view derived from history_deals.
+
+ Returns:
+ True if the view was created, False if required columns are missing.
+ """
+ifnot_POSITIONS_VIEW_REQUIRED_COLUMNS.issubset(deals_columns):
+missing=", ".join(sorted(_POSITIONS_VIEW_REQUIRED_COLUMNS-deals_columns))
+logger.warning(
+"Skipping positions_reconstructed view: history_deals missing columns: %s",
+missing,
+)
+returnFalse
+conn.execute("DROP VIEW IF EXISTS positions_reconstructed")
+conn.execute(
+"CREATE VIEW positions_reconstructed AS"# noqa: S608
+" SELECT"
+" position_id,"
+" symbol,"
+" MIN(CASE WHEN entry = 0 THEN time END) AS open_time,"
+" MAX(CASE WHEN entry IN (1, 2, 3) THEN time END) AS close_time,"
+" MIN(CASE WHEN entry = 0 THEN type END) AS direction,"
+" SUM(CASE WHEN entry = 0 THEN volume ELSE 0 END) AS volume_open,"
+" SUM(CASE WHEN entry IN (1, 2, 3) THEN volume ELSE 0 END) AS volume_close,"
+" SUM(CASE WHEN entry = 2 THEN volume ELSE 0 END) AS volume_reversal,"
+" CASE"
+" WHEN SUM(CASE WHEN entry = 0 THEN volume ELSE 0 END) > 0"
+" THEN SUM(CASE WHEN entry = 0 THEN price * volume ELSE 0 END)"
+" / SUM(CASE WHEN entry = 0 THEN volume ELSE 0 END)"
+" END AS open_price,"
+" CASE"
+" WHEN SUM(CASE WHEN entry IN (1, 2, 3) THEN volume ELSE 0 END) > 0"
+" THEN SUM(CASE WHEN entry IN (1, 2, 3) THEN price * volume ELSE 0 END)"
+" / SUM(CASE WHEN entry IN (1, 2, 3) THEN volume ELSE 0 END)"
+" END AS close_price,"
+" SUM(profit) AS total_profit,"
+" SUM(CASE WHEN entry = 2 THEN 1 ELSE 0 END) AS reversal_count,"
+" COUNT(*) AS deals_count"
+" FROM history_deals"
+f" WHERE type IN {_TRADE_DEAL_TYPES_SQL} AND position_id != 0"
+" GROUP BY position_id, symbol"
+" HAVING SUM(CASE WHEN entry IN (1, 2, 3) THEN 1 ELSE 0 END) > 0",
+)
+returnTrue
@@ -1379,8 +1399,7 @@ a symbol such as EURUSD_M1 cannot collide with EURUSDSource code in mt5cli/history.py
-
defdrop_duplicates_in_table(
-cursor:sqlite3.Cursor,
-table:str,
-ids:list[str],
-*,
-keep:Literal["first","last"]="last",
-scope_where:str|None=None,
-scope_params:tuple[object,...]=(),
-)->None:
-"""Remove duplicate rows, keeping the first or last ROWID per key group.
-
- Raises:
- ValueError: If the table or column names are invalid.
- """
-ifnottable.isidentifier():
-msg=f"Invalid table name: {table}"
-raiseValueError(msg)
-ifinvalid:={columnforcolumninidsifnotcolumn.isidentifier()}:
-msg=f"Invalid column names: {', '.join(sorted(invalid))}"
-raiseValueError(msg)
-ids_csv=", ".join(f'"{column}"'forcolumninids)
-rowid_selector="MIN"ifkeep=="first"else"MAX"
-ifscope_where:
-delete_sql=(
-f"DELETE FROM {table} WHERE {scope_where} AND ROWID NOT IN"# noqa: S608
-f" (SELECT {rowid_selector}(ROWID) FROM {table} WHERE {scope_where}"
-f" GROUP BY {ids_csv})"
-)
-cursor.execute(delete_sql,scope_params+scope_params)
-return
-cursor.execute(
-f"DELETE FROM {table} WHERE ROWID NOT IN"# noqa: S608
-f" (SELECT {rowid_selector}(ROWID) FROM {table} GROUP BY {ids_csv})",
-)
+1055
+1056
defdrop_duplicates_in_table(
+cursor:sqlite3.Cursor,
+table:str,
+ids:list[str],
+*,
+keep:Literal["first","last"]="last",
+scope_where:str|None=None,
+scope_params:tuple[object,...]=(),
+)->None:
+"""Remove duplicate rows, keeping the first or last ROWID per key group.
+
+ Raises:
+ ValueError: If the table or column names are invalid.
+ """
+ifnottable.isidentifier():
+msg=f"Invalid table name: {table}"
+raiseValueError(msg)
+ifinvalid:={columnforcolumninidsifnotcolumn.isidentifier()}:
+msg=f"Invalid column names: {', '.join(sorted(invalid))}"
+raiseValueError(msg)
+ids_csv=", ".join(f'"{column}"'forcolumninids)
+rowid_selector="MIN"ifkeep=="first"else"MAX"
+ifscope_where:
+delete_sql=(
+f"DELETE FROM {table} WHERE {scope_where} AND ROWID NOT IN"# noqa: S608
+f" (SELECT {rowid_selector}(ROWID) FROM {table} WHERE {scope_where}"
+f" GROUP BY {ids_csv})"
+)
+cursor.execute(delete_sql,scope_params+scope_params)
+return
+cursor.execute(
+f"DELETE FROM {table} WHERE ROWID NOT IN"# noqa: S608
+f" (SELECT {rowid_selector}(ROWID) FROM {table} GROUP BY {ids_csv})",
+)
@@ -1782,8 +1802,7 @@ completed bars. Empty frames and single-row frames return empty results.
Source code in mt5cli/history.py
-
defdrop_forming_rate_bar(df_rate:pd.DataFrame)->pd.DataFrame:
-"""Return closed bars from chronologically ordered MT5 rate data.
-
- MetaTrader 5 ``copy_rates_from_pos(start_pos=0)`` includes the still-forming
- current bar as the last row. Slice it off so downstream logic only sees
- completed bars. Empty frames and single-row frames return empty results.
-
- Args:
- df_rate: Rate data ordered oldest-to-newest with the forming bar last.
-
- Returns:
- A new DataFrame with all rows except the last. Index and columns are
- preserved. The input frame is not modified.
- """
-returndf_rate.iloc[:-1].copy()
+123
+124
defdrop_forming_rate_bar(df_rate:pd.DataFrame)->pd.DataFrame:
+"""Return closed bars from chronologically ordered MT5 rate data.
+
+ MetaTrader 5 ``copy_rates_from_pos(start_pos=0)`` includes the still-forming
+ current bar as the last row. Slice it off so downstream logic only sees
+ completed bars. Empty frames and single-row frames return empty results.
+
+ Args:
+ df_rate: Rate data ordered oldest-to-newest with the forming bar last.
+
+ Returns:
+ A new DataFrame with all rows except the last. Index and columns are
+ preserved. The input frame is not modified.
+ """
+returndf_rate.iloc[:-1].copy()
@@ -1835,21 +1855,21 @@ completed bars. Empty frames and single-row frames return empty results.
Source code in mt5cli/history.py
-
defdrop_rate_compatibility_views(conn:sqlite3.Connection)->None:
-"""Drop all mt5cli-managed ``rate_*`` compatibility views."""
-rows=conn.execute(
-"SELECT name FROM sqlite_master WHERE type = 'view' AND name GLOB 'rate_*'",
-).fetchall()
-for(view_name,)inrows:
-quoted_view_name=quote_sqlite_identifier(str(view_name))
-conn.execute(f"DROP VIEW IF EXISTS {quoted_view_name}")
+1299
+1300
defdrop_rate_compatibility_views(conn:sqlite3.Connection)->None:
+"""Drop all mt5cli-managed ``rate_*`` compatibility views."""
+rows=conn.execute(
+"SELECT name FROM sqlite_master WHERE type = 'view' AND name GLOB 'rate_*'",
+).fetchall()
+for(view_name,)inrows:
+quoted_view_name=quote_sqlite_identifier(str(view_name))
+conn.execute(f"DROP VIEW IF EXISTS {quoted_view_name}")
@@ -1912,8 +1932,7 @@ completed bars. Empty frames and single-row frames return empty results.
Source code in mt5cli/history.py
-
deffilter_incremental_history_deals_frame(
-frame:pd.DataFrame,
-symbols:Sequence[str],
-start_by_symbol:dict[str,datetime],
-account_event_start:datetime,
-)->pd.DataFrame:
-"""Filter incrementally fetched history_deals by symbol and event start times.
-
- Returns:
- Rows for selected symbols at or after each symbol start, plus account
- events at or after ``account_event_start``.
- """
-ifframe.empty:
-returnframe.copy()
-parsed_times=_frame_parsed_times(frame)
-time_valid=parsed_times.notna()
-account_event_mask=_history_deals_account_event_mask(frame)
-account_keep=account_event_mask&(parsed_times>=account_event_start)
-trade_keep=pd.Series(data=False,index=frame.index)
-if"symbol"inframe.columns:
-forsymbolinsymbols:
-trade_keep|=(
-(frame["symbol"]==symbol)
-&(parsed_times>=start_by_symbol[symbol])
-&~account_event_mask
-)
-keep=(account_keep|trade_keep)&time_valid
-returnframe.loc[keep].copy()
+1200
+1201
deffilter_incremental_history_deals_frame(
+frame:pd.DataFrame,
+symbols:Sequence[str],
+start_by_symbol:dict[str,datetime],
+account_event_start:datetime,
+)->pd.DataFrame:
+"""Filter incrementally fetched history_deals by symbol and event start times.
+
+ Returns:
+ Rows for selected symbols at or after each symbol start, plus account
+ events at or after ``account_event_start``.
+ """
+ifframe.empty:
+returnframe.copy()
+parsed_times=_frame_parsed_times(frame)
+time_valid=parsed_times.notna()
+account_event_mask=_history_deals_account_event_mask(frame)
+account_keep=account_event_mask&(parsed_times>=account_event_start)
+trade_keep=pd.Series(data=False,index=frame.index)
+if"symbol"inframe.columns:
+forsymbolinsymbols:
+trade_keep|=(
+(frame["symbol"]==symbol)
+&(parsed_times>=start_by_symbol[symbol])
+&~account_event_mask
+)
+keep=(account_keep|trade_keep)&time_valid
+returnframe.loc[keep].copy()
@@ -2019,8 +2039,7 @@ completed bars. Empty frames and single-row frames return empty results.
Source code in mt5cli/history.py
-
defget_history_deals_account_event_start_datetime(
-conn:sqlite3.Connection,
-*,
-fallback_start:datetime,
-)->datetime:
-"""Return the next update start for account-level history_deals rows."""
-table=Dataset.history_deals.table_name
-columns=get_table_columns(conn,table)
-if"time"notincolumns:
-returnfallback_start
-if"type"incolumns:
-where_clause=f"type NOT IN {_TRADE_DEAL_TYPES_SQL}"
-elif"symbol"incolumns:
-where_clause="symbol IS NULL OR symbol = ''"
-else:
-returnfallback_start
-row=conn.execute(
-f"SELECT MAX(time) FROM {table} WHERE {where_clause}",# noqa: S608
-).fetchone()
-parsed=parse_sqlite_timestamp(row[0]ifrowelseNone)
-returnparsedifparsedisnotNoneelsefallback_start
+831
+832
defget_history_deals_account_event_start_datetime(
+conn:sqlite3.Connection,
+*,
+fallback_start:datetime,
+)->datetime:
+"""Return the next update start for account-level history_deals rows."""
+table=Dataset.history_deals.table_name
+columns=get_table_columns(conn,table)
+if"time"notincolumns:
+returnfallback_start
+if"type"incolumns:
+where_clause=f"type NOT IN {_TRADE_DEAL_TYPES_SQL}"
+elif"symbol"incolumns:
+where_clause="symbol IS NULL OR symbol = ''"
+else:
+returnfallback_start
+row=conn.execute(
+f"SELECT MAX(time) FROM {table} WHERE {where_clause}",# noqa: S608
+).fetchone()
+parsed=parse_sqlite_timestamp(row[0]ifrowelseNone)
+returnparsedifparsedisnotNoneelsefallback_start
@@ -2152,8 +2172,7 @@ completed bars. Empty frames and single-row frames return empty results.
Source code in mt5cli/history.py
-
defload_rate_data_from_connection(
-connection:sqlite3.Connection,
-table:str,
-count:int|None=None,
-)->pd.DataFrame:
-"""Load rate-like data from a SQLite table or view.
-
- Args:
- connection: Open SQLite connection.
- table: Source table or view name.
- count: Optional number of most recent rows to load.
-
- Returns:
- DataFrame indexed by ascending ``time``.
-
- Raises:
- ValueError: If inputs, schema, timestamps are invalid, or the table
- or view contains no rows.
- """
-table_name=_validate_rate_load_request(table,count)
-columns=get_table_columns(connection,table_name)
-_ensure_rate_columns(columns,table_name)
-quoted_table=quote_sqlite_identifier(table_name)
-ifcountisNone:
-frame=cast(
-"pd.DataFrame",
-pd.read_sql_query(# type: ignore[reportUnknownMemberType]
-f"SELECT * FROM {quoted_table} ORDER BY time ASC",# noqa: S608
-connection,
-),
-)
-else:
-frame=cast(
-"pd.DataFrame",
-pd.read_sql_query(# type: ignore[reportUnknownMemberType]
-f"SELECT * FROM {quoted_table} ORDER BY time DESC LIMIT ?",# noqa: S608
-connection,
-params=(count,),
-),
-)
-ifframe.empty:
-msg=f"SQLite table or view {table_name!r} contains no rows."
-raiseValueError(msg)
-return_parse_rate_time_index(frame,table_name)
+276
+277
defload_rate_data_from_connection(
+connection:sqlite3.Connection,
+table:str,
+count:int|None=None,
+)->pd.DataFrame:
+"""Load rate-like data from a SQLite table or view.
+
+ Args:
+ connection: Open SQLite connection.
+ table: Source table or view name.
+ count: Optional number of most recent rows to load.
+
+ Returns:
+ DataFrame indexed by ascending ``time``.
+
+ Raises:
+ ValueError: If inputs, schema, timestamps are invalid, or the table
+ or view contains no rows.
+ """
+table_name=_validate_rate_load_request(table,count)
+columns=get_table_columns(connection,table_name)
+_ensure_rate_columns(columns,table_name)
+quoted_table=quote_sqlite_identifier(table_name)
+ifcountisNone:
+frame=cast(
+"pd.DataFrame",
+pd.read_sql_query(# type: ignore[reportUnknownMemberType]
+f"SELECT * FROM {quoted_table} ORDER BY time ASC",# noqa: S608
+connection,
+),
+)
+else:
+frame=cast(
+"pd.DataFrame",
+pd.read_sql_query(# type: ignore[reportUnknownMemberType]
+f"SELECT * FROM {quoted_table} ORDER BY time DESC LIMIT ?",# noqa: S608
+connection,
+params=(count,),
+),
+)
+ifframe.empty:
+msg=f"SQLite table or view {table_name!r} contains no rows."
+raiseValueError(msg)
+return_parse_rate_time_index(frame,table_name)
@@ -3002,8 +3022,7 @@ with symbol=None for each granularity instead of raising.
Source code in mt5cli/history.py
-
727728729730
@@ -3048,53 +3067,54 @@ with symbol=None for each granularity instead of raising.
769770771
-772
defload_rate_series_by_granularity(
-conn_or_path:SqliteConnOrPath,
-symbols:Sequence[str],
-granularities:Sequence[int|str],
-count:int,
-*,
-explicit_tables:Sequence[str]|None=None,
-allow_missing_symbol:bool=False,
-)->dict[tuple[str|None,str],pd.DataFrame]:
-"""Load rate series keyed by symbol and string granularity name.
-
- Builds targets with :func:`build_rate_targets` and loads them with
- :func:`load_rate_series_from_sqlite`, then rekeys the result by granularity
- name (for example ``M1``) instead of the integer timeframe to reduce
- downstream boilerplate.
-
- Args:
- conn_or_path: SQLite database path or open connection.
- symbols: MT5 symbol names. May be empty when ``allow_missing_symbol``.
- granularities: MT5 timeframes as integers or names (for example ``M1``).
- count: Number of most recent rows to load per series.
- explicit_tables: Optional explicit table or view names matching the
- built targets in row-major order. Required when symbols are omitted.
- allow_missing_symbol: When True and ``symbols`` is empty, build targets
- with ``symbol=None`` for each granularity instead of raising.
-
- Returns:
- Mapping keyed by ``(symbol | None, granularity_name)`` to each rate
- DataFrame. Propagates ``ValueError`` (via :func:`build_rate_targets` and
- :func:`load_rate_series_from_sqlite`) when inputs are empty or invalid,
- table resolution fails, or duplicate targets are present.
- """
-targets=build_rate_targets(
-symbols,
-granularities,
-allow_missing_symbol=allow_missing_symbol,
-)
-series=load_rate_series_from_sqlite(
-conn_or_path,
-targets,
-count,
-explicit_tables=explicit_tables,
-)
-return{
-(symbol,resolve_granularity_name(timeframe)):frame
-for(symbol,timeframe),frameinseries.items()
-}
+772
+773
defload_rate_series_by_granularity(
+conn_or_path:SqliteConnOrPath,
+symbols:Sequence[str],
+granularities:Sequence[int|str],
+count:int,
+*,
+explicit_tables:Sequence[str]|None=None,
+allow_missing_symbol:bool=False,
+)->dict[tuple[str|None,str],pd.DataFrame]:
+"""Load rate series keyed by symbol and string granularity name.
+
+ Builds targets with :func:`build_rate_targets` and loads them with
+ :func:`load_rate_series_from_sqlite`, then rekeys the result by granularity
+ name (for example ``M1``) instead of the integer timeframe to reduce
+ downstream boilerplate.
+
+ Args:
+ conn_or_path: SQLite database path or open connection.
+ symbols: MT5 symbol names. May be empty when ``allow_missing_symbol``.
+ granularities: MT5 timeframes as integers or names (for example ``M1``).
+ count: Number of most recent rows to load per series.
+ explicit_tables: Optional explicit table or view names matching the
+ built targets in row-major order. Required when symbols are omitted.
+ allow_missing_symbol: When True and ``symbols`` is empty, build targets
+ with ``symbol=None`` for each granularity instead of raising.
+
+ Returns:
+ Mapping keyed by ``(symbol | None, granularity_name)`` to each rate
+ DataFrame. Propagates ``ValueError`` (via :func:`build_rate_targets` and
+ :func:`load_rate_series_from_sqlite`) when inputs are empty or invalid,
+ table resolution fails, or duplicate targets are present.
+ """
+targets=build_rate_targets(
+symbols,
+granularities,
+allow_missing_symbol=allow_missing_symbol,
+)
+series=load_rate_series_from_sqlite(
+conn_or_path,
+targets,
+count,
+explicit_tables=explicit_tables,
+)
+return{
+(symbol,resolve_granularity_name(timeframe)):frame
+for(symbol,timeframe),frameinseries.items()
+}
@@ -3274,8 +3294,7 @@ fails.
Source code in mt5cli/history.py
-
defload_rate_series_from_sqlite(
-conn_or_path:SqliteConnOrPath,
-targets:Sequence[RateTarget],
-count:int,
-explicit_tables:Sequence[str]|None=None,
-)->dict[tuple[str|None,int],pd.DataFrame]:
-"""Load multiple rate series from a SQLite database.
-
- Args:
- conn_or_path: SQLite database path or open connection.
- targets: Rate targets to load. Each ``(symbol, timeframe_int)`` pair
- must be unique.
- count: Number of most recent rows to load per series.
- explicit_tables: Optional explicit table or view names matching targets.
- When omitted, managed ``rate_*`` compatibility views must already
- exist in the database.
-
- Returns:
- Mapping keyed by ``(symbol, timeframe_int)`` to each rate DataFrame.
-
- Raises:
- ValueError: If ``count`` is not positive, targets are empty, duplicate
- ``(symbol, timeframe_int)`` pairs are present, or table resolution
- fails.
- """
-ifcount<=0:
-msg="count must be positive."
-raiseValueError(msg)
-target_list=list(targets)
-ifnottarget_list:
-msg="At least one rate target is required."
-raiseValueError(msg)
-ifexplicit_tablesisNoneandany(target.symbolisNonefortargetintarget_list):
-msg=(
-"Cannot resolve a rate table for a target without a symbol; "
-"provide explicit_tables."
-)
-raiseValueError(msg)
-seen_keys:set[tuple[str|None,int]]=set()
-fortargetintarget_list:
-key=(target.symbol,target.timeframe_int)
-ifkeyinseen_keys:
-symbol_repr=repr(target.symbol)
-msg=f"Duplicate rate target: ({symbol_repr}, {target.timeframe_int})"
-raiseValueError(msg)
-seen_keys.add(key)
-tables=(
-resolve_rate_tables(None,target_list,explicit_tables)
-ifexplicit_tablesisnotNone
-elseNone
-)
-conn,should_close=_open_existing_sqlite_database(conn_or_path)
-try:
-resolved_tables=tablesorresolve_rate_tables(
-conn,
-target_list,
-require_existing=True,
-)
-return{
-(target.symbol,target.timeframe_int):load_rate_data_from_connection(
-conn,
-table,
-count=count,
-)
-fortarget,tableinzip(target_list,resolved_tables,strict=True)
-}
-finally:
-ifshould_close:
-conn.close()
+