From 0698ced6fed600619264557101cfe053965f20c8 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 3 Apr 2026 13:19:29 -0700 Subject: [PATCH] Add ABC & market analysis notebook, all Dune data downloaded All 18 Dune queries now downloaded (98K+ total rows): - ABC trades: 2,615 detailed + 32K raw (price, reserve, supply per txn) - DEX trades: 26,107 (Honeyswap + Velodrome), with buys/sells split - ABC tributes: monthly buy/sell tribute distribution - Holders: 723 ranked with concentration curve, daily holder changes - Trade summaries: monthly action breakdowns Notebook 05 analyzes: - ABC price history and reserve ratio evolution - Buy/sell asymmetry (5.6:1 sell:buy ratio) - Tribute revenue (insufficient to sustain common pool) - ABC vs DEX price divergence and spread - Token holder concentration (top holder: gideonro.eth at 13.2%) - DEX volume and liquidity across Honeyswap/Velodrome Co-Authored-By: Claude Opus 4.6 --- notebooks/05_abc_and_markets.ipynb | 585 +++++++++++++++++++++++++++++ 1 file changed, 585 insertions(+) create mode 100644 notebooks/05_abc_and_markets.ipynb diff --git a/notebooks/05_abc_and_markets.ipynb b/notebooks/05_abc_and_markets.ipynb new file mode 100644 index 0000000..b9fabe0 --- /dev/null +++ b/notebooks/05_abc_and_markets.ipynb @@ -0,0 +1,585 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 05 — Augmented Bonding Curve & Market Analysis\n", + "\n", + "The core questions:\n", + "1. How did the ABC price track vs secondary market (Honeyswap/Velodrome)?\n", + "2. Was the buy/sell ratio sustainable? (Spoiler: 2,217 sells vs 398 buys)\n", + "3. How much tribute revenue did the ABC generate?\n", + "4. How concentrated was token ownership?\n", + "5. Did price discovery happen on the ABC or the DEX?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.dates as mdates\n", + "import seaborn as sns\n", + "\n", + "sns.set_theme(style='whitegrid', palette='deep')\n", + "plt.rcParams['figure.figsize'] = (14, 6)\n", + "plt.rcParams['figure.dpi'] = 100\n", + "\n", + "DATA = '../data/onchain'\n", + "\n", + "# Load datasets\n", + "abc_trades = pd.read_csv(f'{DATA}/dune_q2994918.csv') # ABC with reserve/supply\n", + "abc_all = pd.read_csv(f'{DATA}/dune_q3743980.csv') # All ABC trades\n", + "dex_all = pd.read_csv(f'{DATA}/dune_q3743672.csv') # All DEX trades\n", + "dex_buys = pd.read_csv(f'{DATA}/dune_q3743698.csv') # DEX buys\n", + "dex_sells = pd.read_csv(f'{DATA}/dune_q3743704.csv') # DEX sells\n", + "tributes_a = pd.read_csv(f'{DATA}/dune_q3744004.csv') # Tributes monthly\n", + "tributes_b = pd.read_csv(f'{DATA}/dune_q3744008.csv') # Tributes monthly alt\n", + "holders = pd.read_csv(f'{DATA}/dune_q3743746.csv') # Top holders\n", + "holder_hist = pd.read_csv(f'{DATA}/dune_q3743731.csv') # Holder count over time\n", + "supply = pd.read_csv(f'{DATA}/dune_q3743718.csv') # Holders & supply\n", + "trade_summary = pd.read_csv(f'{DATA}/dune_q3743710.csv') # Trade action summary\n", + "\n", + "# Parse dates\n", + "abc_trades['date'] = pd.to_datetime(abc_trades['block_time'])\n", + "abc_all['date'] = pd.to_datetime(abc_all['block_time'])\n", + "dex_all['date'] = pd.to_datetime(dex_all['block_time'])\n", + "\n", + "print(f'ABC trades (detailed): {len(abc_trades)} rows, {abc_trades[\"date\"].min().date()} to {abc_trades[\"date\"].max().date()}')\n", + "print(f'ABC trades (all): {len(abc_all)} rows')\n", + "print(f'DEX trades: {len(dex_all)} rows, {dex_all[\"date\"].min().date()} to {dex_all[\"date\"].max().date()}')\n", + "print(f' Projects: {dex_all[\"project\"].unique()}')\n", + "print(f'ABC action counts: {abc_trades[\"action\"].value_counts().to_dict()}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. ABC Price History & Reserve Ratio" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "abc_sorted = abc_trades.sort_values('date').copy()\n", + "\n", + "fig, axes = plt.subplots(3, 1, figsize=(16, 14), sharex=True)\n", + "\n", + "# Price over time colored by action\n", + "ax = axes[0]\n", + "buys = abc_sorted[abc_sorted['action'] == 'Buy']\n", + "sells = abc_sorted[abc_sorted['action'] == 'Sell']\n", + "ax.scatter(buys['date'], buys['price_per_token'], c='#2ecc71', s=15, alpha=0.5, label=f'Buy ({len(buys)})')\n", + "ax.scatter(sells['date'], sells['price_per_token'], c='#e74c3c', s=15, alpha=0.5, label=f'Sell ({len(sells)})')\n", + "ax.set_ylabel('Price (xDAI per TEC)')\n", + "ax.set_title('ABC Price Over Time')\n", + "ax.legend()\n", + "\n", + "# Reserve balance\n", + "ax = axes[1]\n", + "ax.plot(abc_sorted['date'], abc_sorted['reserve_balance'], color='#3498db', linewidth=1)\n", + "ax.fill_between(abc_sorted['date'], abc_sorted['reserve_balance'], alpha=0.2, color='#3498db')\n", + "ax.set_ylabel('Reserve Balance (xDAI)')\n", + "ax.set_title('ABC Reserve Pool Over Time')\n", + "\n", + "# Supply\n", + "ax = axes[2]\n", + "ax.plot(abc_sorted['date'], abc_sorted['cumulative_supply'], color='teal', linewidth=1)\n", + "ax.fill_between(abc_sorted['date'], abc_sorted['cumulative_supply'], alpha=0.2, color='teal')\n", + "ax.set_ylabel('Token Supply')\n", + "ax.set_title('TEC Circulating Supply')\n", + "\n", + "for ax in axes:\n", + " ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n", + " ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))\n", + "\n", + "plt.setp(axes[-1].xaxis.get_majorticklabels(), rotation=45)\n", + "plt.tight_layout()\n", + "plt.savefig(f'{DATA}/../snapshots/abc_price_reserve.png', dpi=150, bbox_inches='tight')\n", + "plt.show()\n", + "\n", + "print(f'\\nABC Price Stats:')\n", + "print(f' Peak: {abc_sorted[\"price_per_token\"].max():.4f} xDAI')\n", + "print(f' Trough: {abc_sorted[\"price_per_token\"].min():.4f} xDAI')\n", + "print(f' Drawdown: {(abc_sorted[\"price_per_token\"].min() / abc_sorted[\"price_per_token\"].max() - 1):.1%}')\n", + "print(f'\\nReserve:')\n", + "print(f' Peak: {abc_sorted[\"reserve_balance\"].max():,.0f} xDAI')\n", + "print(f' Final: {abc_sorted[\"reserve_balance\"].iloc[-1]:,.0f} xDAI')\n", + "print(f'\\nSupply:')\n", + "print(f' Peak: {abc_sorted[\"cumulative_supply\"].max():,.0f} TEC')\n", + "print(f' Final: {abc_sorted[\"cumulative_supply\"].iloc[-1]:,.0f} TEC')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Buy/Sell Asymmetry — The Structural Pressure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Monthly buy vs sell volume\n", + "abc_monthly = abc_sorted.copy()\n", + "abc_monthly['month'] = abc_monthly['date'].dt.to_period('M').dt.to_timestamp()\n", + "\n", + "monthly_agg = abc_monthly.groupby(['month', 'action']).agg(\n", + " count=('Amount', 'count'),\n", + " total_amount=('Amount', 'sum'),\n", + " total_tokens=('noToken', 'sum'),\n", + " avg_price=('price_per_token', 'mean'),\n", + ").reset_index()\n", + "\n", + "fig, axes = plt.subplots(2, 2, figsize=(18, 10))\n", + "\n", + "# Transaction count by month\n", + "ax = axes[0, 0]\n", + "for action, color in [('Buy', '#2ecc71'), ('Sell', '#e74c3c')]:\n", + " subset = monthly_agg[monthly_agg['action'] == action]\n", + " ax.bar(subset['month'] + pd.Timedelta(days=8 if action == 'Sell' else -8),\n", + " subset['count'], width=15, color=color, alpha=0.7, label=action)\n", + "ax.set_ylabel('Transaction Count')\n", + "ax.set_title('Monthly ABC Transaction Count')\n", + "ax.legend()\n", + "\n", + "# Volume by month (xDAI amount)\n", + "ax = axes[0, 1]\n", + "for action, color in [('Buy', '#2ecc71'), ('Sell', '#e74c3c')]:\n", + " subset = monthly_agg[monthly_agg['action'] == action]\n", + " ax.bar(subset['month'] + pd.Timedelta(days=8 if action == 'Sell' else -8),\n", + " subset['total_amount'], width=15, color=color, alpha=0.7, label=action)\n", + "ax.set_ylabel('Volume (xDAI)')\n", + "ax.set_title('Monthly ABC Volume')\n", + "ax.legend()\n", + "\n", + "# Buy/sell ratio over time\n", + "ax = axes[1, 0]\n", + "buy_monthly = monthly_agg[monthly_agg['action'] == 'Buy'].set_index('month')['count']\n", + "sell_monthly = monthly_agg[monthly_agg['action'] == 'Sell'].set_index('month')['count']\n", + "ratio = (buy_monthly / sell_monthly).dropna()\n", + "colors = ['#2ecc71' if r >= 1 else '#e74c3c' for r in ratio]\n", + "ax.bar(ratio.index, ratio.values, width=20, color=colors, alpha=0.7)\n", + "ax.axhline(y=1, color='black', linestyle='--', alpha=0.5, label='Parity')\n", + "ax.set_ylabel('Buy/Sell Ratio')\n", + "ax.set_title('Monthly Buy/Sell Ratio (<1 = net selling pressure)')\n", + "ax.legend()\n", + "\n", + "# Cumulative net flow\n", + "ax = axes[1, 1]\n", + "abc_sorted_copy = abc_sorted.copy()\n", + "abc_sorted_copy['signed_amount'] = abc_sorted_copy.apply(\n", + " lambda r: r['Amount'] if r['action'] == 'Buy' else -r['Amount'], axis=1\n", + ")\n", + "abc_sorted_copy['cum_flow'] = abc_sorted_copy['signed_amount'].cumsum()\n", + "ax.plot(abc_sorted_copy['date'], abc_sorted_copy['cum_flow'], color='steelblue', linewidth=1.5)\n", + "ax.fill_between(abc_sorted_copy['date'], abc_sorted_copy['cum_flow'],\n", + " where=abc_sorted_copy['cum_flow'] >= 0, color='#2ecc71', alpha=0.2)\n", + "ax.fill_between(abc_sorted_copy['date'], abc_sorted_copy['cum_flow'],\n", + " where=abc_sorted_copy['cum_flow'] < 0, color='#e74c3c', alpha=0.2)\n", + "ax.axhline(y=0, color='black', linewidth=0.5)\n", + "ax.set_ylabel('Cumulative Net Flow (xDAI)')\n", + "ax.set_title('Cumulative Buy - Sell Flow (negative = capital leaving)')\n", + "\n", + "for row in axes:\n", + " for ax in row:\n", + " ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(f'{DATA}/../snapshots/abc_buysell.png', dpi=150, bbox_inches='tight')\n", + "plt.show()\n", + "\n", + "total_buy = abc_sorted[abc_sorted['action'] == 'Buy']['Amount'].sum()\n", + "total_sell = abc_sorted[abc_sorted['action'] == 'Sell']['Amount'].sum()\n", + "print(f'\\nBuy/Sell Analysis:')\n", + "print(f' Total buys: {len(buys)} txns, {total_buy:,.0f} xDAI')\n", + "print(f' Total sells: {len(sells)} txns, {total_sell:,.0f} xDAI')\n", + "print(f' Sell:Buy ratio (count): {len(sells)/len(buys):.1f}:1')\n", + "print(f' Sell:Buy ratio (volume): {total_sell/total_buy:.1f}:1')\n", + "print(f' Net outflow: {total_sell - total_buy:,.0f} xDAI')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. ABC Tribute Revenue" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Tribute from individual trades\n", + "abc_sorted['tribute_val'] = pd.to_numeric(abc_sorted['tribute'], errors='coerce')\n", + "tribute_by_action = abc_sorted.groupby('action')['tribute_val'].sum()\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(16, 6))\n", + "\n", + "# Cumulative tribute\n", + "ax = axes[0]\n", + "for action, color in [('Buy', '#2ecc71'), ('Sell', '#e74c3c')]:\n", + " subset = abc_sorted[abc_sorted['action'] == action].copy()\n", + " subset['cum_tribute'] = subset['tribute_val'].cumsum()\n", + " ax.plot(subset['date'], subset['cum_tribute'], color=color, linewidth=1.5, label=f'{action} tribute')\n", + "ax.set_ylabel('Cumulative Tribute (xDAI)')\n", + "ax.set_title('ABC Tribute Revenue Over Time')\n", + "ax.legend()\n", + "\n", + "# Monthly tribute from aggregated data\n", + "ax = axes[1]\n", + "# Use the detailed tribute tables\n", + "for df, label in [(tributes_a, 'Post-migration'), (tributes_b, 'Alt')]:\n", + " df['date'] = pd.to_datetime(df['date'])\n", + "\n", + "t_pivot = tributes_a.pivot_table(index='date', columns='action', values='Tribute_Distribution', aggfunc='sum').fillna(0)\n", + "if 'Buy' in t_pivot.columns:\n", + " ax.bar(t_pivot.index, t_pivot.get('Buy', 0), width=20, color='#2ecc71', alpha=0.7, label='Buy tribute')\n", + "if 'Sell' in t_pivot.columns:\n", + " ax.bar(t_pivot.index, t_pivot.get('Sell', 0), width=20, color='#e74c3c', alpha=0.7,\n", + " bottom=t_pivot.get('Buy', 0), label='Sell tribute')\n", + "ax.set_ylabel('Tribute (xDAI)')\n", + "ax.set_title('Monthly ABC Tribute Revenue')\n", + "ax.legend()\n", + "\n", + "for ax in axes:\n", + " ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(f'{DATA}/../snapshots/abc_tributes.png', dpi=150, bbox_inches='tight')\n", + "plt.show()\n", + "\n", + "print(f'\\nTribute Revenue:')\n", + "for action in ['Buy', 'Sell']:\n", + " t = tribute_by_action.get(action, 0)\n", + " print(f' {action} tribute: {t:,.2f} xDAI')\n", + "print(f' Total: {tribute_by_action.sum():,.2f} xDAI')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Primary (ABC) vs Secondary (DEX) Market\n", + "\n", + "Where did price discovery happen? Did the ABC price diverge from the DEX price?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Extract price from DEX trades\n", + "# DEX buys and sells have price_per_token column\n", + "dex_with_price = pd.concat([\n", + " dex_buys[['block_time', 'project', 'action', 'price_per_token', 'Amount']].copy(),\n", + " dex_sells[['block_time', 'project', 'action', 'price_per_token', 'Amount']].copy(),\n", + "])\n", + "dex_with_price['date'] = pd.to_datetime(dex_with_price['block_time'])\n", + "dex_with_price['price_per_token'] = pd.to_numeric(dex_with_price['price_per_token'], errors='coerce')\n", + "dex_with_price = dex_with_price.dropna(subset=['price_per_token'])\n", + "dex_with_price = dex_with_price[dex_with_price['price_per_token'] > 0]\n", + "\n", + "# Daily average price from each venue\n", + "abc_daily_price = abc_sorted.set_index('date')['price_per_token'].resample('D').mean().dropna()\n", + "dex_daily_price = dex_with_price.set_index('date')['price_per_token'].resample('D').mean().dropna()\n", + "\n", + "fig, axes = plt.subplots(2, 1, figsize=(16, 10))\n", + "\n", + "# Price comparison\n", + "ax = axes[0]\n", + "ax.plot(abc_daily_price.index, abc_daily_price.values, label='ABC Price', color='steelblue', linewidth=1, alpha=0.8)\n", + "ax.plot(dex_daily_price.index, dex_daily_price.values, label='DEX Price', color='coral', linewidth=1, alpha=0.8)\n", + "ax.set_ylabel('Price (xDAI per TEC)')\n", + "ax.set_title('ABC vs DEX Price — Where did price discovery happen?')\n", + "ax.legend()\n", + "\n", + "# Price spread/premium\n", + "ax = axes[1]\n", + "aligned_prices = pd.DataFrame({\n", + " 'abc': abc_daily_price,\n", + " 'dex': dex_daily_price\n", + "}).dropna()\n", + "if len(aligned_prices) > 0:\n", + " spread = (aligned_prices['dex'] - aligned_prices['abc']) / aligned_prices['abc'] * 100\n", + " ax.fill_between(spread.index, spread, where=spread >= 0, color='#2ecc71', alpha=0.3, label='DEX premium')\n", + " ax.fill_between(spread.index, spread, where=spread < 0, color='#e74c3c', alpha=0.3, label='DEX discount')\n", + " ax.plot(spread.index, spread, color='black', linewidth=0.5)\n", + " ax.axhline(y=0, color='black', linewidth=0.5)\n", + " ax.set_ylabel('DEX Premium/Discount (%)')\n", + " ax.set_title('DEX Price Relative to ABC (+ = DEX more expensive)')\n", + " ax.legend()\n", + " \n", + " print(f'\\nPrice Spread Stats:')\n", + " print(f' Mean spread: {spread.mean():.1f}%')\n", + " print(f' Median spread: {spread.median():.1f}%')\n", + " print(f' Max DEX premium: {spread.max():.1f}%')\n", + " print(f' Max DEX discount: {spread.min():.1f}%')\n", + " print(f' Days with DEX discount: {(spread < 0).sum()}/{len(spread)}')\n", + "else:\n", + " ax.text(0.5, 0.5, 'Insufficient overlapping price data', transform=ax.transAxes, ha='center')\n", + "\n", + "for ax in axes:\n", + " ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(f'{DATA}/../snapshots/abc_vs_dex.png', dpi=150, bbox_inches='tight')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Token Holder Concentration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Clean holders data (remove HTML from address column)\n", + "holders_clean = holders.copy()\n", + "holders_clean['addr'] = holders_clean['address_raw']\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(16, 7))\n", + "\n", + "# Top 20 holders\n", + "ax = axes[0]\n", + "top20 = holders_clean.head(20)\n", + "labels = top20.apply(\n", + " lambda r: r['ens_name'] if pd.notna(r['ens_name']) and len(str(r['ens_name'])) < 20\n", + " else f\"{str(r['addr'])[:8]}...\", axis=1\n", + ")\n", + "ax.barh(range(len(top20)), top20['balance'], color='steelblue')\n", + "ax.set_yticks(range(len(top20)))\n", + "ax.set_yticklabels(labels, fontsize=8)\n", + "ax.invert_yaxis()\n", + "ax.set_xlabel('TEC Balance')\n", + "ax.set_title(f'Top 20 Token Holders')\n", + "\n", + "# Cumulative concentration\n", + "ax = axes[1]\n", + "ax.plot(holders_clean['rank_number'], holders_clean['cumulative_perc'] * 100,\n", + " color='coral', linewidth=2)\n", + "ax.axhline(y=50, color='gray', linestyle='--', alpha=0.5)\n", + "ax.axhline(y=80, color='gray', linestyle='--', alpha=0.5)\n", + "# Find the rank at 50% and 80%\n", + "rank_50 = holders_clean[holders_clean['cumulative_perc'] >= 0.5].iloc[0]['rank_number']\n", + "rank_80 = holders_clean[holders_clean['cumulative_perc'] >= 0.8].iloc[0]['rank_number']\n", + "ax.axvline(x=rank_50, color='gray', linestyle=':', alpha=0.5)\n", + "ax.axvline(x=rank_80, color='gray', linestyle=':', alpha=0.5)\n", + "ax.text(rank_50 + 5, 48, f'50% at rank {int(rank_50)}', fontsize=9)\n", + "ax.text(rank_80 + 5, 78, f'80% at rank {int(rank_80)}', fontsize=9)\n", + "ax.set_xlabel('Holder Rank')\n", + "ax.set_ylabel('Cumulative % of Supply')\n", + "ax.set_title('Token Concentration Curve')\n", + "ax.set_xlim(0, min(200, len(holders_clean)))\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(f'{DATA}/../snapshots/holder_concentration.png', dpi=150, bbox_inches='tight')\n", + "plt.show()\n", + "\n", + "top1 = holders_clean.iloc[0]\n", + "top10_pct = holders_clean.head(10)['cumulative_perc'].iloc[-1]\n", + "print(f'\\nHolder Concentration:')\n", + "print(f' Total holders: {len(holders_clean)}')\n", + "print(f' Top 1 ({top1[\"ens_name\"]}): {top1[\"perc\"]:.1%} of supply')\n", + "print(f' Top 10: {top10_pct:.1%} of supply')\n", + "print(f' 50% of supply held by top {int(rank_50)} holders')\n", + "print(f' 80% of supply held by top {int(rank_80)} holders')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Holder Growth Over Time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "holder_hist['date'] = pd.to_datetime(holder_hist['Date'])\n", + "\n", + "fig, axes = plt.subplots(2, 1, figsize=(16, 10))\n", + "\n", + "# Total holders over time\n", + "ax = axes[0]\n", + "for chain in holder_hist['chain'].unique():\n", + " subset = holder_hist[holder_hist['chain'] == chain].sort_values('date')\n", + " ax.plot(subset['date'], subset['Holders'], label=chain, linewidth=1.5)\n", + "ax.set_ylabel('Total Holders')\n", + "ax.set_title('TEC Token Holders Over Time')\n", + "ax.legend()\n", + "\n", + "# Net holder changes\n", + "ax = axes[1]\n", + "for chain in holder_hist['chain'].unique():\n", + " subset = holder_hist[holder_hist['chain'] == chain].sort_values('date')\n", + " colors = ['#2ecc71' if c >= 0 else '#e74c3c' for c in subset['Changes']]\n", + " ax.bar(subset['date'], subset['Changes'], color=colors, width=1, alpha=0.6, label=chain)\n", + "ax.axhline(y=0, color='black', linewidth=0.5)\n", + "ax.set_ylabel('Net Holder Change')\n", + "ax.set_title('Daily Net Holder Changes (+ = new holders, - = leaving)')\n", + "ax.legend()\n", + "\n", + "for ax in axes:\n", + " ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(f'{DATA}/../snapshots/holder_growth.png', dpi=150, bbox_inches='tight')\n", + "plt.show()\n", + "\n", + "net_change = holder_hist.groupby('chain')['Changes'].sum()\n", + "print(f'\\nHolder dynamics:')\n", + "for chain, change in net_change.items():\n", + " peak = holder_hist[holder_hist['chain'] == chain]['Holders'].max()\n", + " final = holder_hist[holder_hist['chain'] == chain].sort_values('date')['Holders'].iloc[-1]\n", + " print(f' {chain}: peak {peak} holders, final {final}, net change {change:+d}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. DEX Volume & Liquidity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# DEX trade volume over time\n", + "dex_all['amount_usd_num'] = pd.to_numeric(dex_all['amount_usd'], errors='coerce')\n", + "dex_all['month'] = dex_all['date'].dt.to_period('M').dt.to_timestamp()\n", + "\n", + "dex_monthly = dex_all.groupby(['month', 'project']).agg(\n", + " trade_count=('block_time', 'count'),\n", + " volume_usd=('amount_usd_num', 'sum'),\n", + ").reset_index()\n", + "\n", + "fig, axes = plt.subplots(2, 1, figsize=(16, 10))\n", + "\n", + "# Trade count\n", + "ax = axes[0]\n", + "for project in dex_monthly['project'].unique():\n", + " subset = dex_monthly[dex_monthly['project'] == project]\n", + " ax.bar(subset['month'], subset['trade_count'], width=20, alpha=0.7, label=project)\n", + "ax.set_ylabel('Trade Count')\n", + "ax.set_title('Monthly DEX Trade Count')\n", + "ax.legend()\n", + "\n", + "# USD Volume (where available)\n", + "ax = axes[1]\n", + "vol = dex_monthly[dex_monthly['volume_usd'] > 0]\n", + "if len(vol) > 0:\n", + " for project in vol['project'].unique():\n", + " subset = vol[vol['project'] == project]\n", + " ax.bar(subset['month'], subset['volume_usd'], width=20, alpha=0.7, label=project)\n", + " ax.set_ylabel('Volume (USD)')\n", + " ax.set_title('Monthly DEX Volume (USD)')\n", + " ax.legend()\n", + "else:\n", + " ax.text(0.5, 0.5, 'USD volume data not available for most trades',\n", + " transform=ax.transAxes, ha='center', fontsize=12)\n", + "\n", + "for ax in axes:\n", + " ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(f'{DATA}/../snapshots/dex_volume.png', dpi=150, bbox_inches='tight')\n", + "plt.show()\n", + "\n", + "print(f'\\nDEX Activity:')\n", + "print(f' Total trades: {len(dex_all)}')\n", + "for project in dex_all['project'].unique():\n", + " n = len(dex_all[dex_all['project'] == project])\n", + " print(f' {project}: {n} trades')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Key Findings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print('=' * 70)\n", + "print('ABC & MARKET ANALYSIS — KEY FINDINGS')\n", + "print('=' * 70)\n", + "print(f'''\n", + "BONDING CURVE MECHANICS:\n", + " Price peak: {abc_sorted[\"price_per_token\"].max():.2f} xDAI\n", + " Price final: {abc_sorted[\"price_per_token\"].iloc[-1]:.2f} xDAI\n", + " Drawdown: {(abc_sorted[\"price_per_token\"].iloc[-1] / abc_sorted[\"price_per_token\"].max() - 1):.0%}\n", + " Reserve peak: {abc_sorted[\"reserve_balance\"].max():,.0f} xDAI → {abc_sorted[\"reserve_balance\"].iloc[-1]:,.0f}\n", + "\n", + "STRUCTURAL SELL PRESSURE:\n", + " Buy transactions: {len(buys)} ({len(buys)/(len(buys)+len(sells)):.0%})\n", + " Sell transactions: {len(sells)} ({len(sells)/(len(buys)+len(sells)):.0%})\n", + " Sell:Buy ratio: {len(sells)/len(buys):.1f}:1\n", + " The ABC experienced persistent, overwhelming sell pressure.\n", + " \n", + "TRIBUTE REVENUE:\n", + " Total: {tribute_by_action.sum():,.0f} xDAI\n", + " This was far too small to sustain common pool operations.\n", + "\n", + "HOLDER CONCENTRATION:\n", + " {len(holders_clean)} total holders\n", + " Top holder ({holders_clean.iloc[0][\"ens_name\"]}): {holders_clean.iloc[0][\"perc\"]:.1%}\n", + " Top 10: {top10_pct:.1%}\n", + "\n", + "CRITICAL QUESTIONS:\n", + " 1. Did the ABC entry tribute discourage buying? (effectively a tax on participation)\n", + " 2. Did the exit tribute slow selling enough, or just reduce returns?\n", + " 3. Was the reserve ratio appropriate for the level of volatility?\n", + " 4. Could the ABC have functioned better with different parameters?\n", + " 5. Did DEX liquidity create a cheaper exit path that undermined the ABC?\n", + "''')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}