Execution·Trade & Position Tracking·Beginner

Track Positions

Implement a comprehensive position tracking system that monitors all open positions across multiple trading strategies and exchange accounts, with real-time unrealized PnL computation, margin utilization monitoring, and liquidation price proximity alerts.

order-executiontrade-&-position-tracking

Position Tracker


Overview

This notebook provides a unified position tracking layer, normalizing open position data from various cryptocurrency exchanges (Binance, Bybit, Kraken) and MetaTrader into a consistent dictionary-based schema.

Position Dictionary Schema

FieldTypeDescription
exchangestrExchange name
symbolstrTicker / instrument
directionstr'long' | 'short' | 'flat'
entry_pricefloatAverage entry price
current_pricefloatLatest mark price
quantityfloatPosition size
notional_usdfloatCurrent notional value
pnl_usdfloatUnrealized Profit and Loss (PnL) in USD
pnl_pctfloatUnrealized PnL as a percentage
leveragefloatEffective leverage
timestampstrSnapshot time (formatted string)

1. Library Imports

This section imports all necessary Python libraries for data manipulation, numerical operations, visualization, and custom data structures.

[ ]
import time
import datetime
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from typing import Optional # Used for type hints where a value might be None

2. Unified Position Data Structure

This section describes the standardized dictionary-based data structure used to represent an open trading position in an exchange-agnostic manner. This approach ensures consistent data handling across different exchange APIs by normalizing various position parameters into a common dictionary format.

3. Exchange-Specific Adapters

This section provides functions to parse raw position data from different cryptocurrency exchanges (Binance, Bybit, Kraken) into a standardized dictionary schema. Each function handles the unique data structure of its respective exchange API.

3.1. Binance Position Parser Function

Purpose: Parses raw Binance Futures position data into a standardized dictionary format.

Parameters:

  • raw (dict): The raw position data dictionary from Binance's futures_position_information() API response.
  • current_price (float): The current market price of the asset, used for PnL and notional value calculations.

Returns:

  • A dict representing the normalized position, or None if no open position is detected (e.g., entry price or quantity is zero).
[ ]
def parse_binance_position(raw: dict, current_price: float) -> Optional[dict]:
    """
    Parse a raw Binance futures position dict into a unified dictionary schema.

    Parameters
    ----------
    raw           : Response dict from `futures_position_information()`.
    current_price : Latest mark price for PnL calculation.

    Returns
    -------
    Dictionary representing the position or None if no position is open.
    """
    # Extract relevant fields from the raw Binance data
    entry = float(raw.get("entryPrice", 0))
    amt   = float(raw.get("positionAmt", 0))
    lev   = float(raw.get("leverage", 1))

    # If entry price or amount is zero, there's no open position to track
    if entry == 0 or amt == 0:
        return None

    # Determine position direction (long or short)
    direction  = "long" if amt > 0 else "short"
    quantity   = abs(amt)
    notional   = quantity * current_price # Calculate current notional value
    pnl_usd    = float(raw.get("unrealizedProfit", 0))
    # Calculate PnL percentage relative to entry value
    pnl_pct    = (pnl_usd / (quantity * entry)) * 100 if entry else 0

    # Return a dictionary with normalized data
    return {
        "exchange":      "Binance",
        "symbol":        raw.get("symbol", ""),
        "direction":     direction,
        "entry_price":   entry,
        "current_price": current_price,
        "quantity":      quantity,
        "notional_usd":  round(notional, 2),
        "pnl_usd":       round(pnl_usd, 4),
        "pnl_pct":       round(pnl_pct, 4),
        "leverage":      lev,
        "timestamp":     datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%d %H:%M:%S")
    }

3.2. Bybit Position Parser Function

Purpose: Parses raw Bybit position data into a standardized dictionary format.

Parameters:

  • raw (dict): An item from Bybit's get_positions()['result']['list'] API response.
  • current_price (float): The current mark price of the asset.

Returns:

  • A dict representing the normalized position, or None if no open position is detected.
[ ]
def parse_bybit_position(raw: dict, current_price: float) -> Optional[dict]:
    """
    Parse a raw Bybit position dict into a unified dictionary schema.

    Parameters
    ----------
    raw           : Item from `get_positions()['result']['list']`.
    current_price : Latest mark price.

    Returns
    -------
    Dictionary representing the position or None if no position is open.
    """
    # Extract average entry price from raw Bybit data
    avg_price = raw.get("avgPrice", "0")
    # If no average price, no open position
    if avg_price in ("", "0"):
        return None

    entry     = float(avg_price)
    size      = float(raw.get("size", 0))
    side      = raw.get("side", "").lower()
    direction = "long" if side == "buy" else "short"
    pnl_usd   = float(raw.get("unrealisedPnl", 0))
    notional  = size * current_price
    pnl_pct   = (pnl_usd / (size * entry)) * 100 if entry else 0

    return {
        "exchange":      "Bybit",
        "symbol":        raw.get("symbol", ""),
        "direction":     direction,
        "entry_price":   entry,
        "current_price": current_price,
        "quantity":      size,
        "notional_usd":  round(notional, 2),
        "pnl_usd":       round(pnl_usd, 4),
        "pnl_pct":       round(pnl_pct, 4),
        "leverage":      float(raw.get("leverage", 1)),
        "timestamp":     datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%d %H:%M:%S")
    }

3.3. Kraken Position Parser Function

Purpose: Parses raw Kraken Futures position data into a standardized dictionary format.

Parameters:

  • raw (dict): An item from Kraken's get_open_positions()['openPositions'] API response.
  • current_price (float): The latest mark price of the asset.

Returns:

  • A dict representing the normalized position, or None if no open position is detected.
[ ]
def parse_kraken_position(raw: dict, current_price: float) -> Optional[dict]:
    """
    Parse a raw Kraken Futures position dict into a unified dictionary schema.

    Parameters
    ----------
    raw           : Item from `get_open_positions()['openPositions']`.
    current_price : Latest mark price.

    Returns
    -------
    Dictionary representing the position or None if no position is open.
    """
    # Extract position size, entry price, and side from raw Kraken data
    size  = float(raw.get("size", 0))
    entry = float(raw.get("price", 0))
    side  = raw.get("side", "").lower()

    # If size or entry price is zero, no open position
    if size == 0 or entry == 0:
        return None

    direction = "long" if side == "long" else "short"
    # Calculate PnL based on position direction
    if direction == "long":
        pnl_usd = (current_price - entry) * size
    else:
        pnl_usd = (entry - current_price) * size

    notional = size * current_price
    pnl_pct  = (pnl_usd / (size * entry)) * 100 if entry else 0

    return {
        "exchange":      "Kraken",
        "symbol":        raw.get("symbol", ""),
        "direction":     direction,
        "entry_price":   entry,
        "current_price": current_price,
        "quantity":      size,
        "notional_usd":  round(notional, 2),
        "pnl_usd":       round(pnl_usd, 4),
        "pnl_pct":       round(pnl_pct, 4),
        "leverage":      1.0, # Kraken raw data might not provide leverage directly, default to 1.0
        "timestamp":     datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%d %H:%M:%S")
    }

4. Position Management Functions

This section defines a set of functions responsible for aggregating, managing, and monitoring open trading positions across various exchanges. This functional approach provides a unified interface to view all active positions, calculate overall Profit and Loss (PnL), and trigger alerts based on predefined Take-Profit (TP) and Stop-Loss (SL) thresholds.

4.1. create_position_manager Function

Purpose: Initializes the position management system by creating a dictionary that holds the current state, including alert thresholds and lists for active positions and historical snapshots.

Parameters:

  • tp_alert_pct (float): The Profit & Loss percentage (PnL%) threshold above which a Take-Profit (TP) alert is triggered. Default is 2.0 (2%).
  • sl_alert_pct (float): The PnL% threshold below which a Stop-Loss (SL) alert is triggered. Default is -1.0 (-1%).

Returns:

  • A dict representing the initialized position manager, containing tp_alert_pct, sl_alert_pct, _positions (an empty list for current positions), and _snapshots (an empty list for historical DataFrames).
[ ]
def create_position_manager(tp_alert_pct: float = 2.0, sl_alert_pct: float = -1.0) -> dict:
    """
    Initializes a new position manager as a dictionary.

    Parameters
    ----------
    tp_alert_pct : PnL% threshold above which a TP alert is raised.
    sl_alert_pct : PnL% threshold below which an SL alert is raised.

    Returns
    -------
    A dictionary representing the position manager state.
    """
    return {
        "tp_alert_pct": tp_alert_pct,
        "sl_alert_pct": sl_alert_pct,
        "_positions":   [],
        "_snapshots":   []
    }

4.2. add_position Function

Purpose: Adds a parsed position (represented as a dictionary) to the list of active positions managed by the manager.

Parameters:

  • manager (dict): The dictionary representing the current state of the position manager.
  • pos (dict, Optional): A dictionary representing a single open position, or None if the parsing function indicated no position.

Returns: None

This function ensures that only valid position dictionaries are added to the tracker, preventing None values from being processed.

[ ]
def add_position(manager: dict, pos: dict) -> None:
    """
    Registers a parsed position dictionary into the manager.
    """
    if pos is not None:
        manager["_positions"].append(pos)

4.3. clear_positions Function

Purpose: Resets the list of current open positions within the manager. This is typically invoked at the beginning of each refresh cycle in a monitoring loop to ensure that stale position data is removed before new data is fetched and added.

Parameters:

  • manager (dict): The dictionary representing the current state of the position manager.

Returns: None

This function is crucial for maintaining a real-time view of positions, as it clears the previous state before updating with fresh data.

[ ]
def clear_positions(manager: dict) -> None:
    """
    Clears current positions in the manager (executed before each refresh cycle).
    """
    manager["_positions"] = []

4.4. get_snapshot Function

Purpose: Converts the current list of open positions stored in the manager into a pandas.DataFrame for a structured, tabular view. It also archives this DataFrame into the manager's _snapshots list for historical record-keeping.

Parameters:

  • manager (dict): The dictionary representing the current state of the position manager.

Returns:

  • A pandas.DataFrame containing all active positions. If no positions are present, an empty DataFrame with predefined columns is returned to maintain schema consistency.
[ ]
def get_snapshot(manager: dict) -> pd.DataFrame:
    """
    Returns a DataFrame of all current positions and saves a historical snapshot.
    """
    positions_list = manager["_positions"]
    if not positions_list:
        # Define column names based on the expected keys in position dictionaries
        columns = [
            "exchange", "symbol", "direction", "entry_price", "current_price",
            "quantity", "notional_usd", "pnl_usd", "pnl_pct", "leverage", "timestamp"
        ]
        return pd.DataFrame(columns=columns)
    df = pd.DataFrame(positions_list)
    manager["_snapshots"].append(df)
    return df

4.5. get_summary Function

Purpose: Calculates and returns an aggregated summary of all active positions within the manager. This summary includes key metrics such as total number of positions, breakdown by direction (long/short), total PnL in USD, total notional value, and the average PnL percentage.

Parameters:

  • manager (dict): The dictionary representing the current state of the position manager.

Returns:

  • A dict containing the aggregated summary statistics. Returns default zero values if no positions are active.
[ ]
def get_summary(manager: dict) -> dict:
    """
    Computes aggregate statistics across all tracked positions in the manager.
    """
    positions_list = manager["_positions"]
    if not positions_list:
        return {"total_positions": 0, "total_pnl_usd": 0, "total_notional": 0}

    total_pnl      = sum(p["pnl_usd"]       for p in positions_list)
    total_notional = sum(p["notional_usd"]   for p in positions_list)
    n_long         = sum(1 for p in positions_list if p["direction"] == "long")
    n_short        = sum(1 for p in positions_list if p["direction"] == "short")
    avg_pnl_pct    = np.mean([p["pnl_pct"] for p in positions_list]) if positions_list else 0

    return {
        "total_positions": len(positions_list),
        "long_positions":  n_long,
        "short_positions": n_short,
        "total_pnl_usd":   round(total_pnl, 4),
        "total_notional":  round(total_notional, 2),
        "avg_pnl_pct":     round(avg_pnl_pct, 4),
    }

4.6. check_alerts Function

Purpose: Evaluates each active position against predefined Take-Profit (TP) and Stop-Loss (SL) percentage thresholds. If a position's PnL percentage breaches these thresholds, an alert message is generated.

Parameters:

  • manager (dict): The dictionary representing the current state of the position manager, containing tp_alert_pct and sl_alert_pct.

Returns:

  • A list of str, where each string is a descriptive alert message for positions that have breached a threshold. Returns an empty list if no alerts are triggered.
[ ]
def check_alerts(manager: dict) -> list[str]:
    """
    Evaluates Take-Profit (TP) and Stop-Loss (SL) alert conditions for positions in the manager.

    Returns
    -------
    List of alert message strings for positions breaching defined thresholds.
    """
    alerts = []
    tp_alert_pct = manager["tp_alert_pct"]
    sl_alert_pct = manager["sl_alert_pct"]
    for p in manager["_positions"]:
        if p["pnl_pct"] >= tp_alert_pct:
            alerts.append(f"[TP ALERT] {p['exchange']} {p['symbol']} {p['direction'].upper()} "
                           f"PnL: {p['pnl_pct']:.2f}% — Consider closing.")
        if p["pnl_pct"] <= sl_alert_pct:
            alerts.append(f"[SL ALERT] {p['exchange']} {p['symbol']} {p['direction'].upper()} "
                           f"PnL: {p['pnl_pct']:.2f}% — Risk threshold breached.")
    return alerts

4.7. display_positions Function

Purpose: Prints a neatly formatted table of all current open positions, an aggregated summary, and any active TP/SL alerts directly to the console. This provides a user-friendly, comprehensive overview of the position state at a given moment.

Parameters:

  • manager (dict): The dictionary representing the current state of the position manager.

Returns: None

This function utilizes get_snapshot, get_summary, and check_alerts to compile and present a complete status report.

[ ]
def display_positions(manager: dict) -> None:
    """
    Prints a formatted position table and summary to the console for the manager's positions.

    Includes alerts for TP/SL breaches.
    """
    df  = get_snapshot(manager)
    sm  = get_summary(manager)
    als = check_alerts(manager)

    print("\n" + "="*70)
    print(f" POSITION SNAPSHOT — {datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S')} UTC")
    print("="*70)
    if df.empty:
        print(" No open positions.")
    else:
        # Select and rename columns for display to match the original class output format
        display_df = df[[
            "exchange", "symbol", "direction", "entry_price", "current_price",
            "quantity", "notional_usd", "pnl_usd", "pnl_pct", "leverage", "timestamp"
        ]]
        display_df.columns = [
            "Exchange", "Symbol", "Direction", "Entry Price", "Current Price",
            "Quantity", "Notional (USD)", "PnL (USD)", "PnL (%)", "Leverage", "Snapshot Time"
        ]
        print(display_df.to_string(index=False))
    print("-"*70)
    for k, v in sm.items():
        print(f"  {k:<22}: {v}")
    if als:
        print("-"*70)
        for alert in als:
            print(f"  ⚠  {alert}")
    print("="*70 + "\n")

5. Simulated Position Data Demonstration

This section demonstrates the functionality of the position management functions using synthetic position data from various exchanges. It illustrates how raw exchange data is parsed, aggregated, and displayed in a unified format, including PnL calculations and alert generation.

5.1. simulated_positions Function

Purpose: Generates synthetic position data to simulate real-world exchange responses for testing the monitoring loop. Each invocation produces a slightly varied current price to mimic market fluctuations.

Parameters: None.

Returns:

  • A list containing a single simulated Binance position, represented as a dictionary. This list is designed to be consumed by the run_position_monitor function.
[ ]
def simulated_positions():
    """
    Return synthetic position list for testing the monitor loop.

    Each call generates a slightly varied current price.
    """
    import random
    # Simulate a fluctuating current price for BTC
    price = 50_000 + random.uniform(-500, 500)
    raw   = {"symbol": "BTCUSDT", "entryPrice": "50000.0",
             "positionAmt": "0.1", "unrealizedProfit": str((price-50_000)*0.1), "leverage": "2"}
    # Return a list containing one simulated Binance position dictionary
    return [parse_binance_position(raw, price)]
[ ]
# ── Generate synthetic position data ──────────────────────────────────────
# Initialize position manager with custom alert thresholds
manager = create_position_manager(tp_alert_pct=2.0, sl_alert_pct=-1.0)

# Simulated raw data from exchanges ──────────────────────────────────────

# Binance position (BTC long example)
binance_raw = {"symbol": "BTCUSDT", "entryPrice": "68000.0", "positionAmt": "0.15",
               "unrealizedProfit": "210.0", "leverage": "5"}
btc_current_price = 69_400.0
# Parse and add the Binance position to the manager
add_position(manager, parse_binance_position(binance_raw, btc_current_price))

# Bybit position (ETH short example)
bybit_raw = {"symbol": "ETHUSDT", "avgPrice": "3500.0", "size": "2.0",
             "side": "Sell", "unrealisedPnl": "80.0", "leverage": "3"}
eth_current_price = 3_460.0
# Parse and add the Bybit position to the manager
add_position(manager, parse_bybit_position(bybit_raw, eth_current_price))

# Kraken position (BTC short in loss example)
kraken_raw = {"symbol": "PF_XBTUSD", "price": "69000.0", "size": "0.05", "side": "short"}
# Parse and add the Kraken position to the manager
add_position(manager, parse_kraken_position(kraken_raw, 69_500.0))

# ── Display manager output ─────────────────────────────────────────────────
# Display the aggregated positions, summary, and any alerts
display_positions(manager)

======================================================================
 POSITION SNAPSHOT — 2026-05-20 10:18:24 UTC
======================================================================
Exchange    Symbol Direction  Entry Price  Current Price  Quantity  Notional (USD)  PnL (USD)  PnL (%)  Leverage       Snapshot Time
 Binance   BTCUSDT      long      68000.0        69400.0      0.15         10410.0      210.0   2.0588       5.0 2026-05-20 10:18:24
   Bybit   ETHUSDT     short       3500.0         3460.0      2.00          6920.0       80.0   1.1429       3.0 2026-05-20 10:18:24
  Kraken PF_XBTUSD     short      69000.0        69500.0      0.05          3475.0      -25.0  -0.7246       1.0 2026-05-20 10:18:24
----------------------------------------------------------------------
  total_positions       : 3
  long_positions        : 1
  short_positions       : 2
  total_pnl_usd         : 265.0
  total_notional        : 20805.0
  avg_pnl_pct           : 0.8257
----------------------------------------------------------------------
  ⚠  [TP ALERT] Binance BTCUSDT LONG PnL: 2.06% — Consider closing.
======================================================================

6. Continuous Monitoring Implementation

This section defines and demonstrates a continuous monitoring loop for tracking open positions. The run_position_monitor function periodically fetches updated position data, processes it using the functional position management utilities, and displays the consolidated view along with any active alerts. This loop simulates real-time position management.

6.1. run_position_monitor Function

Purpose: Implements a continuous monitoring loop that periodically fetches updated position data, processes it using the functional position management utilities, and displays a consolidated view along with any active alerts. This function simulates real-time position management.

Parameters:

  • get_positions_fn (Callable): A function that, when called, returns a list of position dictionaries (e.g., simulated_positions). This function is responsible for fetching or generating the latest position data.
  • interval_s (int): The number of seconds to wait between each refresh cycle of the monitor. Default is 10 seconds.
  • max_cycles (int, Optional): An optional integer to limit the total number of monitoring cycles. If None, the monitor runs indefinitely until manually stopped. Default is None.

Returns: None

This function is the core of the continuous tracking system, ensuring positions are regularly updated and status reports are generated.

[ ]
def run_position_monitor(get_positions_fn, interval_s: int = 10,
                          max_cycles: int = None) -> None:
    """
    Run a continuous position monitoring loop.

    Parameters
    ----------
    get_positions_fn : Callable() → list[dict] — fetches fresh position data.
    interval_s       : Seconds between refresh cycles.
    max_cycles       : Optional limit for number of cycles (None = infinite).
    """
    # Initialize a new position manager for the monitor
    manager = create_position_manager()
    cycle   = 0
    print(f"[MONITOR] Starting position tracker. Refresh: {interval_s}s")

    while True:
        # Clear existing positions in the manager before fetching new ones
        clear_positions(manager)
        try:
            positions = get_positions_fn() # Fetch fresh position data (list of dicts)
            for pos in positions:
                add_position(manager, pos) # Add each fetched position to the manager
            display_positions(manager) # Display the updated positions and summary
        except Exception as e:
            print(f"[ERROR] Failed to fetch positions: {e}")

        cycle += 1
        if max_cycles is not None and cycle >= max_cycles:
            print("[MONITOR] Max cycles reached. Stopping.")
            break # Exit loop if max cycles are reached
        time.sleep(interval_s) # Pause for the defined interval before the next cycle

# To run the continuous monitoring, uncomment the line below:
# run_position_monitor(simulated_positions, interval_s=2, max_cycles=3)

Conclusion

This notebook provides a robust framework for normalizing, tracking, and monitoring cryptocurrency positions across various exchanges. By standardizing data structures and implementing a continuous monitoring loop, users can gain a unified view of their portfolio, receive timely alerts for Take-Profit and Stop-Loss thresholds, and make informed trading decisions. The modular design, with exchange-specific parsing functions and a centralized position manager, allows for easy extension to support additional exchanges or custom alert criteria.