""" 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)