1758 lines
77 KiB
Python
1758 lines
77 KiB
Python
"""
|
|
Game Theory Analysis: Retail Traders vs Big Players
|
|
Models the strategic interaction between retail traders (driven by FOMO/group psychology)
|
|
and institutional players in financial markets.
|
|
|
|
Key Features:
|
|
- Retail traders exhibit FOMO behavior (herding, momentum following)
|
|
- Big players act strategically to exploit retail behavior
|
|
- Finite repeated game (not infinite, as big players are human)
|
|
- Nash equilibrium analysis
|
|
- Order book model: realistic price impact based on order book depth
|
|
- Big players have more capital and their trades move markets through order book consumption
|
|
"""
|
|
import numpy as np
|
|
import matplotlib
|
|
matplotlib.use('Agg') # Use non-interactive backend
|
|
import matplotlib.pyplot as plt
|
|
import pandas as pd
|
|
from scipy.optimize import minimize, differential_evolution
|
|
from scipy.stats import norm
|
|
import itertools
|
|
from collections import defaultdict
|
|
|
|
class RetailTrader:
|
|
"""Models retail trader behavior with FOMO and group psychology"""
|
|
|
|
def __init__(self, base_risk_aversion=0.5, fomo_sensitivity=0.3,
|
|
herd_tendency=0.4, memory_decay=0.9):
|
|
self.base_risk_aversion = base_risk_aversion
|
|
self.fomo_sensitivity = fomo_sensitivity
|
|
self.herd_tendency = herd_tendency
|
|
self.memory_decay = memory_decay
|
|
self.price_memory = []
|
|
self.sentiment = 0.0 # -1 (bearish) to +1 (bullish)
|
|
|
|
def update_sentiment(self, price_change, market_momentum, retail_activity):
|
|
"""Update sentiment based on FOMO and herding"""
|
|
# FOMO component: stronger reaction to positive moves
|
|
fomo_component = self.fomo_sensitivity * np.tanh(price_change * 10)
|
|
|
|
# Herding component: follow the crowd
|
|
herd_component = self.herd_tendency * np.tanh(retail_activity * 5)
|
|
|
|
# Momentum component
|
|
momentum_component = 0.2 * np.tanh(market_momentum * 3)
|
|
|
|
# Update sentiment with memory decay
|
|
new_sentiment = fomo_component + herd_component + momentum_component
|
|
self.sentiment = self.memory_decay * self.sentiment + (1 - self.memory_decay) * new_sentiment
|
|
self.sentiment = np.clip(self.sentiment, -1, 1)
|
|
|
|
return self.sentiment
|
|
|
|
def decide_action(self, current_price, expected_return, volatility):
|
|
"""Decide trading action based on sentiment and risk"""
|
|
# Risk-adjusted expected utility
|
|
risk_adjusted_return = expected_return - self.base_risk_aversion * volatility**2
|
|
|
|
# Sentiment bias
|
|
sentiment_bias = self.sentiment * (1 - self.base_risk_aversion)
|
|
|
|
# Decision threshold
|
|
decision_score = risk_adjusted_return + sentiment_bias
|
|
|
|
if decision_score > 0.02:
|
|
return 'buy', min(abs(decision_score) * 10, 1.0) # position size
|
|
elif decision_score < -0.02:
|
|
return 'sell', min(abs(decision_score) * 10, 1.0)
|
|
else:
|
|
return 'hold', 0.0
|
|
|
|
|
|
class OrderBook:
|
|
"""Models order book depth and price impact"""
|
|
|
|
def __init__(self, base_liquidity=1000, depth_levels=10,
|
|
liquidity_decay=0.95, liquidity_replenish=0.02):
|
|
"""
|
|
base_liquidity: Base liquidity at each price level
|
|
depth_levels: Number of price levels in order book
|
|
liquidity_decay: How much liquidity is consumed (0-1)
|
|
liquidity_replenish: Rate at which liquidity replenishes per round
|
|
"""
|
|
self.base_liquidity = base_liquidity
|
|
self.depth_levels = depth_levels
|
|
self.liquidity_decay = liquidity_decay
|
|
self.liquidity_replenish = liquidity_replenish
|
|
self.bid_depth = np.ones(depth_levels) * base_liquidity # Liquidity at each level below price
|
|
self.ask_depth = np.ones(depth_levels) * base_liquidity # Liquidity at each level above price
|
|
|
|
def calculate_price_impact(self, volume, direction, current_price):
|
|
"""
|
|
Calculate price impact based on order book consumption
|
|
direction: +1 for buy, -1 for sell
|
|
Returns: price impact as fraction (e.g., 0.01 = 1% move)
|
|
"""
|
|
if volume == 0:
|
|
return 0.0
|
|
|
|
remaining_volume = abs(volume)
|
|
total_impact = 0.0
|
|
price_level = current_price
|
|
|
|
# Determine which side of book to consume
|
|
if direction > 0: # Buying - consume ask side (above current price)
|
|
depth_array = self.ask_depth.copy()
|
|
else: # Selling - consume bid side (below current price)
|
|
depth_array = self.bid_depth.copy()
|
|
|
|
# Consume order book levels
|
|
level_spread = 0.002 # 0.2% price increment per level (increased impact)
|
|
for level in range(self.depth_levels):
|
|
if remaining_volume <= 0:
|
|
break
|
|
|
|
available_liquidity = depth_array[level]
|
|
consumed = min(remaining_volume, available_liquidity)
|
|
|
|
# Price impact increases as we go deeper into the book
|
|
# Impact = (level + 1) * spread * (consumed / available_liquidity)
|
|
# More impact when consuming larger portion of available liquidity
|
|
consumption_ratio = consumed / max(available_liquidity, 1)
|
|
level_impact = (level + 1) * level_spread * consumption_ratio
|
|
total_impact += level_impact
|
|
|
|
# Consume liquidity
|
|
depth_array[level] -= consumed
|
|
remaining_volume -= consumed
|
|
|
|
# If volume exceeds all available depth, add extra impact
|
|
if remaining_volume > 0:
|
|
# Large impact for exceeding available liquidity
|
|
excess_impact = 0.005 * (remaining_volume / self.base_liquidity)
|
|
total_impact += excess_impact
|
|
|
|
# Update order book
|
|
if direction > 0:
|
|
self.ask_depth = depth_array
|
|
else:
|
|
self.bid_depth = depth_array
|
|
|
|
return total_impact * np.sign(direction)
|
|
|
|
def replenish_liquidity(self):
|
|
"""Replenish order book liquidity over time"""
|
|
# Replenish both sides
|
|
self.bid_depth = np.minimum(
|
|
self.bid_depth + self.base_liquidity * self.liquidity_replenish,
|
|
self.base_liquidity
|
|
)
|
|
self.ask_depth = np.minimum(
|
|
self.ask_depth + self.base_liquidity * self.liquidity_replenish,
|
|
self.base_liquidity
|
|
)
|
|
|
|
def get_total_liquidity(self):
|
|
"""Get total available liquidity"""
|
|
return np.sum(self.bid_depth) + np.sum(self.ask_depth)
|
|
|
|
|
|
class BigPlayer:
|
|
"""Models institutional/big player strategic behavior"""
|
|
|
|
def __init__(self, capital=1000000, market_impact_coef=0.001,
|
|
patience=0.7, exploit_retail=True,
|
|
sentiment_threshold=0.6, volume_threshold=20, trade_size_pct=0.20):
|
|
self.capital = capital
|
|
self.market_impact_coef = market_impact_coef
|
|
self.patience = patience # How long to wait before acting
|
|
self.exploit_retail = exploit_retail
|
|
self.position = 0.0
|
|
self.retail_sentiment_history = []
|
|
# Configurable thresholds
|
|
self.sentiment_threshold = sentiment_threshold
|
|
self.volume_threshold = volume_threshold
|
|
self.trade_size_pct = trade_size_pct
|
|
|
|
def observe_retail_behavior(self, retail_sentiment, retail_volume):
|
|
"""Observe and learn from retail behavior"""
|
|
self.retail_sentiment_history.append(retail_sentiment)
|
|
if len(self.retail_sentiment_history) > 20:
|
|
self.retail_sentiment_history.pop(0)
|
|
|
|
def strategic_action(self, current_price, retail_sentiment, retail_volume,
|
|
fundamental_value, game_round, order_book=None):
|
|
"""
|
|
Strategic action based on retail behavior and fundamentals
|
|
Returns: (action, size) where size is in actual units (not normalized)
|
|
"""
|
|
if not self.exploit_retail:
|
|
# Simple fundamental trading
|
|
if current_price < fundamental_value * 0.98:
|
|
# Buy: use 5% of capital
|
|
size = min(100, self.capital * 0.05 / current_price)
|
|
return 'buy', size
|
|
elif current_price > fundamental_value * 1.02:
|
|
# Sell: close position
|
|
size = min(100, abs(self.position))
|
|
return 'sell', size
|
|
else:
|
|
return 'hold', 0.0
|
|
|
|
# Exploit retail FOMO
|
|
avg_retail_sentiment = np.mean(self.retail_sentiment_history) if self.retail_sentiment_history else 0
|
|
|
|
# Strategy: fade extreme retail sentiment
|
|
# Use configurable thresholds
|
|
if avg_retail_sentiment > self.sentiment_threshold and retail_volume > self.volume_threshold:
|
|
# Retail is bullish - sell to them (large size)
|
|
size = min(300, self.capital * self.trade_size_pct / current_price)
|
|
return 'sell', size
|
|
elif avg_retail_sentiment < -self.sentiment_threshold and retail_volume > self.volume_threshold:
|
|
# Retail is bearish - buy from them (large size)
|
|
size = min(300, self.capital * self.trade_size_pct / current_price)
|
|
return 'buy', size
|
|
|
|
# Trade on fundamentals more frequently (less patience)
|
|
if game_round % max(1, int(3 / (1 - self.patience))) == 0: # Trade every 3-10 rounds
|
|
if current_price < fundamental_value * 0.98: # More sensitive
|
|
# Medium size fundamental trade
|
|
size = min(100, self.capital * 0.05 / current_price)
|
|
return 'buy', size
|
|
elif current_price > fundamental_value * 1.02: # More sensitive
|
|
# Medium size fundamental trade
|
|
size = min(100, abs(self.position))
|
|
return 'sell', size
|
|
|
|
return 'hold', 0.0
|
|
|
|
|
|
class TradingGame:
|
|
"""Simulates the repeated game between retail traders and big players"""
|
|
|
|
def __init__(self, num_retail_traders=100, num_big_players=5,
|
|
initial_price=100, fundamental_value=100, volatility=0.02,
|
|
order_book_liquidity=50, fundamental_reversion=0.01,
|
|
big_sentiment_threshold=0.6, big_volume_threshold=20,
|
|
big_trade_size_pct=0.20):
|
|
self.num_retail_traders = num_retail_traders
|
|
self.num_big_players = num_big_players
|
|
self.initial_price = initial_price
|
|
self.fundamental_value = fundamental_value
|
|
self.volatility = volatility
|
|
self.fundamental_reversion = fundamental_reversion
|
|
|
|
# Initialize order book with realistic liquidity
|
|
# Lower liquidity = more price impact from trades
|
|
# 50 units per level means 500 total units (10 levels)
|
|
# Retail can trade 0-200 units total, so this creates meaningful impact
|
|
self.order_book = OrderBook(
|
|
base_liquidity=order_book_liquidity,
|
|
depth_levels=10,
|
|
liquidity_decay=0.95,
|
|
liquidity_replenish=0.05 # Faster replenishment
|
|
)
|
|
|
|
# Create independent random state for this game
|
|
# Use a unique seed based on time, object id, and random component
|
|
import time
|
|
import os
|
|
base_seed = (int(time.time() * 1000000) % (2**31) +
|
|
id(self) % 1000000 +
|
|
os.getpid() * 1000) % (2**31)
|
|
# Add some randomness from global RNG to ensure uniqueness
|
|
try:
|
|
base_seed = (base_seed + np.random.randint(0, 1000000)) % (2**31)
|
|
except:
|
|
pass
|
|
self.rng = np.random.RandomState(base_seed)
|
|
|
|
# Initialize players with game-specific random state
|
|
self.retail_traders = [RetailTrader(
|
|
base_risk_aversion=self.rng.uniform(0.3, 0.7),
|
|
fomo_sensitivity=self.rng.uniform(0.2, 0.5),
|
|
herd_tendency=self.rng.uniform(0.3, 0.6)
|
|
) for _ in range(num_retail_traders)]
|
|
|
|
self.big_players = [BigPlayer(
|
|
capital=self.rng.uniform(500000, 2000000),
|
|
exploit_retail=True,
|
|
sentiment_threshold=big_sentiment_threshold,
|
|
volume_threshold=big_volume_threshold,
|
|
trade_size_pct=big_trade_size_pct
|
|
) for _ in range(num_big_players)]
|
|
|
|
self.price_history = [initial_price]
|
|
self.retail_sentiment_history = []
|
|
self.retail_volume_history = []
|
|
self.big_player_volume_history = []
|
|
self.retail_pnl_history = []
|
|
self.big_player_pnl_history = []
|
|
|
|
# Track cumulative positions for proper PnL calculation
|
|
self.retail_position = 0.0 # Cumulative position (positive = long, negative = short)
|
|
self.big_player_position = 0.0
|
|
|
|
def update_price(self, retail_net_volume, big_player_net_volume,
|
|
fundamental_shock=0):
|
|
"""
|
|
Update price based on trading through order book and fundamentals
|
|
Volumes are in actual units (not normalized)
|
|
"""
|
|
current_price = self.price_history[-1]
|
|
|
|
# Calculate price impact through order book
|
|
# Retail traders aggregate volume can be significant (100 traders * 0-2 units = 0-200 units)
|
|
retail_impact = 0.0
|
|
if retail_net_volume != 0:
|
|
retail_direction = np.sign(retail_net_volume)
|
|
retail_impact = self.order_book.calculate_price_impact(
|
|
abs(retail_net_volume), retail_direction, current_price
|
|
)
|
|
# Order book naturally gives less impact per unit for smaller trades
|
|
# But when retail herds together (FOMO), aggregate volume creates impact
|
|
|
|
# Big players have much larger trades - more impact
|
|
big_impact = 0.0
|
|
if big_player_net_volume != 0:
|
|
big_direction = np.sign(big_player_net_volume)
|
|
# Big players' trades consume more order book depth
|
|
big_impact = self.order_book.calculate_price_impact(
|
|
abs(big_player_net_volume), big_direction, current_price
|
|
)
|
|
# Big players' trades have full impact (they move markets)
|
|
big_impact *= 1.0
|
|
|
|
# Replenish order book liquidity
|
|
self.order_book.replenish_liquidity()
|
|
|
|
# Fundamental mean reversion (much stronger to prevent price explosion)
|
|
# Pull price back toward fundamental value
|
|
price_deviation = (current_price - self.fundamental_value) / self.fundamental_value
|
|
fundamental_drift = -self.fundamental_reversion * price_deviation
|
|
|
|
# Random shock using game's independent random state
|
|
random_shock = self.rng.normal(0, self.volatility)
|
|
|
|
# Price update (multiplicative but bounded)
|
|
total_change = retail_impact + big_impact + fundamental_drift + random_shock + fundamental_shock
|
|
total_change = np.clip(total_change, -0.1, 0.1) # Cap at 10% per round
|
|
new_price = current_price * (1 + total_change)
|
|
|
|
return max(new_price, 0.01) # Prevent negative prices
|
|
|
|
def play_round(self, game_round, fundamental_shock=0):
|
|
"""Play one round of the game"""
|
|
current_price = self.price_history[-1]
|
|
|
|
# Calculate market momentum
|
|
if len(self.price_history) > 1:
|
|
momentum = (self.price_history[-1] - self.price_history[-2]) / self.price_history[-2]
|
|
else:
|
|
momentum = 0
|
|
|
|
# Retail traders decide
|
|
retail_actions = []
|
|
retail_sentiments = []
|
|
retail_net_volume = 0
|
|
|
|
for trader in self.retail_traders:
|
|
expected_return = momentum # Simple expectation
|
|
sentiment = trader.update_sentiment(momentum, momentum,
|
|
len([a for a in retail_actions if a[0] != 'hold']) / max(len(retail_actions), 1))
|
|
action, normalized_size = trader.decide_action(current_price, expected_return, self.volatility)
|
|
|
|
# Convert normalized size to actual units (retail traders trade small)
|
|
# Normalized size is 0-1, convert to 0.1-2 units per trader
|
|
actual_size = normalized_size * 2.0 # Retail traders trade 0-2 units each
|
|
retail_actions.append((action, actual_size))
|
|
retail_sentiments.append(sentiment)
|
|
|
|
if action == 'buy':
|
|
retail_net_volume += actual_size
|
|
elif action == 'sell':
|
|
retail_net_volume -= actual_size
|
|
|
|
avg_retail_sentiment = np.mean(retail_sentiments)
|
|
retail_volume = sum([size for _, size in retail_actions])
|
|
|
|
# Big players observe and act
|
|
big_player_net_volume = 0
|
|
for big_player in self.big_players:
|
|
big_player.observe_retail_behavior(avg_retail_sentiment, retail_volume)
|
|
action, size = big_player.strategic_action(
|
|
current_price, avg_retail_sentiment, retail_volume,
|
|
self.fundamental_value, game_round, order_book=self.order_book
|
|
)
|
|
|
|
# Big players trade in actual units (already calculated based on capital)
|
|
if action == 'buy':
|
|
big_player_net_volume += size
|
|
elif action == 'sell':
|
|
big_player_net_volume -= size
|
|
|
|
# Update price
|
|
new_price = self.update_price(retail_net_volume, big_player_net_volume,
|
|
fundamental_shock)
|
|
|
|
# Calculate PnL properly
|
|
# PnL on existing position: position_at_start * price_change
|
|
# PnL on new trades: new_trades * price_change (they entered at current_price, price moved to new_price)
|
|
# Total PnL = (position_at_start + new_trades) * price_change = position_at_end * price_change
|
|
|
|
price_change_amount = new_price - current_price
|
|
|
|
# Calculate PnL on average position during the round
|
|
# This accounts for both existing positions and new trades
|
|
position_at_start = self.retail_position
|
|
position_at_end = self.retail_position + retail_net_volume
|
|
average_position = (position_at_start + position_at_end) / 2
|
|
|
|
# Retail PnL: average position * price change
|
|
retail_pnl = average_position * price_change_amount
|
|
|
|
# Update retail position for next round
|
|
self.retail_position = position_at_end
|
|
|
|
# Big player PnL: same calculation
|
|
position_at_start_big = self.big_player_position
|
|
position_at_end_big = self.big_player_position + big_player_net_volume
|
|
average_position_big = (position_at_start_big + position_at_end_big) / 2
|
|
|
|
big_player_pnl = average_position_big * price_change_amount
|
|
|
|
# Update big player position for next round
|
|
self.big_player_position = position_at_end_big
|
|
|
|
# Update history
|
|
self.price_history.append(new_price)
|
|
self.retail_sentiment_history.append(avg_retail_sentiment)
|
|
self.retail_volume_history.append(retail_volume)
|
|
self.big_player_volume_history.append(abs(big_player_net_volume))
|
|
self.retail_pnl_history.append(retail_pnl)
|
|
self.big_player_pnl_history.append(big_player_pnl)
|
|
|
|
# Calculate exploitation metric: big players trading opposite to retail sentiment
|
|
# Positive sentiment (bullish retail) -> big players should sell (negative volume)
|
|
# Negative sentiment (bearish retail) -> big players should buy (positive volume)
|
|
exploitation_signal = -np.sign(avg_retail_sentiment) * np.sign(big_player_net_volume) if big_player_net_volume != 0 else 0
|
|
|
|
return {
|
|
'price': new_price,
|
|
'retail_sentiment': avg_retail_sentiment,
|
|
'retail_volume': retail_volume,
|
|
'big_player_volume': abs(big_player_net_volume),
|
|
'big_player_direction': np.sign(big_player_net_volume),
|
|
'retail_pnl': retail_pnl,
|
|
'big_player_pnl': big_player_pnl,
|
|
'exploitation_signal': exploitation_signal
|
|
}
|
|
|
|
def simulate_game(self, num_rounds=100, fundamental_shocks=None):
|
|
"""Simulate the full game"""
|
|
if fundamental_shocks is None:
|
|
fundamental_shocks = [0] * num_rounds
|
|
|
|
results = []
|
|
for round_num in range(num_rounds):
|
|
shock = fundamental_shocks[round_num] if round_num < len(fundamental_shocks) else 0
|
|
result = self.play_round(round_num, shock)
|
|
result['round'] = round_num
|
|
results.append(result)
|
|
|
|
return pd.DataFrame(results)
|
|
|
|
def calculate_nash_equilibrium(self):
|
|
"""Calculate approximate Nash equilibrium"""
|
|
# Simplified Nash equilibrium calculation
|
|
# In equilibrium, big players should not be able to improve by changing strategy
|
|
# given retail behavior, and vice versa
|
|
|
|
# Average retail sentiment in equilibrium
|
|
if len(self.retail_sentiment_history) > 10:
|
|
eq_retail_sentiment = np.mean(self.retail_sentiment_history[-10:])
|
|
else:
|
|
eq_retail_sentiment = 0
|
|
|
|
# Equilibrium price should be close to fundamental when both sides are balanced
|
|
if len(self.price_history) > 10:
|
|
eq_price = np.mean(self.price_history[-10:])
|
|
else:
|
|
eq_price = self.initial_price
|
|
|
|
return {
|
|
'equilibrium_price': eq_price,
|
|
'equilibrium_retail_sentiment': eq_retail_sentiment,
|
|
'price_deviation': abs(eq_price - self.fundamental_value) / self.fundamental_value
|
|
}
|
|
|
|
|
|
def analyze_game_theory(num_simulations=50, num_rounds=100):
|
|
"""Run multiple game simulations and analyze results"""
|
|
all_results = []
|
|
nash_equilibria = []
|
|
|
|
for sim in range(num_simulations):
|
|
# Each game gets completely independent random state
|
|
# Use simulation number + time + random component for unique seed
|
|
import time
|
|
base_seed = int(time.time() * 1000000) % (2**31)
|
|
game_seed = (base_seed + sim * 7919 + np.random.randint(0, 1000000)) % (2**31)
|
|
game_rng = np.random.RandomState(game_seed)
|
|
|
|
# Vary initial conditions for more diversity
|
|
initial_price = 100 + game_rng.normal(0, 2) # Small variation in starting price
|
|
fundamental_value = 100 + game_rng.normal(0, 1) # Small variation in fundamental
|
|
|
|
game = TradingGame(
|
|
num_retail_traders=100,
|
|
num_big_players=5,
|
|
initial_price=initial_price,
|
|
fundamental_value=fundamental_value,
|
|
volatility=0.02
|
|
)
|
|
|
|
# Override game's RNG with our independent one for shocks
|
|
game.rng = game_rng
|
|
|
|
# Add random fundamental shocks - unique per game
|
|
fundamental_shocks = game_rng.normal(0, 0.01, num_rounds)
|
|
# Add random large shocks at random times (not fixed rounds)
|
|
num_large_shocks = game_rng.randint(1, 4) # 1-3 large shocks per game
|
|
shock_times = game_rng.choice(num_rounds, num_large_shocks, replace=False)
|
|
shock_sizes = game_rng.normal(0, 0.03, num_large_shocks) # Random shock sizes
|
|
for time, size in zip(shock_times, shock_sizes):
|
|
fundamental_shocks[time] = size
|
|
|
|
results = game.simulate_game(num_rounds, fundamental_shocks)
|
|
results['simulation'] = sim
|
|
|
|
# Calculate Nash equilibrium
|
|
nash = game.calculate_nash_equilibrium()
|
|
nash['simulation'] = sim
|
|
nash_equilibria.append(nash)
|
|
|
|
all_results.append(results)
|
|
|
|
combined_results = pd.concat(all_results, ignore_index=True)
|
|
nash_df = pd.DataFrame(nash_equilibria)
|
|
|
|
return combined_results, nash_df
|
|
|
|
|
|
def calculate_meta_analysis(results_df, nash_df):
|
|
"""Calculate comprehensive meta-analysis across all games"""
|
|
meta_stats = {}
|
|
|
|
# Aggregate PnL statistics
|
|
final_retail_pnl = results_df.groupby('simulation')['retail_pnl'].sum()
|
|
final_big_pnl = results_df.groupby('simulation')['big_player_pnl'].sum()
|
|
|
|
meta_stats['retail_pnl'] = {
|
|
'mean': final_retail_pnl.mean(),
|
|
'median': final_retail_pnl.median(),
|
|
'std': final_retail_pnl.std(),
|
|
'min': final_retail_pnl.min(),
|
|
'max': final_retail_pnl.max(),
|
|
'win_rate': (final_retail_pnl > 0).mean(),
|
|
'sharpe': final_retail_pnl.mean() / final_retail_pnl.std() if final_retail_pnl.std() > 0 else 0
|
|
}
|
|
|
|
meta_stats['big_player_pnl'] = {
|
|
'mean': final_big_pnl.mean(),
|
|
'median': final_big_pnl.median(),
|
|
'std': final_big_pnl.std(),
|
|
'min': final_big_pnl.min(),
|
|
'max': final_big_pnl.max(),
|
|
'win_rate': (final_big_pnl > 0).mean(),
|
|
'sharpe': final_big_pnl.mean() / final_big_pnl.std() if final_big_pnl.std() > 0 else 0
|
|
}
|
|
|
|
# Price efficiency
|
|
price_efficiency = []
|
|
for sim in results_df['simulation'].unique():
|
|
sim_data = results_df[results_df['simulation'] == sim]
|
|
price_deviations = np.abs(sim_data['price'].values - 100) / 100
|
|
efficiency = 1 - price_deviations.mean()
|
|
price_efficiency.append(max(0, efficiency))
|
|
|
|
meta_stats['market_efficiency'] = {
|
|
'mean': np.mean(price_efficiency),
|
|
'median': np.median(price_efficiency),
|
|
'std': np.std(price_efficiency),
|
|
'min': np.min(price_efficiency),
|
|
'max': np.max(price_efficiency)
|
|
}
|
|
|
|
# Nash equilibrium statistics
|
|
meta_stats['nash_equilibrium'] = {
|
|
'mean_price': nash_df['equilibrium_price'].mean(),
|
|
'std_price': nash_df['equilibrium_price'].std(),
|
|
'mean_deviation': nash_df['price_deviation'].mean(),
|
|
'mean_sentiment': nash_df['equilibrium_retail_sentiment'].mean()
|
|
}
|
|
|
|
# FOMO and herding metrics
|
|
sentiment_volatility = results_df.groupby('simulation')['retail_sentiment'].std()
|
|
volume_volatility = results_df.groupby('simulation')['retail_volume'].std()
|
|
sentiment_volume_corr = results_df.groupby('simulation').apply(
|
|
lambda x: x['retail_sentiment'].corr(x['retail_volume'])
|
|
)
|
|
|
|
meta_stats['fomo_herding'] = {
|
|
'avg_sentiment_volatility': sentiment_volatility.mean(),
|
|
'avg_volume_volatility': volume_volatility.mean(),
|
|
'sentiment_volume_correlation': sentiment_volume_corr.mean(),
|
|
'sentiment_volume_correlation_std': sentiment_volume_corr.std()
|
|
}
|
|
|
|
# Exploitation metrics
|
|
exploitation_scores = []
|
|
for sim in results_df['simulation'].unique():
|
|
sim_data = results_df[results_df['simulation'] == sim]
|
|
if len(sim_data) < 10:
|
|
continue
|
|
retail_sentiment = sim_data['retail_sentiment'].values
|
|
big_volume = sim_data['big_player_volume'].values
|
|
abs_sentiment = np.abs(retail_sentiment)
|
|
if len(abs_sentiment) > 5 and big_volume.std() > 0 and abs_sentiment.std() > 0:
|
|
try:
|
|
corr = np.corrcoef(abs_sentiment, big_volume)[0, 1]
|
|
if not np.isnan(corr) and not np.isinf(corr):
|
|
exploitation_scores.append(corr)
|
|
except:
|
|
pass
|
|
|
|
if len(exploitation_scores) > 0:
|
|
meta_stats['exploitation'] = {
|
|
'mean': np.mean(exploitation_scores),
|
|
'median': np.median(exploitation_scores),
|
|
'std': np.std(exploitation_scores)
|
|
}
|
|
else:
|
|
meta_stats['exploitation'] = {
|
|
'mean': 0,
|
|
'median': 0,
|
|
'std': 0
|
|
}
|
|
|
|
return meta_stats
|
|
|
|
|
|
def plot_game_analysis(num_simulations=100, show_random_games=10):
|
|
"""Plot comprehensive game theory analysis with meta-analysis"""
|
|
results_df, nash_df = analyze_game_theory(num_simulations, num_rounds=100)
|
|
|
|
# Calculate meta-analysis
|
|
meta_stats = calculate_meta_analysis(results_df, nash_df)
|
|
|
|
fig = plt.figure(figsize=(20, 16))
|
|
gs = fig.add_gridspec(4, 3, hspace=0.4, wspace=0.3)
|
|
|
|
# Plot 1: Random 10 Games Evolution (Price)
|
|
ax1 = fig.add_subplot(gs[0, 0])
|
|
random_sims = np.random.choice(results_df['simulation'].unique(),
|
|
min(show_random_games, len(results_df['simulation'].unique())),
|
|
replace=False)
|
|
colors = plt.cm.tab10(np.linspace(0, 1, len(random_sims)))
|
|
for idx, sim in enumerate(random_sims):
|
|
sim_data = results_df[results_df['simulation'] == sim]
|
|
ax1.plot(sim_data['round'], sim_data['price'],
|
|
color=colors[idx], alpha=0.6, linewidth=1.5, label=f'Game {sim}')
|
|
ax1.axhline(100, color='r', linestyle='--', linewidth=2, label='Fundamental Value')
|
|
ax1.set_xlabel('Game Round')
|
|
ax1.set_ylabel('Price')
|
|
ax1.set_title(f'Random {len(random_sims)} Games: Price Evolution')
|
|
ax1.legend(loc='best', fontsize=7, ncol=2)
|
|
ax1.grid(True, alpha=0.3)
|
|
|
|
# Plot 2: Random 10 Games Evolution (Sentiment)
|
|
ax2 = fig.add_subplot(gs[0, 1])
|
|
for idx, sim in enumerate(random_sims):
|
|
sim_data = results_df[results_df['simulation'] == sim]
|
|
ax2.plot(sim_data['round'], sim_data['retail_sentiment'],
|
|
color=colors[idx], alpha=0.6, linewidth=1.5)
|
|
ax2.axhline(0, color='black', linestyle=':', linewidth=1, alpha=0.5)
|
|
ax2.set_xlabel('Game Round')
|
|
ax2.set_ylabel('Retail Sentiment')
|
|
ax2.set_title(f'Random {len(random_sims)} Games: Sentiment Evolution')
|
|
ax2.grid(True, alpha=0.3)
|
|
|
|
# Plot 3: Retail vs Big Player PnL Distribution
|
|
ax3 = fig.add_subplot(gs[0, 2])
|
|
final_retail_pnl = results_df.groupby('simulation')['retail_pnl'].sum()
|
|
final_big_pnl = results_df.groupby('simulation')['big_player_pnl'].sum()
|
|
ax3.hist(final_retail_pnl, bins=30, alpha=0.5, label='Retail Traders',
|
|
color='red', edgecolor='black')
|
|
ax3.hist(final_big_pnl, bins=30, alpha=0.5, label='Big Players',
|
|
color='blue', edgecolor='black')
|
|
ax3.axvline(0, color='black', linestyle='--', linewidth=2)
|
|
ax3.axvline(final_retail_pnl.mean(), color='red', linestyle=':', linewidth=2, alpha=0.7)
|
|
ax3.axvline(final_big_pnl.mean(), color='blue', linestyle=':', linewidth=2, alpha=0.7)
|
|
ax3.set_xlabel('Total PnL')
|
|
ax3.set_ylabel('Frequency')
|
|
ax3.set_title('PnL Distribution: Retail vs Big Players')
|
|
ax3.legend()
|
|
ax3.grid(True, alpha=0.3)
|
|
|
|
# Plot 4: Nash Equilibrium Analysis
|
|
ax4 = fig.add_subplot(gs[1, 0])
|
|
ax4.scatter(nash_df['equilibrium_price'], nash_df['price_deviation'],
|
|
c=nash_df['equilibrium_retail_sentiment'], cmap='RdYlGn',
|
|
s=100, alpha=0.6, edgecolors='black')
|
|
ax4.axvline(100, color='r', linestyle='--', label='Fundamental Value')
|
|
ax4.set_xlabel('Equilibrium Price')
|
|
ax4.set_ylabel('Price Deviation from Fundamental')
|
|
ax4.set_title('Nash Equilibrium Analysis')
|
|
ax4.legend()
|
|
ax4.grid(True, alpha=0.3)
|
|
cbar = plt.colorbar(ax4.collections[0], ax=ax4)
|
|
cbar.set_label('Retail Sentiment')
|
|
|
|
# Plot 5: Sentiment vs Volume Relationship
|
|
ax5 = fig.add_subplot(gs[1, 1])
|
|
scatter = ax5.scatter(results_df['retail_sentiment'], results_df['retail_volume'],
|
|
alpha=0.3, s=10, c=results_df['round'], cmap='viridis')
|
|
ax5.set_xlabel('Retail Sentiment')
|
|
ax5.set_ylabel('Retail Trading Volume')
|
|
ax5.set_title('FOMO Effect: Sentiment vs Volume')
|
|
ax5.grid(True, alpha=0.3)
|
|
cbar = plt.colorbar(scatter, ax=ax5)
|
|
cbar.set_label('Game Round')
|
|
|
|
# Plot 6: Cumulative PnL Over Time (All 100 Games)
|
|
ax6 = fig.add_subplot(gs[1, 2])
|
|
|
|
# Calculate cumulative PnL for each game
|
|
retail_cum_pnl_by_game = []
|
|
big_cum_pnl_by_game = []
|
|
rounds = sorted(results_df['round'].unique())
|
|
|
|
for sim in results_df['simulation'].unique():
|
|
sim_data = results_df[results_df['simulation'] == sim].sort_values('round')
|
|
retail_cum = sim_data['retail_pnl'].cumsum().values
|
|
big_cum = sim_data['big_player_pnl'].cumsum().values
|
|
retail_cum_pnl_by_game.append(retail_cum)
|
|
big_cum_pnl_by_game.append(big_cum)
|
|
|
|
# Convert to numpy array (pad with NaN if games have different lengths)
|
|
max_rounds = max(len(cum) for cum in retail_cum_pnl_by_game)
|
|
retail_matrix = np.full((len(retail_cum_pnl_by_game), max_rounds), np.nan)
|
|
big_matrix = np.full((len(big_cum_pnl_by_game), max_rounds), np.nan)
|
|
|
|
for i, (retail_cum, big_cum) in enumerate(zip(retail_cum_pnl_by_game, big_cum_pnl_by_game)):
|
|
retail_matrix[i, :len(retail_cum)] = retail_cum
|
|
big_matrix[i, :len(big_cum)] = big_cum
|
|
|
|
# Calculate mean and std across all games
|
|
retail_mean = np.nanmean(retail_matrix, axis=0)
|
|
retail_std = np.nanstd(retail_matrix, axis=0)
|
|
retail_upper = retail_mean + 1.96 * retail_std # 95% confidence interval
|
|
retail_lower = retail_mean - 1.96 * retail_std
|
|
|
|
big_mean = np.nanmean(big_matrix, axis=0)
|
|
big_std = np.nanstd(big_matrix, axis=0)
|
|
big_upper = big_mean + 1.96 * big_std
|
|
big_lower = big_mean - 1.96 * big_std
|
|
|
|
# Plot confidence bands
|
|
ax6.fill_between(range(len(retail_mean)), retail_lower, retail_upper,
|
|
alpha=0.2, color='red', label='Retail 95% CI')
|
|
ax6.fill_between(range(len(big_mean)), big_lower, big_upper,
|
|
alpha=0.2, color='blue', label='Big Players 95% CI')
|
|
|
|
# Plot mean lines
|
|
ax6.plot(range(len(retail_mean)), retail_mean,
|
|
'r-', linewidth=2, label=f'Retail Traders (Mean, n={num_simulations})')
|
|
ax6.plot(range(len(big_mean)), big_mean,
|
|
'b-', linewidth=2, label=f'Big Players (Mean, n={num_simulations})')
|
|
|
|
# Show a few individual game trajectories (random sample)
|
|
sample_games = np.random.choice(results_df['simulation'].unique(),
|
|
min(5, len(results_df['simulation'].unique())),
|
|
replace=False)
|
|
for sim in sample_games:
|
|
sim_data = results_df[results_df['simulation'] == sim].sort_values('round')
|
|
ax6.plot(sim_data['round'], sim_data['retail_pnl'].cumsum(),
|
|
'r-', alpha=0.15, linewidth=0.5)
|
|
ax6.plot(sim_data['round'], sim_data['big_player_pnl'].cumsum(),
|
|
'b-', alpha=0.15, linewidth=0.5)
|
|
|
|
ax6.axhline(0, color='black', linestyle='--', linewidth=1)
|
|
ax6.set_xlabel('Game Round')
|
|
ax6.set_ylabel('Cumulative PnL')
|
|
ax6.set_title(f'Cumulative PnL Evolution (All {num_simulations} Games)')
|
|
ax6.legend(fontsize=8)
|
|
ax6.grid(True, alpha=0.3)
|
|
|
|
# Plot 7: Market Efficiency (Price vs Fundamental)
|
|
ax7 = fig.add_subplot(gs[2, 0])
|
|
price_efficiency = []
|
|
for sim in results_df['simulation'].unique():
|
|
sim_data = results_df[results_df['simulation'] == sim]
|
|
price_deviations = np.abs(sim_data['price'].values - 100) / 100
|
|
efficiency = 1 - price_deviations.mean() # Efficiency: 1 = perfect, 0 = 100% deviation
|
|
price_efficiency.append(max(0, efficiency)) # Ensure non-negative
|
|
price_efficiency = np.array(price_efficiency)
|
|
if len(price_efficiency) > 0:
|
|
ax7.hist(price_efficiency, bins=30, color='purple', alpha=0.7, edgecolor='black')
|
|
ax7.axvline(np.mean(price_efficiency), color='red', linestyle='--',
|
|
linewidth=2, label=f'Mean: {np.mean(price_efficiency):.3f}')
|
|
ax7.set_xlabel('Market Efficiency (1 - |Price - Fundamental|/Fundamental)')
|
|
ax7.set_ylabel('Frequency')
|
|
ax7.set_title('Market Efficiency Distribution')
|
|
ax7.legend()
|
|
ax7.grid(True, alpha=0.3)
|
|
|
|
# Plot 8: Herding Behavior Analysis
|
|
ax8 = fig.add_subplot(gs[2, 1])
|
|
sentiment_volatility = results_df.groupby('simulation')['retail_sentiment'].std()
|
|
volume_volatility = results_df.groupby('simulation')['retail_volume'].std()
|
|
ax8.scatter(sentiment_volatility, volume_volatility, alpha=0.6, s=100,
|
|
edgecolors='black')
|
|
ax8.set_xlabel('Sentiment Volatility')
|
|
ax8.set_ylabel('Volume Volatility')
|
|
ax8.set_title('Herding Behavior: Sentiment vs Volume Volatility')
|
|
ax8.grid(True, alpha=0.3)
|
|
|
|
# Plot 9: Meta-Analysis Summary
|
|
ax9 = fig.add_subplot(gs[2, 2])
|
|
# Simulate games of different lengths
|
|
game_lengths = [50, 100, 150, 200]
|
|
final_pnl_by_length = []
|
|
for length in game_lengths:
|
|
game = TradingGame()
|
|
results = game.simulate_game(num_rounds=length)
|
|
final_pnl_by_length.append({
|
|
'length': length,
|
|
'retail_pnl': results['retail_pnl'].sum(),
|
|
'big_pnl': results['big_player_pnl'].sum()
|
|
})
|
|
length_df = pd.DataFrame(final_pnl_by_length)
|
|
x = np.arange(len(game_lengths))
|
|
width = 0.35
|
|
ax8.bar(x - width/2, length_df['retail_pnl'], width, label='Retail',
|
|
color='red', alpha=0.7)
|
|
ax8.bar(x + width/2, length_df['big_pnl'], width, label='Big Players',
|
|
color='blue', alpha=0.7)
|
|
ax8.set_xlabel('Game Length (Rounds)')
|
|
ax8.set_ylabel('Final PnL')
|
|
ax8.set_title('Finite Game Effects: PnL vs Game Length')
|
|
ax8.set_xticks(x)
|
|
ax8.set_xticklabels(game_lengths)
|
|
ax8.legend()
|
|
ax8.grid(True, alpha=0.3, axis='y')
|
|
ax8.axhline(0, color='black', linestyle='--', linewidth=1)
|
|
|
|
# Plot 9: Meta-Analysis Summary
|
|
# Create summary table visualization
|
|
ax9.axis('off')
|
|
summary_text = f"""
|
|
META-ANALYSIS SUMMARY (n={num_simulations} games)
|
|
|
|
Retail Traders:
|
|
Mean PnL: ${meta_stats['retail_pnl']['mean']:.2f}
|
|
Win Rate: {meta_stats['retail_pnl']['win_rate']:.1%}
|
|
Sharpe: {meta_stats['retail_pnl']['sharpe']:.3f}
|
|
|
|
Big Players:
|
|
Mean PnL: ${meta_stats['big_player_pnl']['mean']:.2f}
|
|
Win Rate: {meta_stats['big_player_pnl']['win_rate']:.1%}
|
|
Sharpe: {meta_stats['big_player_pnl']['sharpe']:.3f}
|
|
|
|
Market Efficiency: {meta_stats['market_efficiency']['mean']:.3f}
|
|
FOMO Correlation: {meta_stats['fomo_herding']['sentiment_volume_correlation']:.3f}
|
|
Exploitation Score: {meta_stats['exploitation']['mean']:.3f}
|
|
|
|
Nash Equilibrium:
|
|
Price: ${meta_stats['nash_equilibrium']['mean_price']:.2f}
|
|
Deviation: {meta_stats['nash_equilibrium']['mean_deviation']:.4f}
|
|
"""
|
|
ax9.text(0.1, 0.5, summary_text, transform=ax9.transAxes,
|
|
fontsize=10, verticalalignment='center', family='monospace',
|
|
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
|
|
|
|
# Plot 10: Strategic Exploitation (moved to new row)
|
|
ax10 = fig.add_subplot(gs[3, 0])
|
|
# Calculate exploitation metric
|
|
exploitation_scores = []
|
|
for sim in results_df['simulation'].unique():
|
|
sim_data = results_df[results_df['simulation'] == sim].copy()
|
|
if len(sim_data) < 10:
|
|
continue
|
|
retail_sentiment = sim_data['retail_sentiment'].values
|
|
big_volume = sim_data['big_player_volume'].values
|
|
abs_sentiment = np.abs(retail_sentiment)
|
|
if len(abs_sentiment) > 5 and big_volume.std() > 0 and abs_sentiment.std() > 0:
|
|
try:
|
|
corr = np.corrcoef(abs_sentiment, big_volume)[0, 1]
|
|
if not np.isnan(corr) and not np.isinf(corr):
|
|
exploitation_scores.append(corr)
|
|
except:
|
|
pass
|
|
|
|
if len(exploitation_scores) > 0:
|
|
exploitation_scores = np.array(exploitation_scores)
|
|
if len(exploitation_scores) > 5:
|
|
q1, q3 = np.percentile(exploitation_scores, [10, 90])
|
|
exploitation_scores = exploitation_scores[(exploitation_scores >= q1) &
|
|
(exploitation_scores <= q3)]
|
|
if len(exploitation_scores) > 0:
|
|
bins = min(20, max(5, len(exploitation_scores) // 2))
|
|
counts, bins_edges, patches = ax10.hist(exploitation_scores, bins=bins,
|
|
color='orange', alpha=0.7, edgecolor='black')
|
|
mean_score = np.mean(exploitation_scores)
|
|
ax10.axvline(mean_score, color='red', linestyle='--',
|
|
linewidth=2, label=f'Mean: {mean_score:.3f}')
|
|
ax10.axvline(0, color='black', linestyle=':', linewidth=1, alpha=0.5)
|
|
ax10.legend(fontsize=8)
|
|
if len(counts) > 0:
|
|
ax10.set_ylim([0, max(counts) * 1.15])
|
|
else:
|
|
ax10.text(0.5, 0.5, 'Insufficient data', ha='center', va='center',
|
|
transform=ax10.transAxes, fontsize=10)
|
|
ax10.set_xlabel('Exploitation Score')
|
|
ax10.set_ylabel('Frequency')
|
|
ax10.set_title('Big Player Exploitation of Retail FOMO')
|
|
ax10.grid(True, alpha=0.3)
|
|
|
|
plt.suptitle(f'Game Theory Analysis: Retail Traders vs Big Players\n'
|
|
f'Meta-Analysis of {num_simulations} Games - FOMO, Herding, and Strategic Exploitation',
|
|
fontsize=16, fontweight='bold', y=0.995)
|
|
|
|
return fig, results_df, nash_df, meta_stats
|
|
|
|
|
|
def evaluate_configuration(params, num_simulations=10, num_rounds=100):
|
|
"""
|
|
Evaluate a configuration of parameters
|
|
Returns: (big_player_pnl, retail_pnl, profit_difference)
|
|
params: dict with keys:
|
|
- order_book_liquidity
|
|
- big_sentiment_threshold
|
|
- big_volume_threshold
|
|
- big_trade_size_pct
|
|
- fundamental_reversion
|
|
- num_big_players
|
|
"""
|
|
# Extract parameters
|
|
order_book_liquidity = params.get('order_book_liquidity', 50)
|
|
big_sentiment_threshold = params.get('big_sentiment_threshold', 0.6)
|
|
big_volume_threshold = params.get('big_volume_threshold', 20)
|
|
big_trade_size_pct = params.get('big_trade_size_pct', 0.20)
|
|
fundamental_reversion = params.get('fundamental_reversion', 0.01)
|
|
num_big_players = int(params.get('num_big_players', 5))
|
|
|
|
# Store original BigPlayer strategic_action for modification
|
|
# We'll need to modify the TradingGame to accept these parameters
|
|
all_big_pnl = []
|
|
all_retail_pnl = []
|
|
|
|
for sim in range(num_simulations):
|
|
# Each simulation gets completely independent random state
|
|
import time
|
|
base_seed = int(time.time() * 1000000) % (2**31)
|
|
game_seed = (base_seed + sim * 7919 + np.random.randint(0, 1000000)) % (2**31)
|
|
game_rng = np.random.RandomState(game_seed)
|
|
|
|
# Vary initial conditions for diversity
|
|
initial_price = 100 + game_rng.normal(0, 1)
|
|
fundamental_value = 100 + game_rng.normal(0, 0.5)
|
|
|
|
# Create game with custom parameters
|
|
game = TradingGame(
|
|
num_retail_traders=100,
|
|
num_big_players=num_big_players,
|
|
initial_price=initial_price,
|
|
fundamental_value=fundamental_value,
|
|
volatility=0.02,
|
|
order_book_liquidity=order_book_liquidity,
|
|
fundamental_reversion=fundamental_reversion,
|
|
big_sentiment_threshold=big_sentiment_threshold,
|
|
big_volume_threshold=big_volume_threshold,
|
|
big_trade_size_pct=big_trade_size_pct
|
|
)
|
|
|
|
# Override game's RNG with our independent one
|
|
game.rng = game_rng
|
|
|
|
# Run simulation with random fundamental shocks (unique per game)
|
|
fundamental_shocks = game_rng.normal(0, 0.01, num_rounds)
|
|
# Add random large shocks at random times
|
|
num_shocks = game_rng.randint(0, 3)
|
|
if num_shocks > 0:
|
|
shock_times = game_rng.choice(num_rounds, num_shocks, replace=False)
|
|
shock_sizes = game_rng.normal(0, 0.02, num_shocks)
|
|
for time, size in zip(shock_times, shock_sizes):
|
|
fundamental_shocks[time] = size
|
|
results = game.simulate_game(num_rounds, fundamental_shocks)
|
|
|
|
# Calculate total PnL
|
|
total_big_pnl = results['big_player_pnl'].sum()
|
|
total_retail_pnl = results['retail_pnl'].sum()
|
|
|
|
all_big_pnl.append(total_big_pnl)
|
|
all_retail_pnl.append(total_retail_pnl)
|
|
|
|
mean_big_pnl = np.mean(all_big_pnl)
|
|
mean_retail_pnl = np.mean(all_retail_pnl)
|
|
profit_difference = mean_big_pnl - mean_retail_pnl # Big player advantage
|
|
|
|
# Calculate win rates from individual simulations
|
|
big_win_rate = np.mean(np.array(all_big_pnl) > 0)
|
|
retail_win_rate = np.mean(np.array(all_retail_pnl) > 0)
|
|
|
|
return mean_big_pnl, mean_retail_pnl, profit_difference, big_win_rate, retail_win_rate, all_big_pnl, all_retail_pnl
|
|
|
|
|
|
def genetic_optimization(num_generations=20, population_size=30, num_simulations=5):
|
|
"""
|
|
Genetic algorithm to find optimal configuration for big players
|
|
Objective: Maximize (big_player_pnl - retail_pnl)
|
|
"""
|
|
print("\n" + "="*60)
|
|
print("GENETIC ALGORITHM OPTIMIZATION")
|
|
print("="*60)
|
|
print(f"Generations: {num_generations}, Population: {population_size}")
|
|
print(f"Simulations per evaluation: {num_simulations}")
|
|
print("="*60)
|
|
|
|
# Parameter bounds
|
|
bounds = [
|
|
(20, 200), # order_book_liquidity
|
|
(0.3, 0.9), # big_sentiment_threshold
|
|
(10, 100), # big_volume_threshold
|
|
(0.05, 0.30), # big_trade_size_pct
|
|
(0.001, 0.05), # fundamental_reversion
|
|
(3, 10), # num_big_players
|
|
]
|
|
|
|
param_names = [
|
|
'order_book_liquidity',
|
|
'big_sentiment_threshold',
|
|
'big_volume_threshold',
|
|
'big_trade_size_pct',
|
|
'fundamental_reversion',
|
|
'num_big_players'
|
|
]
|
|
|
|
# Objective function (minimize negative profit difference)
|
|
def objective(x):
|
|
params = dict(zip(param_names, x))
|
|
try:
|
|
_, _, profit_diff, _, _, _, _ = evaluate_configuration(params, num_simulations=num_simulations, num_rounds=50)
|
|
return -profit_diff # Negative because we're minimizing
|
|
except Exception as e:
|
|
print(f"Error in evaluation: {e}")
|
|
return 1e10 # Large penalty for invalid configurations
|
|
|
|
# Run differential evolution (genetic algorithm)
|
|
print("\nStarting optimization...")
|
|
result = differential_evolution(
|
|
objective,
|
|
bounds,
|
|
maxiter=num_generations,
|
|
popsize=population_size,
|
|
seed=42,
|
|
polish=True,
|
|
workers=1
|
|
)
|
|
|
|
optimal_params = dict(zip(param_names, result.x))
|
|
optimal_params['num_big_players'] = int(optimal_params['num_big_players'])
|
|
|
|
# Evaluate optimal configuration with more simulations
|
|
print("\nEvaluating optimal configuration with more simulations...")
|
|
big_pnl, retail_pnl, profit_diff, big_wr, retail_wr, _, _ = evaluate_configuration(
|
|
optimal_params, num_simulations=20, num_rounds=100
|
|
)
|
|
|
|
print(f"\n{'='*60}")
|
|
print("OPTIMAL CONFIGURATION FOUND:")
|
|
print(f"{'='*60}")
|
|
for key, value in optimal_params.items():
|
|
print(f" {key}: {value:.4f}" if isinstance(value, float) else f" {key}: {value}")
|
|
print(f"\nResults (20 simulations):")
|
|
print(f" Big Player Mean PnL: ${big_pnl:,.2f}")
|
|
print(f" Retail Mean PnL: ${retail_pnl:,.2f}")
|
|
print(f" Profit Difference: ${profit_diff:,.2f}")
|
|
print(f" Big Player Win Rate: {big_wr:.1%}")
|
|
print(f" Retail Win Rate: {retail_wr:.1%}")
|
|
print(f"{'='*60}\n")
|
|
|
|
return optimal_params, result
|
|
|
|
|
|
def grid_search_optimization(num_simulations=5):
|
|
"""
|
|
Grid search over parameter space (faster but less thorough)
|
|
Returns top configurations
|
|
"""
|
|
print("\n" + "="*60)
|
|
print("GRID SEARCH OPTIMIZATION")
|
|
print("="*60)
|
|
|
|
# Define parameter grids
|
|
param_grids = {
|
|
'order_book_liquidity': [30, 50, 100, 150],
|
|
'big_sentiment_threshold': [0.4, 0.5, 0.6, 0.7],
|
|
'big_volume_threshold': [15, 25, 40, 60],
|
|
'big_trade_size_pct': [0.10, 0.15, 0.20, 0.25],
|
|
'fundamental_reversion': [0.005, 0.01, 0.02, 0.03],
|
|
'num_big_players': [3, 5, 7, 10]
|
|
}
|
|
|
|
# Generate all combinations (sample to avoid too many)
|
|
keys = list(param_grids.keys())
|
|
values = list(param_grids.values())
|
|
|
|
# Limit combinations for speed
|
|
all_combinations = list(itertools.product(*values))
|
|
np.random.shuffle(all_combinations)
|
|
max_combinations = min(50, len(all_combinations)) # Limit to 50 configurations
|
|
sampled_combinations = all_combinations[:max_combinations]
|
|
|
|
print(f"Testing {len(sampled_combinations)} configurations...")
|
|
|
|
results = []
|
|
for i, combo in enumerate(sampled_combinations):
|
|
params = dict(zip(keys, combo))
|
|
try:
|
|
big_pnl, retail_pnl, profit_diff, big_wr, retail_wr, _, _ = evaluate_configuration(
|
|
params, num_simulations=num_simulations, num_rounds=50
|
|
)
|
|
results.append({
|
|
**params,
|
|
'big_pnl': big_pnl,
|
|
'retail_pnl': retail_pnl,
|
|
'profit_diff': profit_diff,
|
|
'big_win_rate': big_wr,
|
|
'retail_win_rate': retail_wr
|
|
})
|
|
if (i + 1) % 10 == 0:
|
|
print(f" Completed {i+1}/{len(sampled_combinations)} configurations...")
|
|
except Exception as e:
|
|
print(f" Error with configuration {i+1}: {e}")
|
|
continue
|
|
|
|
results_df = pd.DataFrame(results)
|
|
results_df = results_df.sort_values('profit_diff', ascending=False)
|
|
|
|
optimal = results_df.iloc[0].to_dict()
|
|
|
|
print(f"\n{'='*60}")
|
|
print("TOP CONFIGURATION FOUND:")
|
|
print(f"{'='*60}")
|
|
for key in keys:
|
|
print(f" {key}: {optimal[key]}")
|
|
print(f"\nResults:")
|
|
print(f" Big Player Mean PnL: ${optimal['big_pnl']:,.2f}")
|
|
print(f" Retail Mean PnL: ${optimal['retail_pnl']:,.2f}")
|
|
print(f" Profit Difference: ${optimal['profit_diff']:,.2f}")
|
|
print(f"{'='*60}\n")
|
|
|
|
return results_df, optimal
|
|
|
|
|
|
def plot_optimization_results(optimal_params, grid_results_df=None, num_simulations=20):
|
|
"""
|
|
Create comprehensive visualization of optimization results
|
|
Highlights optimal configuration vs others
|
|
"""
|
|
print("\nGenerating optimization visualization...")
|
|
|
|
# Evaluate optimal configuration in detail
|
|
optimal_big_pnl, optimal_retail_pnl, optimal_profit_diff, optimal_big_wr, optimal_retail_wr, _, _ = evaluate_configuration(
|
|
optimal_params, num_simulations=num_simulations, num_rounds=100
|
|
)
|
|
|
|
# Run optimal configuration for detailed analysis
|
|
optimal_game = TradingGame(
|
|
num_retail_traders=100,
|
|
num_big_players=int(optimal_params['num_big_players']),
|
|
initial_price=100,
|
|
fundamental_value=100,
|
|
volatility=0.02,
|
|
order_book_liquidity=optimal_params['order_book_liquidity'],
|
|
fundamental_reversion=optimal_params['fundamental_reversion'],
|
|
big_sentiment_threshold=optimal_params['big_sentiment_threshold'],
|
|
big_volume_threshold=optimal_params['big_volume_threshold'],
|
|
big_trade_size_pct=optimal_params['big_trade_size_pct']
|
|
)
|
|
|
|
optimal_results = optimal_game.simulate_game(100)
|
|
|
|
# Create figure with subplots
|
|
fig = plt.figure(figsize=(20, 14))
|
|
gs = fig.add_gridspec(3, 3, hspace=0.4, wspace=0.3)
|
|
|
|
# Plot 1: Optimal Configuration - Price Evolution
|
|
ax1 = fig.add_subplot(gs[0, 0])
|
|
ax1.plot(optimal_results['round'], optimal_results['price'],
|
|
'b-', linewidth=2.5, label='Optimal Config', zorder=3)
|
|
ax1.axhline(100, color='r', linestyle='--', linewidth=2, label='Fundamental Value', alpha=0.7)
|
|
ax1.set_xlabel('Game Round')
|
|
ax1.set_ylabel('Price')
|
|
ax1.set_title('Optimal Configuration: Price Evolution', fontweight='bold')
|
|
ax1.legend()
|
|
ax1.grid(True, alpha=0.3)
|
|
|
|
# Plot 2: Optimal Configuration - PnL Evolution
|
|
ax2 = fig.add_subplot(gs[0, 1])
|
|
retail_cum_pnl = optimal_results['retail_pnl'].cumsum()
|
|
big_cum_pnl = optimal_results['big_player_pnl'].cumsum()
|
|
ax2.plot(optimal_results['round'], retail_cum_pnl,
|
|
'r-', linewidth=2, label='Retail Traders', alpha=0.7)
|
|
ax2.plot(optimal_results['round'], big_cum_pnl,
|
|
'b-', linewidth=2.5, label='Big Players (Optimal)', zorder=3)
|
|
ax2.axhline(0, color='black', linestyle=':', linewidth=1, alpha=0.5)
|
|
ax2.set_xlabel('Game Round')
|
|
ax2.set_ylabel('Cumulative PnL')
|
|
ax2.set_title('Optimal Configuration: Cumulative PnL', fontweight='bold')
|
|
ax2.legend()
|
|
ax2.grid(True, alpha=0.3)
|
|
|
|
# Plot 3: Optimal Configuration - Sentiment vs Big Player Volume
|
|
ax3 = fig.add_subplot(gs[0, 2])
|
|
scatter = ax3.scatter(optimal_results['retail_sentiment'],
|
|
optimal_results['big_player_volume'],
|
|
c=optimal_results['round'], cmap='viridis',
|
|
alpha=0.6, s=50, edgecolors='black', linewidth=0.5)
|
|
ax3.axvline(optimal_params['big_sentiment_threshold'],
|
|
color='red', linestyle='--', linewidth=2,
|
|
label=f"Threshold: {optimal_params['big_sentiment_threshold']:.2f}")
|
|
ax3.axvline(-optimal_params['big_sentiment_threshold'],
|
|
color='red', linestyle='--', linewidth=2)
|
|
ax3.set_xlabel('Retail Sentiment')
|
|
ax3.set_ylabel('Big Player Volume')
|
|
ax3.set_title('Optimal: Exploitation Strategy', fontweight='bold')
|
|
ax3.legend()
|
|
ax3.grid(True, alpha=0.3)
|
|
cbar = plt.colorbar(scatter, ax=ax3)
|
|
cbar.set_label('Game Round')
|
|
|
|
# Plot 4: Parameter Comparison (if grid results available)
|
|
if grid_results_df is not None and len(grid_results_df) > 0:
|
|
ax4 = fig.add_subplot(gs[1, 0])
|
|
top_10 = grid_results_df.head(10)
|
|
y_pos = np.arange(len(top_10))
|
|
colors = ['gold' if i == 0 else 'steelblue' for i in range(len(top_10))]
|
|
ax4.barh(y_pos, top_10['profit_diff'], color=colors, edgecolor='black')
|
|
ax4.set_yticks(y_pos)
|
|
ax4.set_yticklabels([f"Config {i+1}" for i in range(len(top_10))])
|
|
ax4.set_xlabel('Profit Difference (Big - Retail)')
|
|
ax4.set_title('Top 10 Configurations (Optimal Highlighted)', fontweight='bold')
|
|
ax4.axvline(0, color='black', linestyle='--', linewidth=1, alpha=0.5)
|
|
ax4.grid(True, alpha=0.3, axis='x')
|
|
else:
|
|
ax4 = fig.add_subplot(gs[1, 0])
|
|
ax4.text(0.5, 0.5, 'Grid search results not available',
|
|
ha='center', va='center', transform=ax4.transAxes, fontsize=12)
|
|
ax4.axis('off')
|
|
|
|
# Plot 5: Parameter Space - Order Book Liquidity vs Profit Difference
|
|
if grid_results_df is not None and len(grid_results_df) > 0:
|
|
ax5 = fig.add_subplot(gs[1, 1])
|
|
scatter = ax5.scatter(grid_results_df['order_book_liquidity'],
|
|
grid_results_df['profit_diff'],
|
|
c=grid_results_df['num_big_players'],
|
|
cmap='coolwarm', s=100, alpha=0.6,
|
|
edgecolors='black', linewidth=0.5)
|
|
# Highlight optimal
|
|
ax5.scatter([optimal_params['order_book_liquidity']],
|
|
[optimal_profit_diff],
|
|
s=300, marker='*', color='gold',
|
|
edgecolors='black', linewidth=2, zorder=5,
|
|
label='Optimal')
|
|
ax5.set_xlabel('Order Book Liquidity')
|
|
ax5.set_ylabel('Profit Difference (Big - Retail)')
|
|
ax5.set_title('Parameter Space: Liquidity vs Profit', fontweight='bold')
|
|
ax5.legend()
|
|
ax5.grid(True, alpha=0.3)
|
|
cbar = plt.colorbar(scatter, ax=ax5)
|
|
cbar.set_label('Number of Big Players')
|
|
else:
|
|
ax5 = fig.add_subplot(gs[1, 1])
|
|
ax5.scatter([optimal_params['order_book_liquidity']],
|
|
[optimal_profit_diff],
|
|
s=300, marker='*', color='gold',
|
|
edgecolors='black', linewidth=2, zorder=5)
|
|
ax5.set_xlabel('Order Book Liquidity')
|
|
ax5.set_ylabel('Profit Difference')
|
|
ax5.set_title('Optimal Configuration', fontweight='bold')
|
|
ax5.grid(True, alpha=0.3)
|
|
|
|
# Plot 6: Parameter Space - Sentiment Threshold vs Trade Size
|
|
if grid_results_df is not None and len(grid_results_df) > 0:
|
|
ax6 = fig.add_subplot(gs[1, 2])
|
|
scatter = ax6.scatter(grid_results_df['big_sentiment_threshold'],
|
|
grid_results_df['big_trade_size_pct'],
|
|
c=grid_results_df['profit_diff'],
|
|
cmap='RdYlGn', s=100, alpha=0.6,
|
|
edgecolors='black', linewidth=0.5)
|
|
# Highlight optimal
|
|
ax6.scatter([optimal_params['big_sentiment_threshold']],
|
|
[optimal_params['big_trade_size_pct']],
|
|
s=300, marker='*', color='gold',
|
|
edgecolors='black', linewidth=2, zorder=5,
|
|
label='Optimal')
|
|
ax6.set_xlabel('Sentiment Threshold')
|
|
ax6.set_ylabel('Trade Size (% of Capital)')
|
|
ax6.set_title('Parameter Space: Strategy Parameters', fontweight='bold')
|
|
ax6.legend()
|
|
ax6.grid(True, alpha=0.3)
|
|
cbar = plt.colorbar(scatter, ax=ax6)
|
|
cbar.set_label('Profit Difference')
|
|
else:
|
|
ax6 = fig.add_subplot(gs[1, 2])
|
|
ax6.scatter([optimal_params['big_sentiment_threshold']],
|
|
[optimal_params['big_trade_size_pct']],
|
|
s=300, marker='*', color='gold',
|
|
edgecolors='black', linewidth=2, zorder=5)
|
|
ax6.set_xlabel('Sentiment Threshold')
|
|
ax6.set_ylabel('Trade Size (% of Capital)')
|
|
ax6.set_title('Optimal Configuration', fontweight='bold')
|
|
ax6.grid(True, alpha=0.3)
|
|
|
|
# Plot 7: Optimal Configuration Summary Table
|
|
ax7 = fig.add_subplot(gs[2, :])
|
|
ax7.axis('off')
|
|
|
|
summary_text = f"""
|
|
OPTIMAL CONFIGURATION FOR BIG PLAYERS
|
|
{'='*80}
|
|
|
|
Parameters:
|
|
Order Book Liquidity: {optimal_params['order_book_liquidity']:.1f} units/level
|
|
Sentiment Threshold: {optimal_params['big_sentiment_threshold']:.3f}
|
|
Volume Threshold: {optimal_params['big_volume_threshold']:.1f} units
|
|
Trade Size (% of Capital): {optimal_params['big_trade_size_pct']:.1%}
|
|
Fundamental Reversion: {optimal_params['fundamental_reversion']:.4f}
|
|
Number of Big Players: {int(optimal_params['num_big_players'])}
|
|
|
|
Performance Results ({num_simulations} simulations):
|
|
Big Player Mean PnL: ${optimal_big_pnl:,.2f}
|
|
Retail Mean PnL: ${optimal_retail_pnl:,.2f}
|
|
Profit Difference: ${optimal_profit_diff:,.2f}
|
|
Big Player Win Rate: {optimal_big_wr:.1%}
|
|
Retail Win Rate: {optimal_retail_wr:.1%}
|
|
|
|
Strategy Insight:
|
|
Big players exploit retail FOMO by trading {optimal_params['big_trade_size_pct']:.1%} of capital
|
|
when retail sentiment exceeds {optimal_params['big_sentiment_threshold']:.2f} and volume > {optimal_params['big_volume_threshold']:.0f}.
|
|
Lower order book liquidity ({optimal_params['order_book_liquidity']:.0f}) creates more price impact,
|
|
allowing big players to move markets more effectively.
|
|
"""
|
|
|
|
ax7.text(0.05, 0.5, summary_text, transform=ax7.transAxes,
|
|
fontsize=11, verticalalignment='center', family='monospace',
|
|
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
|
|
|
|
plt.suptitle('Genetic Algorithm Optimization: Optimal Configuration for Big Players\n'
|
|
f'Maximizing Profit Difference (Big Player PnL - Retail PnL)',
|
|
fontsize=16, fontweight='bold', y=0.995)
|
|
|
|
return fig, optimal_params, optimal_results
|
|
|
|
|
|
def create_comparison_figure(optimal_params, num_simulations=20):
|
|
"""
|
|
Create a comparison figure showing Optimal vs Default configuration
|
|
"""
|
|
print(" Creating comparison between optimal and default configurations...")
|
|
|
|
# Default configuration
|
|
default_params = {
|
|
'order_book_liquidity': 50,
|
|
'big_sentiment_threshold': 0.6,
|
|
'big_volume_threshold': 20,
|
|
'big_trade_size_pct': 0.20,
|
|
'fundamental_reversion': 0.01,
|
|
'num_big_players': 5
|
|
}
|
|
|
|
# Evaluate both configurations
|
|
opt_big_pnl, opt_retail_pnl, opt_profit_diff, opt_big_wr, opt_retail_wr, opt_big_pnls, opt_retail_pnls = evaluate_configuration(
|
|
optimal_params, num_simulations=num_simulations, num_rounds=100
|
|
)
|
|
|
|
def_big_pnl, def_retail_pnl, def_profit_diff, def_big_wr, def_retail_wr, def_big_pnls, def_retail_pnls = evaluate_configuration(
|
|
default_params, num_simulations=num_simulations, num_rounds=100
|
|
)
|
|
|
|
# Run one detailed simulation of each for trajectory plots
|
|
opt_game = TradingGame(
|
|
num_retail_traders=100,
|
|
num_big_players=int(optimal_params['num_big_players']),
|
|
initial_price=100,
|
|
fundamental_value=100,
|
|
volatility=0.02,
|
|
order_book_liquidity=optimal_params['order_book_liquidity'],
|
|
fundamental_reversion=optimal_params['fundamental_reversion'],
|
|
big_sentiment_threshold=optimal_params['big_sentiment_threshold'],
|
|
big_volume_threshold=optimal_params['big_volume_threshold'],
|
|
big_trade_size_pct=optimal_params['big_trade_size_pct']
|
|
)
|
|
opt_results = opt_game.simulate_game(100)
|
|
|
|
def_game = TradingGame(
|
|
num_retail_traders=100,
|
|
num_big_players=int(default_params['num_big_players']),
|
|
initial_price=100,
|
|
fundamental_value=100,
|
|
volatility=0.02,
|
|
order_book_liquidity=default_params['order_book_liquidity'],
|
|
fundamental_reversion=default_params['fundamental_reversion'],
|
|
big_sentiment_threshold=default_params['big_sentiment_threshold'],
|
|
big_volume_threshold=default_params['big_volume_threshold'],
|
|
big_trade_size_pct=default_params['big_trade_size_pct']
|
|
)
|
|
def_results = def_game.simulate_game(100)
|
|
|
|
# Create figure
|
|
fig = plt.figure(figsize=(18, 12))
|
|
gs = fig.add_gridspec(2, 3, hspace=0.35, wspace=0.3)
|
|
|
|
# Plot 1: Price Evolution Comparison
|
|
ax1 = fig.add_subplot(gs[0, 0])
|
|
ax1.plot(opt_results['round'], opt_results['price'],
|
|
'b-', linewidth=2.5, label='Optimal Config', zorder=3)
|
|
ax1.plot(def_results['round'], def_results['price'],
|
|
'r--', linewidth=2, label='Default Config', alpha=0.7)
|
|
ax1.axhline(100, color='gray', linestyle=':', linewidth=1.5, label='Fundamental Value', alpha=0.5)
|
|
ax1.set_xlabel('Game Round')
|
|
ax1.set_ylabel('Price')
|
|
ax1.set_title('Price Evolution: Optimal vs Default', fontweight='bold')
|
|
ax1.legend()
|
|
ax1.grid(True, alpha=0.3)
|
|
|
|
# Plot 2: Cumulative PnL Comparison
|
|
ax2 = fig.add_subplot(gs[0, 1])
|
|
opt_retail_cum = opt_results['retail_pnl'].cumsum()
|
|
opt_big_cum = opt_results['big_player_pnl'].cumsum()
|
|
def_retail_cum = def_results['retail_pnl'].cumsum()
|
|
def_big_cum = def_results['big_player_pnl'].cumsum()
|
|
|
|
ax2.plot(opt_results['round'], opt_retail_cum,
|
|
'r-', linewidth=2, label='Optimal: Retail', alpha=0.6)
|
|
ax2.plot(opt_results['round'], opt_big_cum,
|
|
'b-', linewidth=2.5, label='Optimal: Big Players', zorder=3)
|
|
ax2.plot(def_results['round'], def_retail_cum,
|
|
'r--', linewidth=1.5, label='Default: Retail', alpha=0.5)
|
|
ax2.plot(def_results['round'], def_big_cum,
|
|
'b--', linewidth=1.5, label='Default: Big Players', alpha=0.5)
|
|
ax2.axhline(0, color='black', linestyle=':', linewidth=1, alpha=0.5)
|
|
ax2.set_xlabel('Game Round')
|
|
ax2.set_ylabel('Cumulative PnL')
|
|
ax2.set_title('Cumulative PnL: Optimal vs Default', fontweight='bold')
|
|
ax2.legend(fontsize=8)
|
|
ax2.grid(True, alpha=0.3)
|
|
|
|
# Plot 3: PnL Distribution Comparison
|
|
ax3 = fig.add_subplot(gs[0, 2])
|
|
ax3.hist(opt_big_pnls, bins=20, alpha=0.6, label='Optimal: Big Players',
|
|
color='blue', edgecolor='black', density=True)
|
|
ax3.hist(def_big_pnls, bins=20, alpha=0.4, label='Default: Big Players',
|
|
color='lightblue', edgecolor='black', linestyle='--', density=True, histtype='step', linewidth=2)
|
|
ax3.axvline(0, color='black', linestyle='--', linewidth=1, alpha=0.5)
|
|
ax3.axvline(opt_big_pnl, color='blue', linestyle=':', linewidth=2, alpha=0.7, label=f'Optimal Mean: ${opt_big_pnl:,.0f}')
|
|
ax3.axvline(def_big_pnl, color='lightblue', linestyle=':', linewidth=2, alpha=0.7, label=f'Default Mean: ${def_big_pnl:,.0f}')
|
|
ax3.set_xlabel('Total PnL')
|
|
ax3.set_ylabel('Density')
|
|
ax3.set_title('Big Player PnL Distribution', fontweight='bold')
|
|
ax3.legend(fontsize=8)
|
|
ax3.grid(True, alpha=0.3)
|
|
|
|
# Plot 4: Performance Metrics Comparison
|
|
ax4 = fig.add_subplot(gs[1, 0])
|
|
metrics = ['Mean PnL', 'Win Rate', 'Profit Diff']
|
|
optimal_values = [opt_big_pnl / 1000, opt_big_wr * 100, opt_profit_diff / 1000] # Scale for visibility
|
|
default_values = [def_big_pnl / 1000, def_big_wr * 100, def_profit_diff / 1000]
|
|
|
|
x = np.arange(len(metrics))
|
|
width = 0.35
|
|
bars1 = ax4.bar(x - width/2, optimal_values, width, label='Optimal', color='blue', alpha=0.7)
|
|
bars2 = ax4.bar(x + width/2, default_values, width, label='Default', color='red', alpha=0.7)
|
|
|
|
ax4.set_ylabel('Value (Scaled)')
|
|
ax4.set_title('Big Player Performance: Optimal vs Default', fontweight='bold')
|
|
ax4.set_xticks(x)
|
|
ax4.set_xticklabels(metrics)
|
|
ax4.legend()
|
|
ax4.grid(True, alpha=0.3, axis='y')
|
|
ax4.axhline(0, color='black', linestyle='--', linewidth=1)
|
|
|
|
# Add value labels on bars
|
|
for bars in [bars1, bars2]:
|
|
for bar in bars:
|
|
height = bar.get_height()
|
|
ax4.text(bar.get_x() + bar.get_width()/2., height,
|
|
f'{height:.1f}', ha='center', va='bottom', fontsize=8)
|
|
|
|
# Plot 5: Parameter Comparison
|
|
ax5 = fig.add_subplot(gs[1, 1])
|
|
param_names = ['Liquidity', 'Sentiment\nThreshold', 'Volume\nThreshold',
|
|
'Trade Size\n(%)', 'Reversion', 'Num Players']
|
|
optimal_params_list = [
|
|
optimal_params['order_book_liquidity'],
|
|
optimal_params['big_sentiment_threshold'],
|
|
optimal_params['big_volume_threshold'],
|
|
optimal_params['big_trade_size_pct'] * 100,
|
|
optimal_params['fundamental_reversion'] * 1000,
|
|
optimal_params['num_big_players']
|
|
]
|
|
default_params_list = [
|
|
default_params['order_book_liquidity'],
|
|
default_params['big_sentiment_threshold'],
|
|
default_params['big_volume_threshold'],
|
|
default_params['big_trade_size_pct'] * 100,
|
|
default_params['fundamental_reversion'] * 1000,
|
|
default_params['num_big_players']
|
|
]
|
|
|
|
x = np.arange(len(param_names))
|
|
bars1 = ax5.bar(x - width/2, optimal_params_list, width, label='Optimal', color='blue', alpha=0.7)
|
|
bars2 = ax5.bar(x + width/2, default_params_list, width, label='Default', color='red', alpha=0.7)
|
|
|
|
ax5.set_ylabel('Parameter Value')
|
|
ax5.set_title('Configuration Parameters', fontweight='bold')
|
|
ax5.set_xticks(x)
|
|
ax5.set_xticklabels(param_names, fontsize=8)
|
|
ax5.legend()
|
|
ax5.grid(True, alpha=0.3, axis='y')
|
|
|
|
# Plot 6: Summary Statistics
|
|
ax6 = fig.add_subplot(gs[1, 2])
|
|
ax6.axis('off')
|
|
|
|
summary_text = f"""
|
|
PERFORMANCE COMPARISON ({num_simulations} simulations)
|
|
{'='*50}
|
|
|
|
OPTIMAL CONFIGURATION:
|
|
Big Player Mean PnL: ${opt_big_pnl:,.2f}
|
|
Retail Mean PnL: ${opt_retail_pnl:,.2f}
|
|
Profit Difference: ${opt_profit_diff:,.2f}
|
|
Big Player Win Rate: {opt_big_wr:.1%}
|
|
Retail Win Rate: {opt_retail_wr:.1%}
|
|
|
|
DEFAULT CONFIGURATION:
|
|
Big Player Mean PnL: ${def_big_pnl:,.2f}
|
|
Retail Mean PnL: ${def_retail_pnl:,.2f}
|
|
Profit Difference: ${def_profit_diff:,.2f}
|
|
Big Player Win Rate: {def_big_wr:.1%}
|
|
Retail Win Rate: {def_retail_wr:.1%}
|
|
|
|
IMPROVEMENT:
|
|
PnL Improvement: ${opt_profit_diff - def_profit_diff:,.2f}
|
|
Win Rate Improvement: {opt_big_wr - def_big_wr:+.1%}
|
|
Relative Improvement: {(opt_profit_diff / def_profit_diff - 1) * 100:+.1f}%
|
|
"""
|
|
|
|
ax6.text(0.05, 0.5, summary_text, transform=ax6.transAxes,
|
|
fontsize=10, verticalalignment='center', family='monospace',
|
|
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
|
|
|
|
plt.suptitle('Optimal vs Default Configuration Comparison\n'
|
|
f'Genetic Algorithm Optimization Results',
|
|
fontsize=16, fontweight='bold', y=0.995)
|
|
|
|
return fig
|
|
|
|
|
|
def export_results_to_csv(results_df, nash_df, meta_stats, output_dir):
|
|
"""Export comprehensive results to CSV files for LaTeX paper writing"""
|
|
import os
|
|
|
|
# 1. Full game results
|
|
results_df.to_csv(os.path.join(output_dir, 'game_theory_full_results.csv'), index=False)
|
|
|
|
# 2. Per-game summary
|
|
game_summary = []
|
|
for sim in results_df['simulation'].unique():
|
|
sim_data = results_df[results_df['simulation'] == sim]
|
|
price_efficiency = 1 - np.abs(sim_data['price'].values - 100).mean() / 100
|
|
|
|
game_summary.append({
|
|
'simulation': sim,
|
|
'final_price': sim_data['price'].iloc[-1],
|
|
'total_retail_pnl': sim_data['retail_pnl'].sum(),
|
|
'total_big_player_pnl': sim_data['big_player_pnl'].sum(),
|
|
'avg_retail_sentiment': sim_data['retail_sentiment'].mean(),
|
|
'retail_sentiment_volatility': sim_data['retail_sentiment'].std(),
|
|
'retail_volume_volatility': sim_data['retail_volume'].std(),
|
|
'sentiment_volume_correlation': sim_data['retail_sentiment'].corr(sim_data['retail_volume']),
|
|
'market_efficiency': max(0, price_efficiency),
|
|
'equilibrium_price': nash_df[nash_df['simulation'] == sim]['equilibrium_price'].values[0] if len(nash_df[nash_df['simulation'] == sim]) > 0 else np.nan,
|
|
'equilibrium_sentiment': nash_df[nash_df['simulation'] == sim]['equilibrium_retail_sentiment'].values[0] if len(nash_df[nash_df['simulation'] == sim]) > 0 else np.nan
|
|
})
|
|
|
|
game_summary_df = pd.DataFrame(game_summary)
|
|
game_summary_df.to_csv(os.path.join(output_dir, 'game_theory_per_game_summary.csv'), index=False)
|
|
|
|
# 3. Meta-analysis summary
|
|
meta_summary = []
|
|
for category, stats in meta_stats.items():
|
|
for metric, value in stats.items():
|
|
meta_summary.append({
|
|
'category': category,
|
|
'metric': metric,
|
|
'value': value
|
|
})
|
|
|
|
meta_summary_df = pd.DataFrame(meta_summary)
|
|
meta_summary_df.to_csv(os.path.join(output_dir, 'game_theory_meta_analysis.csv'), index=False)
|
|
|
|
# 4. Nash equilibrium summary
|
|
nash_df.to_csv(os.path.join(output_dir, 'game_theory_nash_equilibria.csv'), index=False)
|
|
|
|
print(f"\nCSV files exported to {output_dir}:")
|
|
print(" - game_theory_full_results.csv (all round-by-round data)")
|
|
print(" - game_theory_per_game_summary.csv (per-game aggregated metrics)")
|
|
print(" - game_theory_meta_analysis.csv (meta-analysis across all games)")
|
|
print(" - game_theory_nash_equilibria.csv (Nash equilibrium for each game)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import os
|
|
import sys
|
|
|
|
# Check if optimization mode
|
|
if len(sys.argv) > 1 and sys.argv[1] == '--optimize':
|
|
print("=" * 60)
|
|
print("GENETIC ALGORITHM OPTIMIZATION MODE")
|
|
print("=" * 60)
|
|
|
|
# Run grid search (faster for initial exploration)
|
|
print("\n[Step 1] Running grid search optimization...")
|
|
grid_results_df, optimal_from_grid = grid_search_optimization(num_simulations=5)
|
|
|
|
# Use grid search result as starting point for genetic algorithm
|
|
print("\n[Step 2] Running genetic algorithm optimization...")
|
|
optimal_params, ga_result = genetic_optimization(
|
|
num_generations=15,
|
|
population_size=25,
|
|
num_simulations=5
|
|
)
|
|
|
|
# Generate visualization
|
|
print("\n[Step 3] Generating optimization visualization...")
|
|
fig, optimal_params_final, optimal_results = plot_optimization_results(
|
|
optimal_params,
|
|
grid_results_df=grid_results_df,
|
|
num_simulations=20
|
|
)
|
|
|
|
# Save figure
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
figures_dir = os.path.join(script_dir, '..', 'figures')
|
|
figures_path = os.path.abspath(figures_dir)
|
|
os.makedirs(figures_path, exist_ok=True)
|
|
|
|
output_path = os.path.join(figures_path, 'game_theory_optimization.png')
|
|
plt.savefig(output_path, dpi=300, bbox_inches='tight')
|
|
print(f"\n✓ Optimization figure saved to {output_path}")
|
|
|
|
# Also create a comparison figure: Optimal vs Default configuration
|
|
print("\n[Step 4] Generating comparison figure (Optimal vs Default)...")
|
|
fig_comparison = create_comparison_figure(optimal_params_final, num_simulations=20)
|
|
comparison_path = os.path.join(figures_path, 'game_theory_optimal_vs_default.png')
|
|
plt.savefig(comparison_path, dpi=300, bbox_inches='tight')
|
|
print(f"✓ Comparison figure saved to {comparison_path}")
|
|
plt.close('all')
|
|
|
|
# Save optimal configuration
|
|
import json
|
|
config_path = os.path.join(script_dir, '..', 'optimal_config.json')
|
|
with open(config_path, 'w') as f:
|
|
json.dump(optimal_params_final, f, indent=2)
|
|
print(f"✓ Optimal configuration saved to {config_path}")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("OPTIMIZATION COMPLETE!")
|
|
print("=" * 60)
|
|
print(f"Figures saved:")
|
|
print(f" - {output_path}")
|
|
print(f" - {comparison_path}")
|
|
print(f"Configuration saved: {config_path}")
|
|
print("=" * 60)
|
|
|
|
else:
|
|
# Standard analysis mode
|
|
print("Running Game Theory Trading Simulation...")
|
|
print("=" * 60)
|
|
print("Modeling strategic interaction between:")
|
|
print(" - Retail traders (FOMO-driven, herding behavior)")
|
|
print(" - Big players (strategic, exploit retail behavior)")
|
|
print(" - Finite repeated games (not infinite)")
|
|
print("=" * 60)
|
|
print("\nNote: Run with --optimize flag to find optimal configuration")
|
|
print("=" * 60)
|
|
|
|
num_games = 100
|
|
fig, results_df, nash_df, meta_stats = plot_game_analysis(num_simulations=num_games, show_random_games=10)
|
|
|
|
# Save figure
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
figures_dir = os.path.join(script_dir, '..', 'figures')
|
|
figures_path = os.path.abspath(figures_dir)
|
|
os.makedirs(figures_path, exist_ok=True)
|
|
|
|
output_path = os.path.join(figures_path, 'game_theory_trading.png')
|
|
plt.savefig(output_path, dpi=300, bbox_inches='tight')
|
|
print(f"\nFigure saved to {output_path}")
|
|
plt.close()
|
|
|
|
# Export CSV files
|
|
csv_output_dir = os.path.join(script_dir, '..')
|
|
csv_output_path = os.path.abspath(csv_output_dir)
|
|
export_results_to_csv(results_df, nash_df, meta_stats, csv_output_path)
|
|
|
|
# Print meta-analysis summary
|
|
print("\n=== Meta-Analysis Results (100 Games) ===")
|
|
print(f"\nRetail Traders Performance:")
|
|
print(f" Mean Final PnL: ${meta_stats['retail_pnl']['mean']:,.2f}")
|
|
print(f" Median Final PnL: ${meta_stats['retail_pnl']['median']:,.2f}")
|
|
print(f" Std Dev: ${meta_stats['retail_pnl']['std']:,.2f}")
|
|
print(f" Win Rate: {meta_stats['retail_pnl']['win_rate']:.2%}")
|
|
print(f" Sharpe Ratio: {meta_stats['retail_pnl']['sharpe']:.4f}")
|
|
|
|
print(f"\nBig Players Performance:")
|
|
print(f" Mean Final PnL: ${meta_stats['big_player_pnl']['mean']:,.2f}")
|
|
print(f" Median Final PnL: ${meta_stats['big_player_pnl']['median']:,.2f}")
|
|
print(f" Std Dev: ${meta_stats['big_player_pnl']['std']:,.2f}")
|
|
print(f" Win Rate: {meta_stats['big_player_pnl']['win_rate']:.2%}")
|
|
print(f" Sharpe Ratio: {meta_stats['big_player_pnl']['sharpe']:.4f}")
|
|
|
|
print(f"\nNash Equilibrium Analysis:")
|
|
print(f" Mean Equilibrium Price: ${meta_stats['nash_equilibrium']['mean_price']:.2f}")
|
|
print(f" Std Dev Price: ${meta_stats['nash_equilibrium']['std_price']:.2f}")
|
|
print(f" Mean Price Deviation: {meta_stats['nash_equilibrium']['mean_deviation']:.4f}")
|
|
print(f" Mean Retail Sentiment: {meta_stats['nash_equilibrium']['mean_sentiment']:.4f}")
|
|
|
|
print(f"\nMarket Efficiency:")
|
|
print(f" Mean: {meta_stats['market_efficiency']['mean']:.4f}")
|
|
print(f" Median: {meta_stats['market_efficiency']['median']:.4f}")
|
|
print(f" Std Dev: {meta_stats['market_efficiency']['std']:.4f}")
|
|
print(f" Range: [{meta_stats['market_efficiency']['min']:.4f}, {meta_stats['market_efficiency']['max']:.4f}]")
|
|
|
|
print(f"\nFOMO and Herding Effects:")
|
|
print(f" Average Sentiment Volatility: {meta_stats['fomo_herding']['avg_sentiment_volatility']:.4f}")
|
|
print(f" Average Volume Volatility: {meta_stats['fomo_herding']['avg_volume_volatility']:.4f}")
|
|
print(f" Sentiment-Volume Correlation: {meta_stats['fomo_herding']['sentiment_volume_correlation']:.4f} ± {meta_stats['fomo_herding']['sentiment_volume_correlation_std']:.4f}")
|
|
|
|
print(f"\nExploitation Analysis:")
|
|
print(f" Mean Exploitation Score: {meta_stats['exploitation']['mean']:.4f}")
|
|
print(f" Median: {meta_stats['exploitation']['median']:.4f}")
|
|
print(f" Std Dev: {meta_stats['exploitation']['std']:.4f}")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("Key Insights:")
|
|
print("1. Retail traders exhibit FOMO-driven behavior (sentiment-volume correlation)")
|
|
print("2. Big players strategically exploit retail sentiment extremes")
|
|
print("3. Games are finite, leading to different equilibria than infinite games")
|
|
print("4. Market efficiency depends on the balance of power between players")
|
|
print("5. Meta-analysis across 100 games provides robust statistical evidence")
|
|
print("=" * 60)
|