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.
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
| Field | Type | Description |
|---|---|---|
exchange | str | Exchange name |
symbol | str | Ticker / instrument |
direction | str | 'long' | 'short' | 'flat' |
entry_price | float | Average entry price |
current_price | float | Latest mark price |
quantity | float | Position size |
notional_usd | float | Current notional value |
pnl_usd | float | Unrealized Profit and Loss (PnL) in USD |
pnl_pct | float | Unrealized PnL as a percentage |
leverage | float | Effective leverage |
timestamp | str | Snapshot 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 None2. 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'sfutures_position_information()API response.current_price(float): The current market price of the asset, used for PnL and notional value calculations.
Returns:
- A
dictrepresenting the normalized position, orNoneif 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'sget_positions()['result']['list']API response.current_price(float): The current mark price of the asset.
Returns:
- A
dictrepresenting the normalized position, orNoneif 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'sget_open_positions()['openPositions']API response.current_price(float): The latest mark price of the asset.
Returns:
- A
dictrepresenting the normalized position, orNoneif 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 is2.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
dictrepresenting the initialized position manager, containingtp_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, orNoneif 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.DataFramecontaining 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
dictcontaining 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, containingtp_alert_pctandsl_alert_pct.
Returns:
- A
listofstr, 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
listcontaining a single simulated Binance position, represented as a dictionary. This list is designed to be consumed by therun_position_monitorfunction.
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 alistof 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 is10seconds.max_cycles(int, Optional): An optional integer to limit the total number of monitoring cycles. IfNone, the monitor runs indefinitely until manually stopped. Default isNone.
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.