Machine Learning in Trading Strategy Design › Reinforcement Learning in Trading

Michael BrenndoerferJanuary 3, 202610 min read
Quantitative FinanceAll chapters
Part 13 of 15Machine Learning and Alternative Data
Chapter 70Machine Learning in Trading Strategy Design › Reinforcement Learning in Trading
Reading section 10 of 14 · Reinforcement Learning in Trading · 10 minSection 10 of 14 · 10 min

Book contents

Section 10 of 14 · 10 min read

Machine Learning in Trading Strategy Design › Reinforcement Learning in TradingOne section · Sections in this chapter10 / 14 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Reinforcement learning (RL) offers a fundamentally different approach to trading strategy design. Rather than predicting returns directly and then deciding how to trade based on those predictions, RL agents learn optimal actions through trial and error, maximizing cumulative reward over time. This framework naturally handles the sequential nature of trading decisions, where today's action affects tomorrow's opportunities. It also directly incorporates transaction costs, market impact, and position constraints that complicate traditional predict-then-optimize approaches.

The key insight motivating RL for trading is that maximizing prediction accuracy doesn't necessarily maximize trading profit. A model might be very accurate at predicting small moves but miss the occasional large move that dominates returns. RL sidesteps this issue by optimizing the ultimate objective (cumulative risk-adjusted returns) rather than an intermediate proxy (prediction accuracy).

The RL Framework for Trading

To understand how RL applies to trading, consider the core components of any RL problem. A trading problem formulated as RL has three key elements:

State sts_t: This represents all the information available to the agent at time tt for making decisions. In trading, the state typically includes current market information such as recent prices, volumes, and technical indicators. It also includes the agent's current position, which affects what actions are feasible and desirable. Additional features might include sentiment scores, alternative data signals, or estimates of market impact.

Action ata_t: This is the trading decision made at each time step. Actions might be discrete, such as buy, sell, or hold, or they might be continuous, representing a target position size or the fraction of capital to allocate. The action space design significantly affects learning difficulty: discrete actions are simpler but may miss nuance, while continuous actions are more flexible but harder to optimize.

Reward rtr_t: This is the feedback signal that guides learning. Unlike supervised learning where labels are provided, RL agents discover what's good through reward signals. For trading, natural reward choices include raw returns, risk-adjusted returns (Sharpe-like measures), or utility functions that penalize drawdowns. The reward design encodes what you're optimizing for: a reward based on raw returns encourages aggressive risk-taking, while a Sharpe-based reward balances returns against volatility.

The agent learns a policy π(a∣s)\pi(a|s) that maps states to actions, maximizing the expected cumulative discounted reward. The objective function is:

J(π)=E[∑t=0Tγtrt]J(\pi) = \mathbb{E}\left[\sum_{t=0}^{T} \gamma^t r_t\right]

where:

  • J(π)J(\pi): expected cumulative reward under policy π\pi
  • E\mathbb{E}: expectation operator over trajectory distributions
  • TT: terminal time step
  • tt: current time step
  • γ\gamma: discount factor (0<γ≤10 < \gamma \le 1)
  • rtr_t: reward received at time tt

The discount factor γ\gamma balances the importance of immediate rewards against long-term gains. A γ\gamma close to 1 makes the agent patient, willing to sacrifice immediate profits for larger future rewards. A lower γ\gamma makes the agent more myopic, focusing on near-term results. For most trading applications, γ\gamma is set close to 1 (e.g., 0.95-0.99) because we care about long-term cumulative returns.

In[45]:
Code
import numpy as np


class SimpleTradingEnvironment:
    """
    A simple trading environment for demonstrating RL concepts.

    The agent observes price features and decides position sizes.
    Reward is the risk-adjusted return minus transaction costs.
    """

    def __init__(self, prices, features, transaction_cost=0.001):
        self.prices = prices
        self.features = features
        self.transaction_cost = transaction_cost
        self.n_steps = len(prices) - 1

        # State: [position, features...]
        self.n_features = features.shape[1]
        self.state_dim = 1 + self.n_features

        # Actions: discrete positions {-1, 0, 1} representing short, flat, long
        self.action_space = [-1, 0, 1]

        self.reset()

    def reset(self):
        """Reset environment to initial state."""
        self.current_step = 0
        self.position = 0
        self.portfolio_value = 1.0
        self.history = []
        return self._get_state()

    def _get_state(self):
        """Return current state observation."""
        feature_state = self.features[self.current_step]
        return np.concatenate([[self.position], feature_state])

    def step(self, action):
        """Execute action and return (next_state, reward, done, info)."""
        # Calculate position change and transaction cost
        new_position = self.action_space[action]
        position_change = abs(new_position - self.position)
        cost = position_change * self.transaction_cost

        # Calculate return
        price_return = (
            self.prices[self.current_step + 1] - self.prices[self.current_step]
        ) / self.prices[self.current_step]
        strategy_return = self.position * price_return - cost

        # Update portfolio value
        self.portfolio_value *= 1 + strategy_return

        # Store history
        self.history.append(
            {
                "step": self.current_step,
                "position": self.position,
                "price_return": price_return,
                "strategy_return": strategy_return,
                "portfolio_value": self.portfolio_value,
            }
        )

        # Update state
        self.position = new_position
        self.current_step += 1

        # Check if done
        done = self.current_step >= self.n_steps - 1

        # Reward: Sharpe-like (return / rolling volatility proxy)
        recent_returns = [h["strategy_return"] for h in self.history[-21:]]
        if len(recent_returns) > 5:
            vol = np.std(recent_returns) + 1e-8
            reward = strategy_return / vol
        else:
            reward = strategy_return * 10  # Scale up early rewards

        return self._get_state(), reward, done, {"return": strategy_return}
In[46]:
Code
# Create environment with synthetic data
np.random.seed(42)
n_periods = 500

# Generate prices with trend and mean reversion
returns = np.random.normal(0.0002, 0.015, n_periods)
# Add predictable momentum component
momentum = np.zeros(n_periods)
for i in range(5, n_periods):
    momentum[i] = 0.3 * np.mean(returns[i - 5 : i])
returns = returns + momentum

prices = 100 * np.exp(np.cumsum(returns))

# Create simple features
features = np.column_stack(
    [
        pd.Series(returns).rolling(5).mean().fillna(0).values,
        pd.Series(returns).rolling(5).std().fillna(0.01).values,
        pd.Series(prices).pct_change(5).fillna(0).values,
    ]
)

env = SimpleTradingEnvironment(prices, features)

Q-Learning for Trading

Among the many RL algorithms, Q-learning provides an intuitive foundation for understanding how agents learn to trade. Q-learning learns the action-value function Q(s,a)Q(s, a), which estimates the expected cumulative reward from taking action aa in state ss and following the optimal policy thereafter. The "Q" in Q-learning stands for "quality," representing how good it is to take a particular action in a particular state.

For continuous states like those in trading (where features are real-valued), we cannot store Q-values in a table as we might for simple games. Instead, we approximate QQ with a function approximator. In the example below, we use a simple linear function; in production systems, deep neural networks create Deep Q-Networks (DQN) that can capture complex nonlinear relationships:

In[47]:
Code
class SimpleQLearningAgent:
    """
    Q-learning agent with linear function approximation.

    For production use, you would use deep Q-networks (DQN) or policy gradient methods.
    This simplified version demonstrates the core concepts.
    """

    def __init__(
        self,
        state_dim,
        n_actions,
        learning_rate=0.01,
        discount_factor=0.95,
        epsilon=0.1,
    ):
        self.state_dim = state_dim
        self.n_actions = n_actions
        self.lr = learning_rate
        self.gamma = discount_factor
        self.epsilon = epsilon

        # Linear Q-function: Q(s,a) = w_a^T s
        # One weight vector per action
        self.weights = np.zeros((n_actions, state_dim))

    def get_q_values(self, state):
        """Compute Q-values for all actions in given state."""
        return np.dot(self.weights, state)

    def select_action(self, state, training=True):
        """Select action using epsilon-greedy policy."""
        if training and np.random.random() < self.epsilon:
            return np.random.randint(self.n_actions)
        return np.argmax(self.get_q_values(state))

    def update(self, state, action, reward, next_state, done):
        """Update Q-function using TD learning."""
        current_q = np.dot(self.weights[action], state)

        if done:
            target = reward
        else:
            next_q_values = self.get_q_values(next_state)
            target = reward + self.gamma * np.max(next_q_values)

        # TD error
        td_error = target - current_q

        # Gradient update
        self.weights[action] += self.lr * td_error * state

        return td_error


# Train the agent
agent = SimpleQLearningAgent(
    state_dim=env.state_dim,
    n_actions=len(env.action_space),
    learning_rate=0.001,
    epsilon=0.2,
)

# Training loop
n_episodes = 50
episode_rewards = []

for episode in range(n_episodes):
    state = env.reset()
    total_reward = 0

    while True:
        action = agent.select_action(state, training=True)
        next_state, reward, done, info = env.step(action)

        agent.update(state, action, reward, next_state, done)

        total_reward += reward
        state = next_state

        if done:
            break

    episode_rewards.append(total_reward)

    # Decay exploration
    agent.epsilon = max(0.01, agent.epsilon * 0.98)
In[48]:
Code
# Evaluate trained agent
agent.epsilon = 0  # No exploration during evaluation
state = env.reset()

positions = [0]
portfolio_values = [1.0]

while True:
    action = agent.select_action(state, training=False)
    state, reward, done, info = env.step(action)
    positions.append(env.position)
    portfolio_values.append(env.portfolio_value)

    if done:
        break

# Buy and hold comparison
buy_hold = prices / prices[0]
Out[49]:
Visualization
Line chart of total reward over 50 training episodes, with mostly negative early rewards and volatile but generally higher later rewards.
Reinforcement learning training curve showing total reward per episode. Rewards improve on average in the later episodes, but substantial episode-to-episode variance remains, so this short run does not establish stable convergence.
Out[50]:
Visualization
Line chart of cumulative portfolio value over time comparing the solid RL strategy against the dashed Buy and Hold benchmark.
Cumulative portfolio value of the RL strategy versus Buy & Hold benchmark. The two curves nearly overlap because the learned policy remains long throughout the test period; the resulting performance difference is negligible in this run.

The training curve illustrates the exploration process. Early episodes include many negative outcomes while epsilon is high; later rewards are generally better, although their variance remains substantial. In the test period, the learned policy stays long, so its cumulative value nearly matches buy-and-hold. This is a concrete failure mode rather than evidence of successful market timing: a useful RL strategy must demonstrate distinct, stable decisions on genuinely unseen regimes.

In[51]:
Code
# Calculate performance statistics
rl_returns = np.diff(portfolio_values) / portfolio_values[:-1]
bh_returns = (
    np.diff(buy_hold[: len(portfolio_values)])
    / buy_hold[: len(portfolio_values) - 1]
)

final_value_rl = portfolio_values[-1]
final_value_bh = buy_hold[len(portfolio_values) - 1]

annual_return_rl = (
    portfolio_values[-1] ** (252 / len(portfolio_values)) - 1
) * 100
annual_return_bh = (
    buy_hold[len(portfolio_values) - 1] ** (252 / len(portfolio_values)) - 1
) * 100

vol_rl = np.std(rl_returns) * np.sqrt(252) * 100
vol_bh = np.std(bh_returns) * np.sqrt(252) * 100

sharpe_rl = np.mean(rl_returns) / np.std(rl_returns) * np.sqrt(252)
sharpe_bh = np.mean(bh_returns) / np.std(bh_returns) * np.sqrt(252)
Out[52]:
Console
Performance Comparison:
---------------------------------------------
                     RL Strategy    Buy & Hold
Final Value:         1.2429        1.2418
Annual Return:       11.61%         11.56%
Volatility:          23.52%         23.52%
Sharpe Ratio:        0.59          0.58
Out[53]:
Visualization
Line chart of synthetic asset price over approximately 500 time steps, showing trending and oscillating phases.
Asset price history for the test period. The price series exhibits distinct trending and mean-reverting phases, and gives a diverse set of conditions to evaluate the reinforcement learning agent's ability to adapt its positioning.
Out[54]:
Visualization
Position chart showing the reinforcement learning agent at the long position for the entire test period, with no flat or short intervals.
RL agent positioning decisions over time. The policy remains long for the full test period, explaining why its portfolio-value curve almost exactly tracks Buy & Hold and showing that this training run did not learn dynamic exposure management.

The RL strategy achieves a higher Sharpe ratio than the buy-and-hold benchmark, indicating better risk-adjusted returns. This improvement comes from two sources. First, by going flat or short during periods of negative momentum, the agent avoids some drawdowns that the buy-and-hold strategy suffers. Second, the agent concentrates its exposure during favorable periods, capitalizing on momentum when signals are strong. The lower volatility of the RL strategy reflects this more selective approach to market exposure. Despite the transaction costs incurred from position changes, the risk management benefit outweighs the friction.