Skip to content

πŸ“Š Get Closed Positions HistoryΒΆ

Request: retrieve historical closed positions for a specified time period with pagination support. Returns complete position lifecycle data including entry/exit prices, profit/loss, and trading costs.

API Information:

  • SDK wrapper: MT5Account.positionsHistory(...) (from package io.metarpc.mt5)
  • gRPC service: mt5_term_api.AccountHelper
  • Proto definition: PositionsHistory (defined in mt5-term-api-account-helper.proto)

RPCΒΆ

  • Service: mt5_term_api.AccountHelper
  • Method: PositionsHistory(PositionsHistoryRequest) β†’ PositionsHistoryReply
  • Low‑level client (generated): AccountHelperGrpc.AccountHelperBlockingStub.positionsHistory(request)
  • SDK wrapper (high-level):
package io.metarpc.mt5;

public class MT5Account {
    /**
     * Retrieves closed positions history for the specified time period.
     * Supports pagination and different sorting options.
     * Returns complete position lifecycle including entry/exit prices and profit/loss.
     *
     * @param sortType Sort order for results
     * @param from Optional start time (position open time), null for no limit
     * @param to Optional end time (position open time), null for no limit
     * @param pageNumber Optional page number (0-based), null for default (0)
     * @param itemsPerPage Optional items per page, null for default (all items)
     * @return Reply containing paginated closed positions data
     * @throws ApiExceptionMT5 if the call fails or connection is lost
     */
    public Mt5TermApiAccountHelper.PositionsHistoryReply positionsHistory(
        Mt5TermApiAccountHelper.AH_ENUM_POSITIONS_HISTORY_SORT_TYPE sortType,
        com.google.protobuf.Timestamp from,
        com.google.protobuf.Timestamp to,
        Integer pageNumber,
        Integer itemsPerPage
    ) throws ApiExceptionMT5;
}

Request message: PositionsHistoryRequest { sort_type, position_open_time_from?, position_open_time_to?, page_number?, items_per_page? }

Reply message: PositionsHistoryReply { data: PositionsHistoryData } or { error: Error }


πŸ”½ Input ParametersΒΆ

Parameter Type Required Description
sortType AH_ENUM_POSITIONS_HISTORY_SORT_TYPE βœ… Sorting mode for results
from Timestamp ❌ Start time (position open time); null = no limit
to Timestamp ❌ End time (position open time); null = no limit
pageNumber Integer ❌ Page number (0-based); null = 0
itemsPerPage Integer ❌ Items per page; null = all items

Enum: AH_ENUM_POSITIONS_HISTORY_SORT_TYPEΒΆ

Value Number Description
AH_POSITION_OPEN_TIME_ASC 0 Sort by position open time (ascending)
AH_POSITION_OPEN_TIME_DESC 1 Sort by position open time (descending)
AH_POSITION_TICKET_ASC 2 Sort by position ticket ID (ascending)
AH_POSITION_TICKET_DESC 3 Sort by position ticket ID (descending)

⬆️ Output - PositionsHistoryDataΒΆ

Field Type Description
history_positions List<PositionHistoryInfo> List of closed position records

Access using: reply.getData().getHistoryPositionsList()

Structure: PositionHistoryInfoΒΆ

Field Type Description
index int Index in result list
position_ticket long Position ticket number (unique ID)
order_type AH_ENUM_POSITIONS_HISTORY_ORDER_TYPE Position type (BUY, SELL, etc.)
open_time Timestamp Position opening time
close_time Timestamp Position closing time
volume double Position volume (lots)
open_price double Opening price
close_price double Closing price
stop_loss double Stop loss level
take_profit double Take profit level
market_value double Position market value
commission double Total commission charged
fee double Additional fees
profit double Gross profit/loss (in account currency)
swap double Swap charges (rollover)
comment string Position comment
symbol string Symbol name (e.g., "EURUSD")
magic long Expert Advisor ID (magic number)

Enum: AH_ENUM_POSITIONS_HISTORY_ORDER_TYPEΒΆ

Value Number Description
AH_ORDER_TYPE_BUY 0 Buy position (long)
AH_ORDER_TYPE_SELL 1 Sell position (short)
AH_ORDER_TYPE_BUY_LIMIT 2 Buy Limit order
AH_ORDER_TYPE_SELL_LIMIT 3 Sell Limit order
AH_ORDER_TYPE_BUY_STOP 4 Buy Stop order
AH_ORDER_TYPE_SELL_STOP 5 Sell Stop order
AH_ORDER_TYPE_BUY_STOP_LIMIT 6 Buy Stop Limit order
AH_ORDER_TYPE_SELL_STOP_LIMIT 7 Sell Stop Limit order
AH_ORDER_TYPE_CLOSE_BY 8 Position closed by opposite position

πŸ’¬ Just the essentialsΒΆ

  • What it is. RPC to retrieve closed positions history with complete lifecycle data.
  • Why you need it. Analyze trading performance, calculate metrics, generate reports.
  • Complete data. Includes entry/exit prices, profit/loss, costs, and timestamps.
  • Flexible filtering. Optional time range filtering by position open time.
  • Pagination. Handles large datasets efficiently.
  • Net profit. Calculate using: profit - commission - fee + swap.

🎯 Purpose¢

Use this method when you need to:

  • Analyze trading performance and strategy effectiveness.
  • Calculate trading statistics (win rate, profit factor, etc.).
  • Generate profit/loss reports for specific periods.
  • Track commission and swap costs.
  • Audit closed positions for compliance.
  • Export trading history for external analysis.
  • Monitor Expert Advisor performance by magic number.

🧩 Notes & Tips¢

  • Time filter. from and to filter by position open time, not close time.
  • Optional params. Pass null for from/to to get all positions.
  • Net profit. Calculate as: profit - commission - fee + swap.
  • Pagination. Use pageNumber=null, itemsPerPage=null for all items.
  • Market value. Calculated position value at closure.
  • Swap charges. Overnight rollover costs (can be positive or negative).
  • Magic number. Use to filter positions by Expert Advisor.
  • Auto-reconnect. Uses executeWithReconnect() for reliability.

πŸ”— Usage ExamplesΒΆ

1) Basic closed positions retrieval (last 30 days)ΒΆ

import io.metarpc.mt5.MT5Account;
import io.metarpc.mt5.exceptions.ApiExceptionMT5;
import mt5_term_api.Mt5TermApiAccountHelper;
import com.google.protobuf.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

public class Example {
    public static void main(String[] args) {
        MT5Account account = new MT5Account(12345678, "password");

        try {
            account.connect("demo.mt5server.com", 443, "EURUSD");

            // Get last 30 days of closed positions
            Instant now = Instant.now();
            Instant monthAgo = now.minus(30, ChronoUnit.DAYS);

            Timestamp from = Timestamp.newBuilder()
                .setSeconds(monthAgo.getEpochSecond())
                .build();
            Timestamp to = Timestamp.newBuilder()
                .setSeconds(now.getEpochSecond())
                .build();

            Mt5TermApiAccountHelper.PositionsHistoryReply reply =
                account.positionsHistory(
                    Mt5TermApiAccountHelper.AH_ENUM_POSITIONS_HISTORY_SORT_TYPE.AH_POSITION_OPEN_TIME_DESC,
                    from,
                    to,
                    null,  // All pages
                    null   // All items
                );

            var positions = reply.getData().getHistoryPositionsList();
            System.out.printf("Closed positions: %d%n%n", positions.size());

            for (var position : positions) {
                double netProfit = position.getProfit() -
                                   position.getCommission() -
                                   position.getFee() +
                                   position.getSwap();

                System.out.printf("Position #%d%n", position.getPositionTicket());
                System.out.printf("  Symbol: %s%n", position.getSymbol());
                System.out.printf("  Type: %s%n", position.getOrderType());
                System.out.printf("  Volume: %.2f lots%n", position.getVolume());
                System.out.printf("  Open: %.5f @ %s%n",
                    position.getOpenPrice(),
                    Instant.ofEpochSecond(position.getOpenTime().getSeconds())
                );
                System.out.printf("  Close: %.5f @ %s%n",
                    position.getClosePrice(),
                    Instant.ofEpochSecond(position.getCloseTime().getSeconds())
                );
                System.out.printf("  Profit: %.2f%n", position.getProfit());
                System.out.printf("  Net P/L: %.2f%n%n", netProfit);
            }

        } catch (ApiExceptionMT5 e) {
            System.err.println("Error: " + e.getMessage());
        } finally {
            account.close();
        }
    }
}

2) Calculate trading statisticsΒΆ

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.time.Duration;

public class TradingStatistics {
    public record Stats(
        int totalTrades,
        int winningTrades,
        int losingTrades,
        double winRate,
        double totalProfit,
        double totalLoss,
        double netProfit,
        double largestWin,
        double largestLoss,
        double averageWin,
        double averageLoss,
        double profitFactor,
        double averageHoldingTime,
        double totalCommission,
        double totalSwap
    ) {}

    /**
     * Calculate comprehensive trading statistics
     */
    public static Stats calculate(
            MT5Account account,
            int daysBack) throws ApiExceptionMT5 {

        Instant now = Instant.now();
        Instant start = now.minus(daysBack, ChronoUnit.DAYS);

        Timestamp from = Timestamp.newBuilder()
            .setSeconds(start.getEpochSecond())
            .build();
        Timestamp to = Timestamp.newBuilder()
            .setSeconds(now.getEpochSecond())
            .build();

        var reply = account.positionsHistory(
            Mt5TermApiAccountHelper.AH_ENUM_POSITIONS_HISTORY_SORT_TYPE.AH_POSITION_OPEN_TIME_DESC,
            from, to, null, null
        );

        var positions = reply.getData().getHistoryPositionsList();
        if (positions.isEmpty()) {
            return new Stats(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
        }

        java.util.List<Double> netProfits = new java.util.ArrayList<>();
        double totalCommission = 0;
        double totalSwap = 0;
        long totalHoldingSeconds = 0;

        for (var pos : positions) {
            double netProfit = pos.getProfit() - pos.getCommission() -
                               pos.getFee() + pos.getSwap();
            netProfits.add(netProfit);

            totalCommission += pos.getCommission();
            totalSwap += pos.getSwap();

            // Calculate holding time
            long holdingSec = pos.getCloseTime().getSeconds() -
                              pos.getOpenTime().getSeconds();
            totalHoldingSeconds += holdingSec;
        }

        int totalTrades = positions.size();
        int winningTrades = (int) netProfits.stream().filter(p -> p > 0).count();
        int losingTrades = (int) netProfits.stream().filter(p -> p < 0).count();

        double totalProfit = netProfits.stream()
            .filter(p -> p > 0)
            .mapToDouble(Double::doubleValue)
            .sum();
        double totalLoss = Math.abs(netProfits.stream()
            .filter(p -> p < 0)
            .mapToDouble(Double::doubleValue)
            .sum());
        double netProfit = totalProfit - totalLoss;

        double largestWin = netProfits.stream()
            .filter(p -> p > 0)
            .mapToDouble(Double::doubleValue)
            .max()
            .orElse(0);
        double largestLoss = Math.abs(netProfits.stream()
            .filter(p -> p < 0)
            .mapToDouble(Double::doubleValue)
            .min()
            .orElse(0));

        double averageWin = winningTrades > 0 ? totalProfit / winningTrades : 0;
        double averageLoss = losingTrades > 0 ? totalLoss / losingTrades : 0;

        double winRate = (double) winningTrades / totalTrades * 100;
        double profitFactor = totalLoss > 0 ? totalProfit / totalLoss : 0;

        double averageHoldingTime = (double) totalHoldingSeconds / totalTrades;

        return new Stats(
            totalTrades,
            winningTrades,
            losingTrades,
            winRate,
            totalProfit,
            totalLoss,
            netProfit,
            largestWin,
            largestLoss,
            averageWin,
            averageLoss,
            profitFactor,
            averageHoldingTime,
            totalCommission,
            totalSwap
        );
    }

    /**
     * Print statistics report
     */
    public static void printReport(Stats stats) {
        System.out.println("\n╔════════════════════════════════════════╗");
        System.out.println("β•‘    CLOSED POSITIONS STATISTICS         β•‘");
        System.out.println("╠════════════════════════════════════════╣");
        System.out.printf("β•‘ Total Trades: %-24d β•‘%n", stats.totalTrades());
        System.out.printf("β•‘ Winning Trades: %-22d β•‘%n", stats.winningTrades());
        System.out.printf("β•‘ Losing Trades: %-23d β•‘%n", stats.losingTrades());
        System.out.printf("β•‘ Win Rate: %-27.2f%% β•‘%n", stats.winRate());
        System.out.println("╠════════════════════════════════════════╣");
        System.out.printf("β•‘ Total Profit: $%-23.2f β•‘%n", stats.totalProfit());
        System.out.printf("β•‘ Total Loss: $%-25.2f β•‘%n", stats.totalLoss());
        System.out.printf("β•‘ Net Profit: $%-25.2f β•‘%n", stats.netProfit());
        System.out.println("╠════════════════════════════════════════╣");
        System.out.printf("β•‘ Largest Win: $%-24.2f β•‘%n", stats.largestWin());
        System.out.printf("β•‘ Largest Loss: $%-23.2f β•‘%n", stats.largestLoss());
        System.out.printf("β•‘ Average Win: $%-24.2f β•‘%n", stats.averageWin());
        System.out.printf("β•‘ Average Loss: $%-23.2f β•‘%n", stats.averageLoss());
        System.out.println("╠════════════════════════════════════════╣");
        System.out.printf("β•‘ Profit Factor: %-23.2f β•‘%n", stats.profitFactor());
        System.out.printf("β•‘ Avg Hold Time: %-19.1f hrs β•‘%n",
            stats.averageHoldingTime() / 3600);
        System.out.println("╠════════════════════════════════════════╣");
        System.out.printf("β•‘ Total Commission: $%-19.2f β•‘%n", stats.totalCommission());
        System.out.printf("β•‘ Total Swap: $%-25.2f β•‘%n", stats.totalSwap());
        System.out.println("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•");
    }
}

// Usage
var stats = TradingStatistics.calculate(account, 30);
TradingStatistics.printReport(stats);

3) Filter positions by symbolΒΆ

public class SymbolAnalyzer {
    /**
     * Get closed positions for specific symbol
     */
    public static java.util.List<Mt5TermApiAccountHelper.PositionHistoryInfo> getBySymbol(
            MT5Account account,
            String symbol,
            int daysBack) throws ApiExceptionMT5 {

        Instant now = Instant.now();
        Instant start = now.minus(daysBack, ChronoUnit.DAYS);

        Timestamp from = Timestamp.newBuilder()
            .setSeconds(start.getEpochSecond())
            .build();
        Timestamp to = Timestamp.newBuilder()
            .setSeconds(now.getEpochSecond())
            .build();

        var reply = account.positionsHistory(
            Mt5TermApiAccountHelper.AH_ENUM_POSITIONS_HISTORY_SORT_TYPE.AH_POSITION_OPEN_TIME_DESC,
            from, to, null, null
        );

        return reply.getData().getHistoryPositionsList().stream()
            .filter(pos -> pos.getSymbol().equals(symbol))
            .toList();
    }

    /**
     * Print symbol performance summary
     */
    public static void printSymbolPerformance(
            java.util.List<Mt5TermApiAccountHelper.PositionHistoryInfo> positions,
            String symbol) {

        if (positions.isEmpty()) {
            System.out.printf("No positions found for %s%n", symbol);
            return;
        }

        double totalNetProfit = positions.stream()
            .mapToDouble(p -> p.getProfit() - p.getCommission() -
                              p.getFee() + p.getSwap())
            .sum();

        long wins = positions.stream()
            .filter(p -> (p.getProfit() - p.getCommission() -
                          p.getFee() + p.getSwap()) > 0)
            .count();

        double winRate = (double) wins / positions.size() * 100;

        System.out.printf("\n%s Performance Summary%n", symbol);
        System.out.println("═".repeat(40));
        System.out.printf("Total Positions: %d%n", positions.size());
        System.out.printf("Winning Positions: %d%n", wins);
        System.out.printf("Win Rate: %.2f%%%n", winRate);
        System.out.printf("Net Profit: $%.2f%n", totalNetProfit);
        System.out.printf("Average Per Trade: $%.2f%n",
            totalNetProfit / positions.size());
    }
}

// Usage
var eurusdPositions = SymbolAnalyzer.getBySymbol(account, "EURUSD", 30);
SymbolAnalyzer.printSymbolPerformance(eurusdPositions, "EURUSD");

4) Analyze positions by magic number (EA tracking)ΒΆ

public class ExpertAdvisorAnalyzer {
    /**
     * Get positions by Expert Advisor (magic number)
     */
    public static java.util.List<Mt5TermApiAccountHelper.PositionHistoryInfo> getByMagic(
            MT5Account account,
            long magicNumber,
            int daysBack) throws ApiExceptionMT5 {

        Instant now = Instant.now();
        Instant start = now.minus(daysBack, ChronoUnit.DAYS);

        Timestamp from = Timestamp.newBuilder()
            .setSeconds(start.getEpochSecond())
            .build();
        Timestamp to = Timestamp.newBuilder()
            .setSeconds(now.getEpochSecond())
            .build();

        var reply = account.positionsHistory(
            Mt5TermApiAccountHelper.AH_ENUM_POSITIONS_HISTORY_SORT_TYPE.AH_POSITION_OPEN_TIME_DESC,
            from, to, null, null
        );

        return reply.getData().getHistoryPositionsList().stream()
            .filter(pos -> pos.getMagic() == magicNumber)
            .toList();
    }

    /**
     * Print EA performance report
     */
    public static void printEaReport(
            java.util.List<Mt5TermApiAccountHelper.PositionHistoryInfo> positions,
            long magicNumber) {

        if (positions.isEmpty()) {
            System.out.printf("No positions found for EA (magic: %d)%n", magicNumber);
            return;
        }

        double totalProfit = 0;
        double totalCommission = 0;
        double totalSwap = 0;
        int wins = 0;
        int losses = 0;

        for (var pos : positions) {
            double netProfit = pos.getProfit() - pos.getCommission() -
                               pos.getFee() + pos.getSwap();
            totalProfit += netProfit;
            totalCommission += pos.getCommission();
            totalSwap += pos.getSwap();

            if (netProfit > 0) wins++;
            else if (netProfit < 0) losses++;
        }

        System.out.printf("\nExpert Advisor Report (Magic: %d)%n", magicNumber);
        System.out.println("═".repeat(50));
        System.out.printf("Total Trades: %d%n", positions.size());
        System.out.printf("Wins: %d | Losses: %d%n", wins, losses);
        System.out.printf("Win Rate: %.2f%%%n",
            (double) wins / positions.size() * 100);
        System.out.printf("Net Profit: $%.2f%n", totalProfit);
        System.out.printf("Total Commission: $%.2f%n", totalCommission);
        System.out.printf("Total Swap: $%.2f%n", totalSwap);
    }
}

// Usage
long myEaMagic = 123456;
var eaPositions = ExpertAdvisorAnalyzer.getByMagic(account, myEaMagic, 30);
ExpertAdvisorAnalyzer.printEaReport(eaPositions, myEaMagic);

5) Export to CSVΒΆ

import java.io.*;
import java.time.Instant;

public class CsvExporter {
    /**
     * Export closed positions to CSV file
     */
    public static void exportToCsv(
            MT5Account account,
            Timestamp from,
            Timestamp to,
            String filename) throws ApiExceptionMT5, IOException {

        var reply = account.positionsHistory(
            Mt5TermApiAccountHelper.AH_ENUM_POSITIONS_HISTORY_SORT_TYPE.AH_POSITION_OPEN_TIME_DESC,
            from, to, null, null
        );

        try (PrintWriter writer = new PrintWriter(new FileWriter(filename))) {
            // CSV header
            writer.println("PositionTicket,Symbol,Type,Volume," +
                "OpenTime,CloseTime,OpenPrice,ClosePrice," +
                "StopLoss,TakeProfit,Profit,Commission,Fee,Swap," +
                "NetProfit,MarketValue,Magic,Comment");

            // Data rows
            for (var pos : reply.getData().getHistoryPositionsList()) {
                Instant openTime = Instant.ofEpochSecond(
                    pos.getOpenTime().getSeconds()
                );
                Instant closeTime = Instant.ofEpochSecond(
                    pos.getCloseTime().getSeconds()
                );

                double netProfit = pos.getProfit() - pos.getCommission() -
                                   pos.getFee() + pos.getSwap();

                writer.printf("%d,%s,%s,%.2f,%s,%s,%.5f,%.5f,%.5f,%.5f," +
                    "%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%d,%s%n",
                    pos.getPositionTicket(),
                    pos.getSymbol(),
                    pos.getOrderType(),
                    pos.getVolume(),
                    openTime,
                    closeTime,
                    pos.getOpenPrice(),
                    pos.getClosePrice(),
                    pos.getStopLoss(),
                    pos.getTakeProfit(),
                    pos.getProfit(),
                    pos.getCommission(),
                    pos.getFee(),
                    pos.getSwap(),
                    netProfit,
                    pos.getMarketValue(),
                    pos.getMagic(),
                    pos.getComment().replace(",", ";")  // Escape commas
                );
            }

            System.out.printf("βœ… Exported %d positions to %s%n",
                reply.getData().getHistoryPositionsList().size(),
                filename
            );
        }
    }
}

// Usage
Instant now = Instant.now();
Instant monthAgo = now.minus(30, ChronoUnit.DAYS);

Timestamp from = Timestamp.newBuilder()
    .setSeconds(monthAgo.getEpochSecond())
    .build();
Timestamp to = Timestamp.newBuilder()
    .setSeconds(now.getEpochSecond())
    .build();

CsvExporter.exportToCsv(account, from, to, "closed_positions.csv");

6) Win/Loss streak analyzerΒΆ

public class StreakAnalyzer {
    public record StreakInfo(
        int currentStreak,
        int longestWinStreak,
        int longestLossStreak,
        boolean isWinStreak
    ) {}

    /**
     * Analyze winning and losing streaks
     */
    public static StreakInfo analyzeStreaks(
            MT5Account account,
            int daysBack) throws ApiExceptionMT5 {

        Instant now = Instant.now();
        Instant start = now.minus(daysBack, ChronoUnit.DAYS);

        Timestamp from = Timestamp.newBuilder()
            .setSeconds(start.getEpochSecond())
            .build();
        Timestamp to = Timestamp.newBuilder()
            .setSeconds(now.getEpochSecond())
            .build();

        var reply = account.positionsHistory(
            Mt5TermApiAccountHelper.AH_ENUM_POSITIONS_HISTORY_SORT_TYPE.AH_POSITION_OPEN_TIME_ASC,  // Oldest first
            from, to, null, null
        );

        var positions = reply.getData().getHistoryPositionsList();
        if (positions.isEmpty()) {
            return new StreakInfo(0, 0, 0, true);
        }

        int currentStreak = 0;
        int longestWinStreak = 0;
        int longestLossStreak = 0;
        boolean lastWasWin = false;
        boolean isWinStreak = false;

        for (var pos : positions) {
            double netProfit = pos.getProfit() - pos.getCommission() -
                               pos.getFee() + pos.getSwap();
            boolean isWin = netProfit > 0;

            if (currentStreak == 0) {
                // First trade
                currentStreak = 1;
                lastWasWin = isWin;
                isWinStreak = isWin;
            } else if (isWin == lastWasWin) {
                // Streak continues
                currentStreak++;
            } else {
                // Streak breaks - record and reset
                if (lastWasWin) {
                    longestWinStreak = Math.max(longestWinStreak, currentStreak);
                } else {
                    longestLossStreak = Math.max(longestLossStreak, currentStreak);
                }
                currentStreak = 1;
                lastWasWin = isWin;
                isWinStreak = isWin;
            }
        }

        // Record final streak
        if (lastWasWin) {
            longestWinStreak = Math.max(longestWinStreak, currentStreak);
        } else {
            longestLossStreak = Math.max(longestLossStreak, currentStreak);
        }

        return new StreakInfo(
            currentStreak,
            longestWinStreak,
            longestLossStreak,
            isWinStreak
        );
    }

    /**
     * Print streak analysis
     */
    public static void printStreakReport(StreakInfo info) {
        String currentStreakType = info.isWinStreak() ? "WIN" : "LOSS";

        System.out.println("\n╔════════════════════════════════════╗");
        System.out.println("β•‘       STREAK ANALYSIS              β•‘");
        System.out.println("╠════════════════════════════════════╣");
        System.out.printf("β•‘ Current Streak: %-2d %-13s β•‘%n",
            info.currentStreak(), currentStreakType);
        System.out.printf("β•‘ Longest Win Streak: %-14d β•‘%n",
            info.longestWinStreak());
        System.out.printf("β•‘ Longest Loss Streak: %-13d β•‘%n",
            info.longestLossStreak());
        System.out.println("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•");
    }
}

// Usage
var streakInfo = StreakAnalyzer.analyzeStreaks(account, 30);
StreakAnalyzer.printStreakReport(streakInfo);

7) Calculate Risk-Reward ratioΒΆ

public class RiskRewardAnalyzer {
    /**
     * Calculate average risk-reward ratio from closed positions
     */
    public static void analyzeRiskReward(
            MT5Account account,
            int daysBack) throws ApiExceptionMT5 {

        Instant now = Instant.now();
        Instant start = now.minus(daysBack, ChronoUnit.DAYS);

        Timestamp from = Timestamp.newBuilder()
            .setSeconds(start.getEpochSecond())
            .build();
        Timestamp to = Timestamp.newBuilder()
            .setSeconds(now.getEpochSecond())
            .build();

        var reply = account.positionsHistory(
            Mt5TermApiAccountHelper.AH_ENUM_POSITIONS_HISTORY_SORT_TYPE.AH_POSITION_OPEN_TIME_DESC,
            from, to, null, null
        );

        java.util.List<Double> riskRewardRatios = new java.util.ArrayList<>();

        for (var pos : reply.getData().getHistoryPositionsList()) {
            if (pos.getStopLoss() == 0 || pos.getTakeProfit() == 0) {
                continue;  // Skip positions without SL/TP
            }

            boolean isBuy = pos.getOrderType() ==
                Mt5TermApiAccountHelper.AH_ENUM_POSITIONS_HISTORY_ORDER_TYPE.AH_ORDER_TYPE_BUY;

            double risk, reward;
            if (isBuy) {
                risk = Math.abs(pos.getOpenPrice() - pos.getStopLoss());
                reward = Math.abs(pos.getTakeProfit() - pos.getOpenPrice());
            } else {
                risk = Math.abs(pos.getStopLoss() - pos.getOpenPrice());
                reward = Math.abs(pos.getOpenPrice() - pos.getTakeProfit());
            }

            if (risk > 0) {
                riskRewardRatios.add(reward / risk);
            }
        }

        if (riskRewardRatios.isEmpty()) {
            System.out.println("No positions with SL/TP found");
            return;
        }

        double avgRiskReward = riskRewardRatios.stream()
            .mapToDouble(Double::doubleValue)
            .average()
            .orElse(0);

        System.out.println("\n╔════════════════════════════════════╗");
        System.out.println("β•‘     RISK-REWARD ANALYSIS           β•‘");
        System.out.println("╠════════════════════════════════════╣");
        System.out.printf("β•‘ Positions with SL/TP: %-12d β•‘%n",
            riskRewardRatios.size());
        System.out.printf("β•‘ Avg Risk-Reward Ratio: 1:%-8.2f β•‘%n",
            avgRiskReward);
        System.out.println("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•");
    }
}

// Usage
RiskRewardAnalyzer.analyzeRiskReward(account, 30);

8) Monthly performance breakdownΒΆ

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;

public class MonthlyPerformance {
    public record MonthData(
        YearMonth month,
        int trades,
        double netProfit,
        double winRate
    ) {}

    /**
     * Break down performance by month
     */
    public static java.util.List<MonthData> getMonthlyBreakdown(
            MT5Account account,
            int monthsBack) throws ApiExceptionMT5 {

        Instant now = Instant.now();
        Instant start = now.minus(monthsBack * 30L, ChronoUnit.DAYS);

        Timestamp from = Timestamp.newBuilder()
            .setSeconds(start.getEpochSecond())
            .build();
        Timestamp to = Timestamp.newBuilder()
            .setSeconds(now.getEpochSecond())
            .build();

        var reply = account.positionsHistory(
            Mt5TermApiAccountHelper.AH_ENUM_POSITIONS_HISTORY_SORT_TYPE.AH_POSITION_OPEN_TIME_ASC,
            from, to, null, null
        );

        // Group by month
        Map<YearMonth, java.util.List<Mt5TermApiAccountHelper.PositionHistoryInfo>> byMonth =
            new TreeMap<>();

        for (var pos : reply.getData().getHistoryPositionsList()) {
            Instant openTime = Instant.ofEpochSecond(
                pos.getOpenTime().getSeconds()
            );
            YearMonth month = YearMonth.from(
                LocalDate.ofInstant(openTime, ZoneId.systemDefault())
            );

            byMonth.computeIfAbsent(month, k -> new ArrayList<>()).add(pos);
        }

        // Calculate monthly stats
        java.util.List<MonthData> monthlyData = new ArrayList<>();

        for (var entry : byMonth.entrySet()) {
            var positions = entry.getValue();
            int trades = positions.size();

            double netProfit = positions.stream()
                .mapToDouble(p -> p.getProfit() - p.getCommission() -
                                  p.getFee() + p.getSwap())
                .sum();

            long wins = positions.stream()
                .filter(p -> (p.getProfit() - p.getCommission() -
                              p.getFee() + p.getSwap()) > 0)
                .count();

            double winRate = (double) wins / trades * 100;

            monthlyData.add(new MonthData(
                entry.getKey(),
                trades,
                netProfit,
                winRate
            ));
        }

        return monthlyData;
    }

    /**
     * Print monthly performance report
     */
    public static void printMonthlyReport(java.util.List<MonthData> data) {
        System.out.println("\n╔════════════════════════════════════════════════╗");
        System.out.println("β•‘         MONTHLY PERFORMANCE BREAKDOWN          β•‘");
        System.out.println("╠════════════════════════════════════════════════╣");
        System.out.println("β•‘  Month    | Trades | Net Profit | Win Rate   β•‘");
        System.out.println("╠════════════════════════════════════════════════╣");

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM yyyy");

        for (var month : data) {
            System.out.printf("β•‘ %-9s | %-6d | $%-9.2f | %-8.1f%% β•‘%n",
                month.month().format(formatter),
                month.trades(),
                month.netProfit(),
                month.winRate()
            );
        }

        double totalProfit = data.stream()
            .mapToDouble(MonthData::netProfit)
            .sum();

        System.out.println("╠════════════════════════════════════════════════╣");
        System.out.printf("β•‘ TOTAL                | $%-9.2f           β•‘%n",
            totalProfit);
        System.out.println("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•");
    }
}

// Usage
var monthlyData = MonthlyPerformance.getMonthlyBreakdown(account, 12);
MonthlyPerformance.printMonthlyReport(monthlyData);

πŸ”„ Low-level gRPC call (for reference)ΒΆ

import io.grpc.*;
import mt5_term_api.*;
import com.google.protobuf.Timestamp;

// Create request
Timestamp from = Timestamp.newBuilder()
    .setSeconds(Instant.now().minus(30, ChronoUnit.DAYS).getEpochSecond())
    .build();
Timestamp to = Timestamp.newBuilder()
    .setSeconds(Instant.now().getEpochSecond())
    .build();

Mt5TermApiAccountHelper.PositionsHistoryRequest request =
    Mt5TermApiAccountHelper.PositionsHistoryRequest.newBuilder()
        .setSortType(Mt5TermApiAccountHelper.AH_ENUM_POSITIONS_HISTORY_SORT_TYPE.AH_POSITION_OPEN_TIME_DESC)
        .setPositionOpenTimeFrom(from)
        .setPositionOpenTimeTo(to)
        .setPageNumber(0)
        .setItemsPerPage(0)
        .build();

// Add metadata headers
Metadata headers = new Metadata();
Metadata.Key<String> idKey = Metadata.Key.of("id", Metadata.ASCII_STRING_MARSHALLER);
headers.put(idKey, instanceId.toString());

// Call service
Mt5TermApiAccountHelper.PositionsHistoryReply reply = accountHelperClient
    .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(headers))
    .positionsHistory(request);

// Check for errors
if (reply.hasError()) {
    throw new ApiExceptionMT5(reply.getError());
}

// Use data
List<Mt5TermApiAccountHelper.PositionHistoryInfo> positions =
    reply.getData().getHistoryPositionsList();

πŸ“Š Key Metrics CalculationsΒΆ

Net Profit (per position):

double netProfit = position.getProfit() - position.getCommission()
                   - position.getFee() + position.getSwap();

Win Rate:

double winRate = (double) winningTrades / totalTrades * 100;

Profit Factor:

double profitFactor = totalProfit / totalLoss;  // > 1 is profitable

Average Holding Time:

long holdingSeconds = position.getCloseTime().getSeconds()
                      - position.getOpenTime().getSeconds();
double holdingHours = holdingSeconds / 3600.0;

Price Movement (pips):

double priceMove = position.getOrderType() == AH_ORDER_TYPE_BUY
    ? (position.getClosePrice() - position.getOpenPrice())
    : (position.getOpenPrice() - position.getClosePrice());