diff --git a/examples/information_cascades/README.md b/examples/information_cascades/README.md new file mode 100644 index 000000000..8603fdb16 --- /dev/null +++ b/examples/information_cascades/README.md @@ -0,0 +1,90 @@ +# Information Cascades & Trading Behavior Model + +This example implements a hybrid model of **Information Cascades and Trading Behavior** using **Mesa** and **Solara**. + +The model studies how individual opinions evolve through repeated pairwise interactions (based on bounded confidence) and how investor overconfidence leads to excessive trading and wealth destruction. + +--- + +## Model Description + +Each investor agent holds a continuous opinion value in the range **(-1, 1)**, an overconfidence level between **(1.0, 5.0)**, and an initial wealth of **1000.0**. +At each time step: + +1. A random pair of agents is selected. +2. If the difference between their opinions is less than a confidence threshold **ε (epsilon)**, they interact. +3. During an interaction, both agents adjust their opinions toward each other by a fraction **μ (mu)**, adjusted by their stubbornness (overconfidence). +4. Agents then execute trades with a probability based on their confidence level. Each trade deducts a fixed transaction cost from their wealth. + +Depending on parameter values, the model can exhibit: +- Herd formation (Consensus) +- Echo chambers (Polarization and Fragmentation) +- Systematic wealth depletion due to overtrading + +--- + +## Parameters + +| Parameter | Description | +|-------------------------|------------| +| `n` | Number of investors in the market | +| `epsilon (ε)` | Confidence threshold controlling whether agents interact | +| `mu (μ)` | Convergence rate controlling how strongly opinions are updated | +| `transaction_cost` | The fee deducted from an agent's wealth per executed trade | + +--- + +## Collected Metrics + +The model tracks the following quantities over time: + +- **Variance** – dispersion of opinions in the population +- **Avg Wealth** – the average remaining capital across all agents + +These metrics are visualized alongside individual opinion trajectories and wealth distribution. + +--- + +## Visualization + +This example includes a Solara-based interactive visualization that shows: + +- Opinion trajectories of all agents (Herd Formation) +- A scatter plot validating that "Trading is Hazardous to Your Wealth" (Confidence vs. Wealth) +- Opinion Variance over time +- Average Wealth over time +- Real-time trading performance stats + +The market parameters can be adjusted in real-time using the sliders. + +--- + +## Installation + +To install the dependencies, use `pip` to install the requirements: + +```bash + $ pip install -r requirements.txt +``` + +From this directory, run: +```bash + $ solara run app.py +``` + +Then open your browser to local host http://localhost:8765/ and press Reset, then Step or Play. + +## Files +- model.py: Defines the TradingDWModel, including market parameters, agent interactions, and data collection. +- agents.py: Defines the InvestorAgent class, the logic for updating agent opinions, and the trading execution mechanism. +- app.py: Contains the code for the interactive Solara visualization, including opinion trajectories, scatter plots, and custom performance metrics. + + +## References +- Barber, B. M., & Odean, T. (2000). +Trading is hazardous to your wealth: The common stock investment performance of individual investors. +The Journal of Finance, 55(2), 773-806. + +- Banerjee, A. V. (1992). +A simple model of herd behavior. +The Quarterly Journal of Economics, 107(3), 797-817. diff --git a/examples/information_cascades/app.py b/examples/information_cascades/app.py new file mode 100644 index 000000000..6f3775b12 --- /dev/null +++ b/examples/information_cascades/app.py @@ -0,0 +1,134 @@ +import matplotlib.pyplot as plt +import solara +import solara.lab +from information_cascades.model import TradingDWModel +from mesa.visualization import SolaraViz, make_plot_component + + +def TradingPerformanceStats(model): + agents = list(model.agents) + if not agents: + return solara.Text("initializing...") + + avg_gross = sum(a.gross_wealth for a in agents) / len(agents) + avg_net = sum(a.net_wealth for a in agents) / len(agents) + + sorted_agents = sorted(agents, key=lambda x: x.trades) + n = len(sorted_agents) + split = max(1, n // 5) + + low_traders = sorted_agents[:split] + high_traders = sorted_agents[-split:] + + avg_net_low = sum(a.net_wealth for a in low_traders) / len(low_traders) + avg_net_high = sum(a.net_wealth for a in high_traders) / len(high_traders) + + return solara.Card( + title="Barber & Odean (2000) Validation", + children=[ + solara.Markdown(f"**Market Avg (Gross)**: {avg_gross:.2f}"), + solara.Markdown(f"**Market Avg (Net)**: {avg_net:.2f}"), + solara.Markdown("---"), + solara.Markdown( + f"**Low Turnover Top 20% Net Wealth**: {avg_net_low:.2f}" + ), + solara.Markdown( + f"**High Turnover Top 20% Net Wealth**: {avg_net_high:.2f}" + ), + ], + ) + + +def WealthVsConfidenceScatter(model): + fig, ax = plt.subplots(figsize=(6, 4), constrained_layout=True) + agents = list(model.agents) + confidences = [a.confidence for a in agents] + wealths = [a.net_wealth for a in agents] + + if wealths: + avg_w = sum(wealths) / len(wealths) + ax.scatter(confidences, wealths, alpha=0.6, c=wealths, cmap="RdYlGn") + + w_min, w_max = min(wealths), max(wealths) + padding = (w_max - w_min) * 0.2 if w_max > w_min else 10 + ax.set_ylim(w_min - padding, w_max + padding) + + ax.axhline(avg_w, color="blue", linestyle="--", alpha=0.5) + ax.fill_between( + [1, 5], + w_min - padding, + avg_w, + color="red", + alpha=0.1, + label="Underperforming", + ) + + ax.set_xlabel("Confidence Level") + ax.set_ylabel("Net Wealth") + ax.set_title("Trading is Hazardous to Your Wealth") + + return solara.FigureMatplotlib(fig) + + +def OpinionTrajectoryPlot(model): + fig, ax = plt.subplots(figsize=(6, 4), constrained_layout=True) + df = model.datacollector.get_agent_vars_dataframe() + if df.empty: + return solara.FigureMatplotlib(fig) + + opinions = df["opinion"].unstack() + opinions.plot(ax=ax, legend=False, alpha=0.4) + ax.set_xlabel("Steps") + ax.set_ylabel("Opinion") + ax.set_title("Herd Formation (Banerjee, 1992)") + return solara.FigureMatplotlib(fig) + + +model_params = { + "n": { + "type": "SliderInt", + "value": 100, + "label": "Number of Investors", + "min": 20, + "max": 300, + "step": 1, + }, + "epsilon": { + "type": "SliderFloat", + "value": 0.6, + "label": "Confidence Threshold (ε)", + "min": 0.01, + "max": 1.0, + "step": 0.01, + }, + "mu": { + "type": "SliderFloat", + "value": 0.1, + "label": "Convergence Rate (μ)", + "min": 0.01, + "max": 0.5, + "step": 0.01, + }, + "transaction_cost": { + "type": "SliderFloat", + "value": 0.5, + "label": "Transaction Cost", + "min": 0, + "max": 10, + "step": 0.5, + }, +} + +initial_model = TradingDWModel() + +page = SolaraViz( + model=initial_model, + model_params=model_params, + components=[ + TradingPerformanceStats, + OpinionTrajectoryPlot, + WealthVsConfidenceScatter, + make_plot_component("Variance"), + make_plot_component(["Avg Gross Wealth", "Avg Net Wealth"]), + ], +) diff --git a/examples/information_cascades/information_cascades/__init__.py b/examples/information_cascades/information_cascades/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/information_cascades/information_cascades/agents.py b/examples/information_cascades/information_cascades/agents.py new file mode 100644 index 000000000..cde088369 --- /dev/null +++ b/examples/information_cascades/information_cascades/agents.py @@ -0,0 +1,36 @@ +from mesa import Agent + + +class InvestorAgent(Agent): + """ + While Banerjee's Herding Effect highlights how investors blindly follow the crowd, Barber & Odean's Overconfidence + Theory explains their stubborn reliance on flawed personal judgment; together, they amplify irrational market + volatility and pricing inefficiencies. + """ + + def __init__(self, model, opinion, confidence): + super().__init__(model) + self.opinion = opinion + self.confidence = confidence + """ + The core of the Barber and Odean theory lies in the critical distinction between gross returns + and net returns, demonstrating how overconfident investors' excessive trading costs erode potential gains. + """ + self.gross_wealth = ( + 1000.0 # Theoretical Market Wealth Under a Buy-and-Hold Strategy (Gross) + ) + self.net_wealth = 1000.0 # Actual Wealth After Deducting Frequent Trading Commissions and Fees (Net) + self.trades = 0 + + def update_opinion(self, other_opinion, mu): + # The more inflated the confidence, the more stubborn the bias (resulting in a smaller effective mu + effective_mu = mu / self.confidence + self.opinion += effective_mu * (other_opinion - self.opinion) + + def execute_trade(self): + # Barber & Odean: Overconfidence leads to excessive turnover rates. + trade_prob = 0.05 * self.confidence + if self.random.random() < trade_prob: + self.trades += 1 + # Only net wealth accounts for the deduction of transaction friction costs. + self.net_wealth -= self.model.transaction_cost diff --git a/examples/information_cascades/information_cascades/model.py b/examples/information_cascades/information_cascades/model.py new file mode 100644 index 000000000..974707cb6 --- /dev/null +++ b/examples/information_cascades/information_cascades/model.py @@ -0,0 +1,74 @@ +import statistics + +from mesa import Model +from mesa.datacollection import DataCollector + +from .agents import InvestorAgent + + +class TradingDWModel(Model): + def __init__(self, n=100, epsilon=0.2, mu=0.5, transaction_cost=2.0, rng=None): + super().__init__(rng=rng) + self.n = n + self.epsilon = epsilon + self.mu = mu + self.transaction_cost = transaction_cost + self.attempted_interactions = 0 + self.accepted_interactions = 0 + + self.datacollector = DataCollector( + model_reporters={ + "Variance": self.compute_variance, + "Avg Gross Wealth": lambda m: ( + statistics.mean([a.gross_wealth for a in m.agents]) + if m.agents + else 0 + ), + "Avg Net Wealth": lambda m: ( + statistics.mean([a.net_wealth for a in m.agents]) if m.agents else 0 + ), + }, + agent_reporters={ + "opinion": "opinion", + "net_wealth": "net_wealth", + "confidence": "confidence", + "trades": "trades", + }, + ) + + for _ in range(self.n): + op = self.random.uniform(-1, 1) + conf = self.random.uniform(1.0, 5.0) + agent = InvestorAgent(self, op, conf) + self.agents.add(agent) + + self.datacollector.collect(self) + + def step(self): + agent_list = list(self.agents) + + # Simulating Natural Market Volatility Returns (Random Walk with Positive Drift + market_return = self.random.normalvariate(0.001, 0.01) + for agent in agent_list: + agent.gross_wealth *= 1 + market_return + agent.net_wealth *= 1 + market_return + + for _ in range(self.n): + agent_a, agent_b = self.random.sample(agent_list, 2) + self.attempted_interactions += 1 + + # Banerjee: Communication within cognitive thresholds leads to opinion convergence (herd formation). + if abs(agent_a.opinion - agent_b.opinion) < self.epsilon: + old_op_a = agent_a.opinion + agent_a.update_opinion(agent_b.opinion, self.mu) + agent_b.update_opinion(old_op_a, self.mu) + + agent_a.execute_trade() + agent_b.execute_trade() + self.accepted_interactions += 1 + + self.datacollector.collect(self) + + def compute_variance(self): + opinions = [a.opinion for a in self.agents] + return statistics.variance(opinions) if len(opinions) > 1 else 0 diff --git a/examples/information_cascades/requirements.txt b/examples/information_cascades/requirements.txt new file mode 100644 index 000000000..e08682a35 --- /dev/null +++ b/examples/information_cascades/requirements.txt @@ -0,0 +1,7 @@ +mesa>3.0.0 +solara +matplotlib +pandas +numpy +networkx +altair \ No newline at end of file