Agent Based Market Sim
Build an agent-based artificial market simulation populated with heterogeneous trading agents following diverse strategies to generate realistic emergent macro-level market dynamics and statistical properties from micro-level agent interaction rules for strategy robustness testing across market regimes.
Agent-Based Market Simulation — Research & Experimentation
Category: Research & Experimentation | Subcategory: Simulation
What This Notebook Does
Agent-based models (ABMs) simulate financial markets as the aggregate outcome of many individual agents — each following simple rules — rather than assuming the market reaches an equilibrium price. This bottom-up approach can reproduce emergent market phenomena that top-down models miss, including:
- Fat-tailed return distributions: even agents with Gaussian noise produce crashes when herding behaviour kicks in
- Volatility clustering: periods of high volatility cluster together because momentum agents amplify small moves
- Flash crashes: a sudden liquidity vacuum created by simultaneous stop-loss triggers
- Market microstructure effects: bid-ask spreads emerging from order flow
This notebook implements a simplified ABM with three agent types that represent common participant archetypes in crypto markets:
- Fundamentalists: buy when price is below a fundamental value, sell when above — they provide mean-reversion force
- Trend followers (chartists): buy when price is rising, sell when falling — they amplify momentum
- Noise traders: random order flow with no signal — they add volatility and prevent the market from being too predictable
This notebook:
- Defines all three agent types with parameterised behaviour rules
- Runs the market simulation for N_STEPS time steps
- Analyses the resulting price process for stylised facts
- Tests how changing agent composition affects market dynamics
- Visualises price path, return distribution, and agent activity
- Exports simulation results
!pip install numpy pandas matplotlib seaborn scipy --quietimport numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')Imports ready.
Section 1 — Configuration
N_FUNDAMENTALISTS, N_TREND_FOLLOWERS, and N_NOISE_TRADERS determine the market composition. The ratio of these groups dramatically affects emergent dynamics: a market with many trend followers will have stronger momentum and more extreme crashes; a fundamentalist-dominated market is more efficient. FUNDAMENTAL_VALUE is the intrinsic value fundamentalists target.
N_STEPS = 1000 # simulation steps (trading days)
N_FUNDAMENTALISTS = 50
N_TREND_FOLLOWERS = 100
N_NOISE_TRADERS = 80
INITIAL_PRICE = 30_000
FUNDAMENTAL_VALUE = 32_000 # fundamentalists anchor on this
# Agent strength parameters
FUND_STRENGTH = 0.003 # how hard fundamentalists push price toward fundamental value
TREND_LOOKBACK = 5 # trend followers look back this many steps
TREND_STRENGTH = 0.002 # how aggressively trend followers chase momentum
NOISE_SCALE = 0.010 # noise trader daily order size
SEED = 42
print(f'Agents: {N_FUNDAMENTALISTS} fundamentalists, {N_TREND_FOLLOWERS} trend followers, '
f'{N_NOISE_TRADERS} noise traders')Agents: 50 fundamentalists, 100 trend followers, 80 noise traders
Section 2 — Agent Definitions
Each agent type is a function that takes the current price history and returns an order (positive = buy, negative = sell, measured in units of price impact). The fundamentalist order is proportional to the gap between current price and fundamental value. The trend follower order is proportional to the recent N-step price change. The noise trader places a random order with no information.
Agent Function Explanations
-
fundamentalist_order(price_history, fundamental, strength, rng): This function simulates a fundamentalist agent. It calculates an order based on the difference between the current market price and a predefinedfundamentalvalue. If the price is below the fundamental value, the agent places a buy order; if it's above, a sell order. Thestrengthparameter controls how aggressively the agent pushes the price towards the fundamental value, andrngintroduces a small amount of random noise to the order. -
trend_follower_order(price_history, lookback, strength, rng): This function represents a trend-following agent. It examines the price movement over a specifiedlookbackperiod to determine momentum. If the price has been rising, the agent places a buy order to follow the trend; if falling, a sell order. Thestrengthdictates the aggressiveness of the trend-following behavior, andrngadds a random component. -
noise_trader_order(scale, rng): This function models a noise trader. These agents place random buy or sell orders with no underlying signal or strategy. Thescaleparameter determines the typical size of these random orders, andrnggenerates the random order. Noise traders introduce unpredictable fluctuations into the market.
def fundamentalist_order(price_history, fundamental, strength, rng):
"""
Order from a fundamentalist agent: buy if below fundamental value, sell if above.
Parameters
----------
price_history : list Recent prices.
fundamental : float Fundamental value.
strength : float Order scaling factor.
rng : Generator Random number generator (for noise).
Returns
-------
float Signed order (positive=buy, negative=sell).
"""
price = price_history[-1]
gap = (fundamental - price) / fundamental
return strength * gap * (1 + rng.standard_normal() * 0.2)
def trend_follower_order(price_history, lookback, strength, rng):
"""
Order from a trend-following agent: extrapolate recent price momentum.
Parameters
----------
price_history : list Recent prices (must have >= lookback+1 elements).
lookback : int Window for momentum signal.
strength : float Order scaling factor.
rng : Generator Random number generator (for noise).
Returns
-------
float Signed order.
"""
if len(price_history) <= lookback:
return rng.standard_normal() * strength * 0.1
past_return = (price_history[-1] - price_history[-lookback-1]) / price_history[-lookback-1]
return strength * past_return * (1 + rng.standard_normal() * 0.3)
def noise_trader_order(scale, rng):
"""
Random order from a noise trader.
Parameters
----------
scale : float Standard deviation of order size.
rng : Generator Random number generator.
Returns
-------
float Signed random order.
"""
return rng.standard_normal() * scale
print('Agent functions defined.')Agent functions defined.
Section 3 — Market Simulation Loop
At each time step: (1) each agent places an order, (2) all orders are aggregated into net order flow, (3) price is updated as P_new = P_old × (1 + net_flow). This is a simplified Walrasian price-clearing mechanism — positive net flow pushes price up, negative flow pushes it down. We also record the order contribution of each agent type to analyse market dynamics.
rng = np.random.default_rng(SEED)
prices = [INITIAL_PRICE]
fund_flow = []
trend_flow = []
noise_flow = []
for t in range(N_STEPS):
# Aggregate orders from all agents
f_total = sum(fundamentalist_order(prices, FUNDAMENTAL_VALUE, FUND_STRENGTH, rng)
for _ in range(N_FUNDAMENTALISTS))
tr_total = sum(trend_follower_order(prices, TREND_LOOKBACK, TREND_STRENGTH, rng)
for _ in range(N_TREND_FOLLOWERS))
n_total = sum(noise_trader_order(NOISE_SCALE, rng)
for _ in range(N_NOISE_TRADERS))
net_flow = f_total + tr_total + n_total
new_price = prices[-1] * (1 + net_flow)
prices.append(max(new_price, 1.0)) # price cannot go negative
fund_flow.append(f_total)
trend_flow.append(tr_total)
noise_flow.append(n_total)
idx = pd.date_range('2022-01-01', periods=N_STEPS+1, freq='B')
prices_s = pd.Series(prices, index=idx)
returns = prices_s.pct_change().dropna()
print(f'Simulation complete: {N_STEPS} steps')
print(f'Ann Vol: {returns.std()*np.sqrt(252):.1%} | Kurtosis: {stats.kurtosis(returns):.2f} | Skew: {stats.skew(returns):.2f}')Simulation complete: 1000 steps Ann Vol: 218.7% | Kurtosis: 3.16 | Skew: 0.05
Section 4 — Stylised Facts Validation
A good market model should reproduce the stylised facts of real return series: fat tails (excess kurtosis > 0), negative skewness (crashes are worse than rallies), and volatility clustering (large moves cluster together). We test all three. Volatility clustering is measured by the autocorrelation of squared returns — significant positive autocorrelation in squared returns is the ARCH effect.
from statsmodels.stats.diagnostic import acorr_ljungbox
kurt = stats.kurtosis(returns)
skewn = stats.skew(returns)
lb_sq = acorr_ljungbox(returns**2, lags=[10], return_df=True)['lb_pvalue'].values[0]
print('=== Stylised Facts Check ===')
print(f'Excess Kurtosis: {kurt:.2f} (target > 0, fat tails)')
print(f'Skewness: {skewn:.2f} (negative = left tail)')
print(f'ARCH effect (LB p-val): {lb_sq:.4f} (< 0.05 = volatility clustering)')
print(f'\nFat tails: {"PASS" if kurt > 0 else "FAIL"}')
print(f'Vol clustering: {"PASS" if lb_sq < 0.05 else "FAIL"}')=== Stylised Facts Check === Excess Kurtosis: 3.16 (target > 0, fat tails) Skewness: 0.05 (negative = left tail) ARCH effect (LB p-val): 0.0000 (< 0.05 = volatility clustering) Fat tails: PASS Vol clustering: PASS
Section 5 — Visualisation
The left panel shows the simulated price path with stacked bars showing each agent type's contribution to order flow — this reveals when trend followers dominate (sustained uptrends) vs when fundamentalists prevail (sharp reversals back to fair value). The right panel compares the return distribution to a fitted normal curve, showing the fat tails produced by the model.
fig, axes = plt.subplots(2, 2, figsize=(14, 9))
fig.suptitle('Agent-Based Market Simulation', fontsize=13, fontweight='bold')
ax1 = axes[0, 0]
ax1.plot(prices_s.index, prices_s, color='#1976d2', lw=1.5)
ax1.axhline(FUNDAMENTAL_VALUE, color='red', ls='--', lw=1, label=f'Fundamental = {FUNDAMENTAL_VALUE:,.0f}')
ax1.set_ylabel('Price'); ax1.legend(fontsize=8)
ax1.set_title('Simulated Price Path')
ax2 = axes[0, 1]
t_range = range(min(200, N_STEPS))
ax2.fill_between(t_range, fund_flow[:200], alpha=0.7, color='#43a047', label='Fundamentalists')
ax2.fill_between(t_range, trend_flow[:200], alpha=0.7, color='#e53935', label='Trend followers')
ax2.fill_between(t_range, noise_flow[:200], alpha=0.5, color='#9e9e9e', label='Noise traders')
ax2.axhline(0, color='black', lw=0.8, ls='--')
ax2.set_ylabel('Aggregate Order Flow')
ax2.legend(fontsize=8, loc='upper right')
ax2.set_title('Agent Order Flow (First 200 Steps)')
ax3 = axes[1, 0]
ax3.hist(returns, bins=60, density=True, color='#1976d2', alpha=0.7, label='Simulated')
x = np.linspace(returns.min(), returns.max(), 200)
ax3.plot(x, stats.norm.pdf(x, returns.mean(), returns.std()),
color='black', lw=2, ls='--', label='Normal fit')
ax3.set_xlabel('Daily Return')
ax3.legend(fontsize=8)
ax3.set_title(f'Return Distribution (Kurt={kurt:.2f})')
ax4 = axes[1, 1]
roll_vol = returns.rolling(21).std() * np.sqrt(252)
ax4.plot(roll_vol.index, roll_vol, color='#fb8c00', lw=1.5)
ax4.set_ylabel('Rolling 21d Ann. Vol')
ax4.set_title('Volatility Clustering')
plt.tight_layout(); plt.show()Section 6 — Export
Save the simulated price path and agent order flow time series. The order flow data is useful for microstructure research — for example, measuring the correlation between trend-follower order flow and next-period price change.
sim_out = pd.DataFrame({
'price': prices_s.iloc[1:],
'return': returns,
'fund_flow': fund_flow,
'trend_flow': trend_flow,
'noise_flow': noise_flow
})
sim_out.to_csv('agent_based_market_sim.csv')
print('Saved: agent_based_market_sim.csv')Saved: agent_based_market_sim.csv
Conclusion
This notebook demonstrates a simple agent-based market simulation, showcasing how emergent market phenomena can arise from the interactions of different agent types. By adjusting the composition and parameters of fundamentalists, trend followers, and noise traders, one can observe various market behaviors, including fat tails and volatility clustering.