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 <noreply@anthropic.com>
This commit is contained in:
parent
0dc3c41111
commit
0698ced6fe
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue