Research·Model Explainability·Advanced

Trade Reason Logger

Automatically log clear human-readable natural language explanations for every trade the system executes, translating opaque ML model output scores and interacting technical indicator readings into plain-language trading rationale for essential post-trade review, debugging, and regulatory compliance documentation.

model-explainabilityquant-research

Log Human-Readable Trade Reasons

This notebook demonstrates how to effectively log human-readable trade reasons within a trading system. Capturing detailed and understandable reasons for trades is crucial for post-analysis, strategy refinement, compliance, and auditing. It allows traders and strategists to review why certain actions were taken, learn from past decisions, and identify patterns.

Concepts Covered:

ConceptDescription
Structured LoggingStoring trade reasons in a consistent, easily parseable format.
Human-ReadabilityEnsuring log messages are clear and understandable without deep technical knowledge.
State ManagementUsing dictionaries to maintain and pass system state.
Data SimulationGenerating mock market data and trade events for demonstration.
VisualizationPresenting logged data in an intuitive graphical format.

2. Dependency Installation

This section installs all necessary external Python libraries required for the notebook. For this topic, we will primarily use pandas for data manipulation and logging (built-in) for logging, but tqdm is useful for progress bars in simulations.

[1]
# Install necessary libraries
!pip install pandas tqdm
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)
Requirement already satisfied: tqdm in /usr/local/lib/python3.12/dist-packages (4.67.3)
Requirement already satisfied: numpy>=1.26.0 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.0.2)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.2)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)

3. Library Imports

This section imports all required Python libraries. Standard libraries are imported first, followed by third-party libraries.

[2]
import logging
import datetime
import time
import random
from collections import deque
from typing import Dict, Any, List, Optional

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from tqdm.notebook import tqdm

# Configure basic logging for the notebook
# This ensures that log messages are displayed in a human-readable format
# and can be captured easily.
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

4. Core Functions

This section defines the core functions for managing and logging trade reasons. Each function is presented in its own dedicated code block, accompanied by a detailed markdown header explaining its purpose, algorithm, parameters, and return values.

Function Name: create_trade_log_state

This function initializes the state dictionary for managing trade logs. It sets up an empty list to store all trade reason entries, which will be dictionaries containing structured information about each trade.

Parameters: None

Returns: dict: An initial state dictionary containing an empty list for trade_reasons.

[3]
def create_trade_log_state() -> Dict[str, Any]:
    """
    Initializes the state dictionary for trade reason logging.

    Returns
    -------
    dict
        An initial state dictionary with an empty list for 'trade_reasons'.

    Examples
    --------
    >>> state = create_trade_log_state()
    >>> isinstance(state, dict)
    True
    >>> 'trade_reasons' in state
    True
    >>> isinstance(state['trade_reasons'], list)
    True
    """
    logger.info("Initializing trade log state.")
    return {
        "trade_reasons": []
    }

Function Name: log_trade_reason

This function is responsible for recording a single human-readable trade reason into the system's state. It captures essential details such as timestamp, instrument, trade direction (buy/sell), price, quantity, and the specific reason. Each reason is stored as a dictionary within the trade_reasons list in the state.

Parameters: state (dict): The current state dictionary, which must contain a trade_reasons list. instrument (str): The trading instrument (e.g., 'AAPL', 'EURUSD'). direction (str): The direction of the trade ('BUY' or 'SELL'). price (float): The price at which the trade occurred. quantity (float): The quantity traded. reason_code (str): A short, predefined code for the reason (e.g., 'MOMENTUM_BREAKOUT'). description (str): A detailed, human-readable description of the trade reason. tags (Optional[List[str]]): Optional list of tags for further categorization (e.g., ['technical', 'strategy_v1']).

Returns: dict: The updated state dictionary with the new trade reason appended.

[4]
def log_trade_reason(
    state: Dict[str, Any],
    instrument: str,
    direction: str,
    price: float,
    quantity: float,
    reason_code: str,
    description: str,
    tags: Optional[List[str]] = None
) -> Dict[str, Any]:
    """
    Logs a human-readable trade reason into the state.

    Parameters
    ----------
    state : dict
        Current state dictionary, must contain 'trade_reasons' list.
    instrument : str
        The trading instrument (e.g., 'AAPL', 'EURUSD').
    direction : str
        The direction of the trade ('BUY' or 'SELL').
    price : float
        The price at which the trade occurred.
    quantity : float
        The quantity traded.
    reason_code : str
        A short, predefined code for the reason (e.g., 'MOMENTUM_BREAKOUT').
    description : str
        A detailed, human-readable description of the trade reason.
    tags : Optional[List[str]], optional
        Optional list of tags for further categorization, defaults to None.

    Returns
    -------
    dict
        The updated state dictionary with the new trade reason appended.

    Examples
    --------
    >>> state = create_trade_log_state()
    >>> state = log_trade_reason(state, 'MSFT', 'BUY', 150.25, 100.0,
    ...                          'SUPPORT_BOUNCE', 'Price bounced off daily support level.',
    ...                          tags=['technical'])
    >>> len(state['trade_reasons']) == 1
    True
    >>> state['trade_reasons'][0]['instrument'] == 'MSFT'
    True
    """
    if 'trade_reasons' not in state:
        logger.error("State dictionary missing 'trade_reasons' list. Initializing.")
        state['trade_reasons'] = []

    trade_entry = {
        "timestamp": datetime.datetime.now().isoformat(),
        "instrument": instrument,
        "direction": direction,
        "price": price,
        "quantity": quantity,
        "reason_code": reason_code,
        "description": description,
        "tags": tags if tags is not None else []
    }
    state['trade_reasons'].append(trade_entry)
    logger.debug(f"Logged trade reason for {instrument} ({direction}): {reason_code}")
    return state

Function Name: get_trade_reasons_df

This function retrieves all logged trade reasons from the state and converts them into a pandas DataFrame. This format is highly suitable for analysis, filtering, and visualization. It also handles cases where no trade reasons have been logged yet.

Parameters: state (dict): The current state dictionary containing trade_reasons.

Returns: pd.DataFrame: A DataFrame containing all logged trade reasons. Returns an empty DataFrame if no reasons are logged.

[5]
def get_trade_reasons_df(state: Dict[str, Any]) -> pd.DataFrame:
    """
    Converts the logged trade reasons from the state into a pandas DataFrame.

    Parameters
    ----------
    state : dict
        Current state dictionary, must contain 'trade_reasons' list.

    Returns
    -------
    pd.DataFrame
        A DataFrame containing all logged trade reasons.
        Returns an empty DataFrame if no reasons are logged.

    Examples
    --------
    >>> state = create_trade_log_state()
    >>> df = get_trade_reasons_df(state)
    >>> isinstance(df, pd.DataFrame)
    True
    >>> df.empty
    True
    >>> state = log_trade_reason(state, 'GOOG', 'SELL', 2500.0, 50.0,
    ...                          'RESISTANCE_REJECT', 'Price rejected at daily resistance.',
    ...                          tags=['technical'])
    >>> df = get_trade_reasons_df(state)
    >>> not df.empty
    True
    """
    if not state.get('trade_reasons'):
        logger.warning("No trade reasons found in state. Returning empty DataFrame.")
        return pd.DataFrame()

    df = pd.DataFrame(state['trade_reasons'])
    df['timestamp'] = pd.to_datetime(df['timestamp'])
    logger.info(f"Successfully converted {len(df)} trade reasons to DataFrame.")
    return df

Function Name: create_simulated_market_data

This helper function generates a synthetic dataset resembling market price data over a specified period. This data will be used to simulate trading activity and generate trade reasons for demonstration purposes.

Parameters: start_date (str): The start date for the simulated data in 'YYYY-MM-DD' format. end_date (str): The end date for the simulated data in 'YYYY-MM-DD' format. instrument (str): The name of the instrument (e.g., 'STOCK_A'). initial_price (float): The starting price for the simulation. volatility (float): A factor determining the magnitude of price fluctuations.

Returns: pd.DataFrame: A DataFrame containing simulated 'timestamp' and 'price' columns.

[6]
def create_simulated_market_data(
    start_date: str,
    end_date: str,
    instrument: str,
    initial_price: float,
    volatility: float
) -> pd.DataFrame:
    """
    Generates simulated market price data.

    Parameters
    ----------
    start_date : str
        Start date for the simulation (YYYY-MM-DD).
    end_date : str
        End date for the simulation (YYYY-MM-DD).
    instrument : str
        The name of the instrument.
    initial_price : float
        The starting price.
    volatility : float
        A factor for price fluctuations.

    Returns
    -------
    pd.DataFrame
        DataFrame with 'timestamp' and 'price' columns.

    Examples
    --------
    >>> df_sim = create_simulated_market_data('2023-01-01', '2023-01-05', 'TEST', 100.0, 0.01)
    >>> isinstance(df_sim, pd.DataFrame)
    True
    >>> 'price' in df_sim.columns
    True
    >>> len(df_sim) > 0
    True
    """
    dates = pd.date_range(start=start_date, end=end_date, freq='H')
    prices = [initial_price]
    for _ in range(1, len(dates)):
        change = np.random.normal(0, volatility) * prices[-1]
        prices.append(prices[-1] + change)

    df = pd.DataFrame({
        'timestamp': dates,
        'instrument': instrument,
        'price': prices
    })
    logger.info(f"Generated {len(df)} simulated data points for {instrument}.")
    return df

Function Name: simulate_trading_strategy

This function simulates a basic trading strategy over the provided market data, generating and logging trade reasons based on simple conditions (e.g., price crosses a moving average). It demonstrates how log_trade_reason would be integrated into an actual trading simulation or execution system.

Parameters: state (dict): The current state dictionary for trade logging. market_data (pd.DataFrame): DataFrame containing simulated market data with 'timestamp', 'instrument', and 'price' columns. window_size (int): The window size for a simple moving average (SMA) used in the strategy.

Returns: dict: The updated state dictionary with newly logged trade reasons.

[7]
def simulate_trading_strategy(
    state: Dict[str, Any],
    market_data: pd.DataFrame,
    window_size: int = 10
) -> Dict[str, Any]:
    """
    Simulates a simple trading strategy and logs trade reasons.

    The strategy generates a 'BUY' signal when price crosses above a simple moving average (SMA),
    and a 'SELL' signal when price crosses below the SMA.

    Parameters
    ----------
    state : dict
        Current state dictionary for trade logging.
    market_data : pd.DataFrame
        DataFrame with 'timestamp', 'instrument', and 'price' columns.
    window_size : int, optional
        The window size for the simple moving average, defaults to 10.

    Returns
    -------
    dict
        The updated state dictionary with newly logged trade reasons.

    Examples
    --------
    >>> initial_state = create_trade_log_state()
    >>> sim_data = create_simulated_market_data('2023-01-01', '2023-01-02', 'TEST', 100.0, 0.005)
    >>> updated_state = simulate_trading_strategy(initial_state, sim_data, window_size=5)
    >>> isinstance(updated_state, dict)
    True
    >>> len(updated_state['trade_reasons']) >= 0
    True
    """
    df = market_data.copy()
    instrument = df['instrument'].iloc[0]
    df['SMA'] = df['price'].rolling(window=window_size).mean()

    # Initialize previous price/SMA relationship
    prev_price_above_sma = False
    if not df.iloc[:window_size].empty:
      if df['price'].iloc[window_size - 1] > df['SMA'].iloc[window_size - 1]:
          prev_price_above_sma = True

    # Simulate with random jitter for realism
    for i in tqdm(range(window_size, len(df)), desc=f"Simulating {instrument} trades"):
        current_row = df.iloc[i]
        current_price = current_row['price']
        current_sma = current_row['SMA']

        # Add random jitter to simulate processing delay or market dynamics
        time.sleep(0.001 * random.uniform(0.8, 1.2))

        if current_price > current_sma and not prev_price_above_sma:
            # Price crossed above SMA: BUY signal
            state = log_trade_reason(
                state,
                instrument,
                'BUY',
                current_price,
                random.choice([10, 20, 50]), # Random quantity
                'SMA_CROSS_UP',
                f"Price {current_price:.2f} crossed above {window_size}-period SMA {current_sma:.2f}.",
                tags=['technical', 'trend_following']
            )
            prev_price_above_sma = True
        elif current_price < current_sma and prev_price_above_sma:
            # Price crossed below SMA: SELL signal
            state = log_trade_reason(
                state,
                instrument,
                'SELL',
                current_price,
                random.choice([10, 20, 50]), # Random quantity
                'SMA_CROSS_DOWN',
                f"Price {current_price:.2f} crossed below {window_size}-period SMA {current_sma:.2f}.",
                tags=['technical', 'trend_following']
            )
            prev_price_above_sma = False

    logger.info(f"Finished simulating trades for {instrument}.")
    return state

5. Demonstration/Visualization

This section demonstrates the usage of the core functions by simulating a trading scenario, logging various trade reasons, and then visualizing the aggregated results. We will create mock market data, apply a simple trading strategy, and display the logged trade reasons in both tabular and graphical formats.

[8]
# 1. Initialize the trade log state
trade_log_state = create_trade_log_state()

# 2. Log some initial manual trade reasons (before simulation)
trade_log_state = log_trade_reason(
    trade_log_state, 'ETHUSD', 'BUY', 1800.50, 0.5,
    'MANUAL_ENTRY', 'Initial discretionary buy order based on market sentiment.',
    tags=['manual', 'sentiment']
)
trade_log_state = log_trade_reason(
    trade_log_state, 'BTCUSD', 'SELL', 30500.00, 0.1,
    'RISK_REDUCTION', 'Reducing exposure due to upcoming economic news.',
    tags=['risk_management', 'macro']
)

# 3. Create simulated market data for multiple instruments
sim_data_a = create_simulated_market_data('2023-01-01', '2023-01-10', 'STOCK_A', 100.0, 0.005)
sim_data_b = create_simulated_market_data('2023-01-01', '2023-01-10', 'STOCK_B', 50.0, 0.01)
sim_data_c = create_simulated_market_data('2023-01-01', '2023-01-10', 'STOCK_C', 200.0, 0.003)

# Combine data for simulation
combined_sim_data = pd.concat([sim_data_a, sim_data_b, sim_data_c])

# 4. Simulate trading strategy and log reasons
trade_log_state = simulate_trading_strategy(trade_log_state, sim_data_a, window_size=5)
trade_log_state = simulate_trading_strategy(trade_log_state, sim_data_b, window_size=15)
trade_log_state = simulate_trading_strategy(trade_log_state, sim_data_c, window_size=8)

# 5. Retrieve all logged trade reasons into a DataFrame
all_trade_reasons_df = get_trade_reasons_df(trade_log_state)

print("\n--- All Logged Trade Reasons ---")
display(all_trade_reasons_df.head(10))
print(f"Total unique reason codes: {all_trade_reasons_df['reason_code'].nunique()}")
print(f"Total unique instruments: {all_trade_reasons_df['instrument'].nunique()}")
/tmp/ipykernel_8557/2976738080.py:39: FutureWarning: 'H' is deprecated and will be removed in a future version, please use 'h' instead.
  dates = pd.date_range(start=start_date, end=end_date, freq='H')
Simulating STOCK_A trades:   0%|          | 0/212 [00:00<?, ?it/s]
Simulating STOCK_B trades:   0%|          | 0/202 [00:00<?, ?it/s]
Simulating STOCK_C trades:   0%|          | 0/209 [00:00<?, ?it/s]

--- All Logged Trade Reasons ---
timestamp instrument direction price quantity reason_code description tags
0 2026-06-12 10:40:53.726612 ETHUSD BUY 1800.500000 0.5 MANUAL_ENTRY Initial discretionary buy order based on marke... [manual, sentiment]
1 2026-06-12 10:40:53.726752 BTCUSD SELL 30500.000000 0.1 RISK_REDUCTION Reducing exposure due to upcoming economic news. [risk_management, macro]
2 2026-06-12 10:40:53.797186 STOCK_A BUY 98.203843 10.0 SMA_CROSS_UP Price 98.20 crossed above 5-period SMA 97.73. [technical, trend_following]
3 2026-06-12 10:40:53.802239 STOCK_A SELL 97.940394 50.0 SMA_CROSS_DOWN Price 97.94 crossed below 5-period SMA 98.33. [technical, trend_following]
4 2026-06-12 10:40:53.808649 STOCK_A BUY 97.427896 20.0 SMA_CROSS_UP Price 97.43 crossed above 5-period SMA 97.26. [technical, trend_following]
5 2026-06-12 10:40:53.814018 STOCK_A SELL 97.320291 20.0 SMA_CROSS_DOWN Price 97.32 crossed below 5-period SMA 97.35. [technical, trend_following]
6 2026-06-12 10:40:53.815112 STOCK_A BUY 97.419052 50.0 SMA_CROSS_UP Price 97.42 crossed above 5-period SMA 97.35. [technical, trend_following]
7 2026-06-12 10:40:53.817927 STOCK_A SELL 97.361365 20.0 SMA_CROSS_DOWN Price 97.36 crossed below 5-period SMA 97.48. [technical, trend_following]
8 2026-06-12 10:40:53.826713 STOCK_A BUY 96.133343 50.0 SMA_CROSS_UP Price 96.13 crossed above 5-period SMA 95.93. [technical, trend_following]
9 2026-06-12 10:40:53.829013 STOCK_A SELL 95.892047 50.0 SMA_CROSS_DOWN Price 95.89 crossed below 5-period SMA 96.03. [technical, trend_following]
Total unique reason codes: 4
Total unique instruments: 5

Visualization of Trade Reasons

We will now visualize the logged trade reasons to gain insights into their distribution and patterns. This helps in understanding which reasons are most frequent and how they are distributed across instruments and time.

[9]
if not all_trade_reasons_df.empty:
    # Plot 1: Distribution of Trade Reason Codes
    plt.figure(figsize=(12, 6))
    sns.countplot(y='reason_code', data=all_trade_reasons_df, order=all_trade_reasons_df['reason_code'].value_counts().index, palette='viridis')
    plt.title('Distribution of Trade Reason Codes')
    plt.xlabel('Number of Trades')
    plt.ylabel('Reason Code')
    plt.tight_layout()
    plt.show()

    # Plot 2: Trades Over Time by Instrument
    plt.figure(figsize=(14, 7))
    sns.scatterplot(
        x='timestamp',
        y='price',
        hue='instrument',
        size='quantity',
        style='direction',
        data=all_trade_reasons_df.sort_values('timestamp'),
        sizes=(50, 500), # Size range for quantity
        alpha=0.7
    )
    plt.title('Trades Over Time by Instrument, Price, and Quantity')
    plt.xlabel('Timestamp')
    plt.ylabel('Price')
    plt.xticks(rotation=45)
    plt.grid(True, linestyle='--', alpha=0.6)
    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
    plt.tight_layout()
    plt.show()

    # Plot 3: Trade Direction Breakdown by Instrument
    plt.figure(figsize=(12, 6))
    sns.countplot(x='instrument', hue='direction', data=all_trade_reasons_df, palette='coolwarm')
    plt.title('Trade Direction Breakdown by Instrument')
    plt.xlabel('Instrument')
    plt.ylabel('Number of Trades')
    plt.legend(title='Direction')
    plt.tight_layout()
    plt.show()

    # Plot 4: Average Quantity by Reason Code
    avg_qty_by_reason = all_trade_reasons_df.groupby('reason_code')['quantity'].mean().sort_values(ascending=False).reset_index()
    plt.figure(figsize=(12, 6))
    sns.barplot(x='quantity', y='reason_code', data=avg_qty_by_reason, palette='plasma')
    plt.title('Average Trade Quantity by Reason Code')
    plt.xlabel('Average Quantity')
    plt.ylabel('Reason Code')
    plt.tight_layout()
    plt.show()
else:
    logger.info("No trade reasons to visualize.")
/tmp/ipykernel_8557/3197471329.py:4: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `y` variable to `hue` and set `legend=False` for the same effect.

  sns.countplot(y='reason_code', data=all_trade_reasons_df, order=all_trade_reasons_df['reason_code'].value_counts().index, palette='viridis')
cell output
cell output
cell output
/tmp/ipykernel_8557/3197471329.py:45: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `y` variable to `hue` and set `legend=False` for the same effect.

  sns.barplot(x='quantity', y='reason_code', data=avg_qty_by_reason, palette='plasma')
cell output

7. Conclusion

This notebook has demonstrated a comprehensive approach to logging human-readable trade reasons within a Google Colab environment. We covered the following key components:

  • Structured Logging: Implemented functions to log trade reasons with a clear, consistent structure, including metadata like timestamp, instrument, price, quantity, reason code, description, and tags.
  • State Management: Utilized simple Python dictionaries to maintain the state of logged trade reasons, adhering to the 'no classes' requirement.
  • Data Simulation: Generated synthetic market data to simulate realistic trading scenarios and trigger trade events.
  • Strategy Integration: Showcased how log_trade_reason can be integrated into a basic trading strategy to capture the rationale behind automated trades.
  • Visualization: Presented various plots, including reason code distribution, trades over time, and direction breakdown, to visualize and analyze the logged data effectively.
  • Production Considerations: Outlined important best practices for deploying such logging mechanisms in a robust, scalable, and maintainable production system.

By following these principles, trading systems can generate invaluable data for post-trade analysis, compliance, and continuous improvement of trading strategies, ultimately leading to better decision-making and performance.