# Utils Module

## mt5cli.utils

Utility constants, types, and functions for the mt5cli package.

### DATETIME_TYPE `module-attribute`

    DATETIME_TYPE = _DateTimeType()

### REQUEST_TYPE `module-attribute`

    REQUEST_TYPE = _RequestType()

### TICK_FLAGS_TYPE `module-attribute`

    TICK_FLAGS_TYPE = _TickFlagsType()

### TIMEFRAME_NAMES `module-attribute`

    TIMEFRAME_NAMES: tuple[str, ...] = tuple(
        name
        for name in TIMEFRAME_MAP
        if not startswith("TIMEFRAME_")
    )

### TIMEFRAME_TYPE `module-attribute`

    TIMEFRAME_TYPE = _TimeframeType()

### Dataset

Bases: `StrEnum`

Datasets supported by the `collect-history` command.

#### history_deals `class-attribute` `instance-attribute`

    history_deals = 'history-deals'

#### history_orders `class-attribute` `instance-attribute`

    history_orders = 'history-orders'

#### rates `class-attribute` `instance-attribute`

    rates = 'rates'

#### table_name `property`

    table_name: str

Return the SQLite table name for this dataset.

#### ticks `class-attribute` `instance-attribute`

    ticks = 'ticks'

### IfExists

Bases: `StrEnum`

SQLite table conflict behavior for the `collect-history` command.

#### APPEND `class-attribute` `instance-attribute`

    APPEND = 'append'

#### FAIL `class-attribute` `instance-attribute`

    FAIL = 'fail'

#### REPLACE `class-attribute` `instance-attribute`

    REPLACE = 'replace'

### LogLevel

Bases: `StrEnum`

Logging verbosity levels.

#### DEBUG `class-attribute` `instance-attribute`

    DEBUG = 'DEBUG'

#### ERROR `class-attribute` `instance-attribute`

    ERROR = 'ERROR'

#### INFO `class-attribute` `instance-attribute`

    INFO = 'INFO'

#### WARNING `class-attribute` `instance-attribute`

    WARNING = 'WARNING'

### OutputFormat

Bases: `StrEnum`

Supported output file formats.

#### csv `class-attribute` `instance-attribute`

    csv = 'csv'

#### json `class-attribute` `instance-attribute`

    json = 'json'

#### parquet `class-attribute` `instance-attribute`

    parquet = 'parquet'

#### sqlite3 `class-attribute` `instance-attribute`

    sqlite3 = 'sqlite3'

### coerce_login

    coerce_login(login: int | str | None) -> int | None

Coerce a login value to int, treating empty strings as unset.

Returns:

Type | Description
--- | ---
`int | None` | Integer login, or None when unset or an empty string.

Source code in `mt5cli/utils.py`

    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253

    def coerce_login(login: int | str | None) -> int | None:
        """Coerce a login value to int, treating empty strings as unset.

        Returns:
            Integer login, or None when unset or an empty string.
        """
        if login is None or isinstance(login, int):
            return login
        text = login.strip()
        if not text:
            return None
        return int(text)

### detect_format

    detect_format(
        output_path: Path, explicit_format: str | None = None
    ) -> str

Detect the output format from a file extension or explicit format string.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`output_path` | `Path` | Path to the output file. | required
`explicit_format` | `str | None` | Explicitly specified format, if any. | `None`

Returns:

Type | Description
--- | ---
`str` | The detected format string.

Raises:

Type | Description
--- | ---
`ValueError` | If the format cannot be determined.

Source code in `mt5cli/utils.py`

    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239

    def detect_format(
        output_path: Path,
        explicit_format: str | None = None,
    ) -> str:
        """Detect the output format from a file extension or explicit format string.

        Args:
            output_path: Path to the output file.
            explicit_format: Explicitly specified format, if any.

        Returns:
            The detected format string.

        Raises:
            ValueError: If the format cannot be determined.
        """
        if explicit_format is not None:
            return explicit_format
        suffix = output_path.suffix.lower()
        if suffix in _FORMAT_EXTENSIONS:
            return _FORMAT_EXTENSIONS[suffix]
        msg = (
            f"Cannot detect format from extension '{suffix}'."
            " Use --format to specify the output format."
        )
        raise ValueError(msg)

### export_dataframe

    export_dataframe(
        df: DataFrame,
        output_path: Path,
        output_format: str,
        table_name: str = "data",
    ) -> None

Export a pandas DataFrame to the specified file format.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`df` | `DataFrame` | DataFrame to export. | required
`output_path` | `Path` | Path to the output file. | required
`output_format` | `str` | Output format (csv, json, parquet, or sqlite3). | required
`table_name` | `str` | Table name for SQLite3 output. | `'data'`

Raises:

Type | Description
--- | ---
`ImportError` | If the parquet format is requested but pyarrow is not installed.
`ValueError` | If the output format is not supported.

Source code in `mt5cli/utils.py`

    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347

    def export_dataframe(
        df: pd.DataFrame,
        output_path: Path,
        output_format: str,
        table_name: str = "data",
    ) -> None:
        """Export a pandas DataFrame to the specified file format.

        Args:
            df: DataFrame to export.
            output_path: Path to the output file.
            output_format: Output format (csv, json, parquet, or sqlite3).
            table_name: Table name for SQLite3 output.

        Raises:
            ImportError: If the parquet format is requested but pyarrow is not installed.
            ValueError: If the output format is not supported.
        """
        if output_format == "csv":
            df.to_csv(output_path, index=False)
        elif output_format == "json":
            df.to_json(
                output_path,
                orient="records",
                date_format="iso",
                indent=2,
            )
        elif output_format == "parquet":
            try:
                __import__("pyarrow")
            except ImportError as exc:
                msg = (
                    "Parquet export requires the optional dependency pyarrow. "
                    'Install it with: pip install "mt5cli[parquet]"'
                )
                raise ImportError(msg) from exc
            df.to_parquet(output_path, index=False)
        elif output_format == "sqlite3":
            export_dataframe_to_sqlite(
                df,
                output_path,
                table_name,
                if_exists=IfExists.REPLACE,
                index=False,
            )
        else:
            msg = f"Unsupported output format: {output_format}"
            raise ValueError(msg)

### export_dataframe_to_sqlite

    export_dataframe_to_sqlite(
        df: DataFrame,
        output_path: Path,
        table_name: str = "data",
        *,
        if_exists: IfExists = APPEND,
        index: bool = False,
        index_label: str | None = None,
        deduplicate_on: Sequence[str] | None = None,
    ) -> None

Write a DataFrame to SQLite with configurable append and deduplication.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`df` | `DataFrame` | DataFrame to export. | required
`output_path` | `Path` | SQLite database path. | required
`table_name` | `str` | Target table name. | `'data'`
`if_exists` | `IfExists` | Conflict behavior when the table already exists. | `APPEND`
`index` | `bool` | Whether to write the DataFrame index as a column. | `False`
`index_label` | `str | None` | Column name for the index when `index=True`. | `None`
`deduplicate_on` | `Sequence[str] | None` | Optional key columns to deduplicate after writing, keeping the latest `ROWID` per key group. Deduplication scans the full table, so repeated appends cost O(table size); index the key columns when appending frequently. | `None`

Source code in `mt5cli/utils.py`

    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297

    def export_dataframe_to_sqlite(
        df: pd.DataFrame,
        output_path: Path,
        table_name: str = "data",
        *,
        if_exists: IfExists = IfExists.APPEND,
        index: bool = False,
        index_label: str | None = None,
        deduplicate_on: Sequence[str] | None = None,
    ) -> None:
        """Write a DataFrame to SQLite with configurable append and deduplication.

        Args:
            df: DataFrame to export.
            output_path: SQLite database path.
            table_name: Target table name.
            if_exists: Conflict behavior when the table already exists.
            index: Whether to write the DataFrame index as a column.
            index_label: Column name for the index when ``index=True``.
            deduplicate_on: Optional key columns to deduplicate after writing,
                keeping the latest ``ROWID`` per key group. Deduplication scans the
                full table, so repeated appends cost O(table size); index the key
                columns when appending frequently.
        """
        with sqlite3.connect(output_path) as conn:
            df.to_sql(  # type: ignore[reportUnknownMemberType]
                table_name,
                conn,
                if_exists=if_exists.value,
                index=index,
                index_label=index_label,
            )
            if deduplicate_on:
                from .history import drop_duplicates_in_table  # noqa: PLC0415

                drop_duplicates_in_table(
                    conn.cursor(),
                    table_name,
                    list(deduplicate_on),
                    keep="last",
                )
                conn.commit()

### parse_datetime

    parse_datetime(value: str) -> datetime

Parse an ISO 8601 datetime string to a timezone-aware datetime.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`value` | `str` | ISO 8601 datetime string (e.g., '2024-01-01' or '2024-01-01T12:00:00+00:00'). | required

Returns:

Type | Description
--- | ---
`datetime` | Parsed datetime with UTC timezone if no timezone is specified.

Raises:

Type | Description
--- | ---
`ValueError` | If the string cannot be parsed.

Source code in `mt5cli/utils.py`

    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370

    def parse_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)
        except ValueError:
            msg = f"Invalid datetime format: '{value}'. Use ISO 8601 format."
            raise ValueError(msg) from None
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=UTC)
        return dt

### parse_request

    parse_request(value: str) -> dict[str, Any]

Parse a JSON-formatted order request string or file reference.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`value` | `str` | JSON object string, or '@path' to read JSON from a file. | required

Returns:

Type | Description
--- | ---
`dict[str, Any]` | Parsed request dictionary.

Raises:

Type | Description
--- | ---
`ValueError` | If the request file cannot be read or the value is not a JSON object.

Source code in `mt5cli/utils.py`

    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455

    def parse_request(value: str) -> dict[str, Any]:
        """Parse a JSON-formatted order request string or file reference.

        Args:
            value: JSON object string, or '@path' to read JSON from a file.

        Returns:
            Parsed request dictionary.

        Raises:
            ValueError: If the request file cannot be read or the value is not a
                JSON object.
        """
        if value.startswith("@"):
            path = Path(value[1:])
            try:
                text = path.read_text(encoding="utf-8")
            except (OSError, UnicodeDecodeError) as exc:
                msg = f"Failed to read JSON request file '{path}': {exc}"
                raise ValueError(msg) from exc
        else:
            text = value
        try:
            parsed: object = json.loads(text)
        except json.JSONDecodeError as exc:
            msg = f"Invalid JSON request: {exc}"
            raise ValueError(msg) from exc
        if not _is_request_dict(parsed):
            msg = "Order request must be a JSON object."
            raise ValueError(msg)
        return parsed

### parse_tick_flags

    parse_tick_flags(value: object) -> int

Parse tick flags string or integer value.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`value` | `object` | Tick flag name (ALL, INFO, TRADE, COPY_TICKS_*) or integer value. | required

Returns:

Type | Description
--- | ---
`int` | Integer tick flag value compatible with MetaTrader 5 `COPY_TICKS_*`.

Raises:

Type | Description
--- | ---
`ValueError` | If the flag is invalid.

Source code in `mt5cli/utils.py`

    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418

    def parse_tick_flags(value: object) -> int:
        """Parse tick flags string or integer value.

        Args:
            value: Tick flag name (ALL, INFO, TRADE, COPY_TICKS_*) or integer value.

        Returns:
            Integer tick flag value compatible with MetaTrader 5 ``COPY_TICKS_*``.

        Raises:
            ValueError: If the flag is invalid.
        """
        try:
            return _parse_copy_ticks(value)
        except ValueError:
            display = value if isinstance(value, str) else repr(value)
            valid = ", ".join(_TICK_FLAG_NAMES)
            msg = (
                f"Invalid tick flags: '{display}'. "
                f"Use one of: {valid}, or a supported integer."
            )
            raise ValueError(msg) from None

### parse_timeframe

    parse_timeframe(value: object) -> int

Parse a timeframe string or integer value.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`value` | `object` | Timeframe name (e.g., 'M1', 'H1', 'D1') or integer value. | required

Returns:

Type | Description
--- | ---
`int` | Integer timeframe value.

Raises:

Type | Description
--- | ---
`ValueError` | If the timeframe is invalid.

Source code in `mt5cli/utils.py`

    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394

    def parse_timeframe(value: object) -> int:
        """Parse a timeframe string or integer value.

        Args:
            value: Timeframe name (e.g., 'M1', 'H1', 'D1') or integer value.

        Returns:
            Integer timeframe value.

        Raises:
            ValueError: If the timeframe is invalid.
        """
        try:
            return _parse_timeframe(value)
        except ValueError:
            display = value if isinstance(value, str) else repr(value)
            valid = ", ".join(TIMEFRAME_NAMES)
            msg = (
                f"Invalid timeframe: '{display}'. "
                f"Use one of: {valid}, or a supported integer."
            )
            raise ValueError(msg) from None

#### Search

From here you can search these documents. Enter your search terms below.

#### Keyboard Shortcuts

Keys | Action
--- | ---
`?` | Open this help
`n` | Next page
`p` | Previous page
`s` | Search