myco-bonding-curve/notebooks/06_dca_and_signals.ipynb

478 lines
17 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# DCA Strategies & Signal Routing\n",
"\n",
"Interactive exploration of the DCA execution engine, signal router,\n",
"subscription DCA, and CRDT-native schedule replication.\n",
"\n",
"**Sections:**\n",
"1. Signal Router Response across price trajectories\n",
"2. DCA vs Lump Sum comparison\n",
"3. Subscription DCA + Loyalty bonuses\n",
"4. CRDT Schedule Lifecycle (create → materialize → reconcile → merge)\n",
"5. CRDT Network Simulation — divergence and convergence"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%matplotlib inline\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.colors import to_rgba\n",
"\n",
"plt.rcParams['figure.figsize'] = (12, 6)\n",
"plt.rcParams['figure.dpi'] = 100"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Signal Router Response\n",
"\n",
"Four price trajectories (stable, bull, crash, volatile) fed through the\n",
"signal router. Each subplot shows how adaptive parameters respond."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from src.primitives.signal_router import (\n",
" AdaptiveParams, SignalRouterConfig, simulate_signal_routing,\n",
")\n",
"\n",
"base_params = AdaptiveParams(\n",
" flow_threshold=0.1,\n",
" pamm_alpha_bar=10.0,\n",
" surge_fee_rate=0.05,\n",
" oracle_multiplier_velocity=0.0,\n",
")\n",
"config = SignalRouterConfig()\n",
"\n",
"# Generate trajectories\n",
"steps = 200\n",
"t = np.linspace(0, 1, steps)\n",
"trajectories = {\n",
" 'stable': [1.0] * steps,\n",
" 'bull': (1.0 + t * 0.5).tolist(),\n",
" 'crash': (1.0 - 0.4 * t + 0.1 * np.sin(t * 20)).tolist(),\n",
" 'volatile': (1.0 + 0.3 * np.sin(t * 30) + 0.1 * np.cos(t * 7)).tolist(),\n",
"}\n",
"\n",
"params_to_plot = ['flow_threshold', 'pamm_alpha_bar', 'surge_fee_rate', 'oracle_velocity']\n",
"\n",
"fig, axes = plt.subplots(4, 4, figsize=(16, 12), sharex='col')\n",
"\n",
"for j, (traj_name, prices) in enumerate(trajectories.items()):\n",
" result = simulate_signal_routing(base_params, config, prices)\n",
" for i, param in enumerate(params_to_plot):\n",
" ax = axes[i, j]\n",
" ax.plot(result['times'], result[param], linewidth=1.2)\n",
" if i == 0:\n",
" ax.set_title(traj_name, fontsize=11, fontweight='bold')\n",
" if j == 0:\n",
" ax.set_ylabel(param.replace('_', ' '), fontsize=9)\n",
" if i == 3:\n",
" ax.set_xlabel('Time')\n",
"\n",
"fig.suptitle('Signal Router: Parameter Response to Price Trajectories', fontsize=13)\n",
"fig.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. DCA vs Lump Sum\n",
"\n",
"Compare fixed-size DCA and TWAP-aware DCA against lump-sum deposit.\n",
"Shows chunk prices, cumulative tokens, and the curve path."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from src.composed.simulator import scenario_dca_comparison\n",
"\n",
"results = scenario_dca_comparison(\n",
" total_amount=10_000.0,\n",
" n_chunks=20,\n",
" interval=1.0,\n",
")\n",
"\n",
"fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n",
"\n",
"colors = {'fixed': '#2196F3', 'twap_aware': '#FF9800'}\n",
"\n",
"for name, dca_result in results.items():\n",
" history = dca_result.order.history\n",
" chunks_x = list(range(len(history)))\n",
" prices = [h['price'] for h in history]\n",
" tokens = [h['tokens'] for h in history]\n",
" cumulative = np.cumsum(tokens)\n",
"\n",
" axes[0].plot(chunks_x, prices, '-o', color=colors[name],\n",
" label=name, markersize=4)\n",
" axes[1].bar(\n",
" [x + (0.2 if name == 'twap_aware' else -0.2) for x in chunks_x],\n",
" tokens, width=0.35, color=colors[name], label=name, alpha=0.7,\n",
" )\n",
" axes[2].plot(chunks_x, cumulative, '-s', color=colors[name],\n",
" label=name, markersize=4)\n",
"\n",
"# Add lump sum reference line\n",
"for name, dca_result in results.items():\n",
" axes[2].axhline(dca_result.lump_sum_tokens, color='gray',\n",
" linestyle='--', alpha=0.5,\n",
" label='lump sum' if name == 'fixed' else None)\n",
"\n",
"axes[0].set_title('Price per Chunk')\n",
"axes[0].set_xlabel('Chunk #')\n",
"axes[0].set_ylabel('Effective Price')\n",
"axes[0].legend()\n",
"\n",
"axes[1].set_title('Tokens per Chunk')\n",
"axes[1].set_xlabel('Chunk #')\n",
"axes[1].set_ylabel('Tokens')\n",
"axes[1].legend()\n",
"\n",
"axes[2].set_title('Cumulative Tokens')\n",
"axes[2].set_xlabel('Chunk #')\n",
"axes[2].set_ylabel('Total Tokens')\n",
"axes[2].legend()\n",
"\n",
"fig.suptitle('DCA vs Lump Sum Comparison', fontsize=13)\n",
"fig.tight_layout()\n",
"plt.show()\n",
"\n",
"# Summary table\n",
"for name, r in results.items():\n",
" print(f\"{name:12s} tokens={r.order.total_tokens_received:.2f} \"\n",
" f\"avg_price={r.order.avg_price:.6f} \"\n",
" f\"dca_adv={r.dca_advantage:+.4f}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. Subscription DCA + Loyalty\n",
"\n",
"Simulates subscription DCA over 1 year for two tiers.\n",
"Shows curve-minted tokens versus loyalty bonus accumulation."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from src.composed.subscription_dca import (\n",
" SubscriptionDCAConfig, simulate_subscription_dca,\n",
")\n",
"from src.composed.myco_surface import MycoSystemConfig\n",
"from src.commitments.subscription import SubscriptionTier\n",
"\n",
"tiers = {\n",
" 'basic': SubscriptionTier(\n",
" name='basic', payment_per_period=100.0, period_length=30.0,\n",
" loyalty_multiplier_max=1.5, loyalty_halflife=90.0,\n",
" ),\n",
" 'premium': SubscriptionTier(\n",
" name='premium', payment_per_period=500.0, period_length=30.0,\n",
" loyalty_multiplier_max=2.0, loyalty_halflife=60.0,\n",
" ),\n",
"}\n",
"\n",
"config = SubscriptionDCAConfig(n_chunks=5, spread_fraction=0.8)\n",
"sys_config = MycoSystemConfig(n_reserve_assets=3)\n",
"\n",
"subscribers = [\n",
" ('alice', 'basic', 0.0),\n",
" ('bob', 'premium', 0.0),\n",
" ('carol', 'basic', 30.0),\n",
"]\n",
"\n",
"sim = simulate_subscription_dca(\n",
" tiers=tiers, config=config, system_config=sys_config,\n",
" subscribers=subscribers, duration=365.0, dt=1.0,\n",
")\n",
"\n",
"fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n",
"\n",
"axes[0].plot(sim['times'], sim['total_curve_tokens'], label='Curve Tokens')\n",
"axes[0].plot(sim['times'], sim['total_loyalty_bonus'], label='Loyalty Bonus', linestyle='--')\n",
"axes[0].set_title('Token Accumulation')\n",
"axes[0].set_xlabel('Days')\n",
"axes[0].set_ylabel('Tokens')\n",
"axes[0].legend()\n",
"\n",
"total_tokens = sim['total_curve_tokens'] + sim['total_loyalty_bonus']\n",
"axes[1].fill_between(sim['times'], 0, sim['total_curve_tokens'],\n",
" alpha=0.5, label='Curve')\n",
"axes[1].fill_between(sim['times'], sim['total_curve_tokens'], total_tokens,\n",
" alpha=0.5, label='Loyalty Bonus')\n",
"axes[1].set_title('Stacked Token Sources')\n",
"axes[1].set_xlabel('Days')\n",
"axes[1].set_ylabel('Tokens')\n",
"axes[1].legend()\n",
"\n",
"axes[2].plot(sim['times'], sim['total_supply'])\n",
"axes[2].set_title('Total $MYCO Supply')\n",
"axes[2].set_xlabel('Days')\n",
"axes[2].set_ylabel('Supply')\n",
"\n",
"fig.suptitle('Subscription DCA + Loyalty Over 1 Year', fontsize=13)\n",
"fig.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. CRDT Schedule Lifecycle\n",
"\n",
"Demonstrates the DCA schedule lifecycle:\n",
"`create_dca_schedule` → `materialize_chunks` → `reconcile_settled_chunks` → `merge_dca_schedules`\n",
"\n",
"Shows status bar charts across two replicas."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from src.crdt.dca_schedule import (\n",
" create_dca_schedule, DCAScheduleRegistry,\n",
" materialize_chunks, merge_dca_schedules,\n",
")\n",
"from src.crdt.intent_matching import IntentSet\n",
"\n",
"# Create a schedule on replica A\n",
"schedule = create_dca_schedule(\n",
" schedule_id='s1', maker='alice',\n",
" total_amount=1000.0, n_chunks=10,\n",
" start_time=0.0, interval=1.0,\n",
")\n",
"reg_a = DCAScheduleRegistry(schedules={'s1': schedule})\n",
"reg_b = DCAScheduleRegistry() # Empty replica B\n",
"\n",
"# Lifecycle stages\n",
"stages = []\n",
"\n",
"# Stage 1: Initial (A has schedule, B empty)\n",
"def count_statuses(reg, sid='s1'):\n",
" if sid not in reg.schedules:\n",
" return {'pending': 0, 'submitted': 0, 'executed': 0}\n",
" chunks = reg.schedules[sid].chunks.values()\n",
" return {\n",
" 'pending': sum(1 for c in chunks if c.status == 'pending'),\n",
" 'submitted': sum(1 for c in chunks if c.status == 'submitted'),\n",
" 'executed': sum(1 for c in chunks if c.status == 'executed'),\n",
" }\n",
"\n",
"stages.append(('A: Created', count_statuses(reg_a), 'B: Empty', count_statuses(reg_b)))\n",
"\n",
"# Stage 2: Materialize first 5 chunks on A\n",
"iset = IntentSet()\n",
"reg_a, iset, materialized = materialize_chunks(reg_a, iset, current_time=5.0)\n",
"stages.append(('A: Materialized', count_statuses(reg_a), 'B: Empty', count_statuses(reg_b)))\n",
"\n",
"# Stage 3: Merge A → B\n",
"reg_b = merge_dca_schedules(reg_b, reg_a)\n",
"stages.append(('A: Post-merge', count_statuses(reg_a), 'B: Post-merge', count_statuses(reg_b)))\n",
"\n",
"# Stage 4: Materialize remaining on B\n",
"iset_b = IntentSet()\n",
"reg_b, iset_b, _ = materialize_chunks(reg_b, iset_b, current_time=10.0)\n",
"stages.append(('A: Stale', count_statuses(reg_a), 'B: All materialized', count_statuses(reg_b)))\n",
"\n",
"# Stage 5: Final merge\n",
"merged = merge_dca_schedules(reg_a, reg_b)\n",
"stages.append(('A: Final', count_statuses(merged), 'B: Final', count_statuses(merged)))\n",
"\n",
"# Plot\n",
"fig, axes = plt.subplots(2, len(stages), figsize=(16, 6), sharey='row')\n",
"status_colors = {'pending': '#90CAF9', 'submitted': '#FFA726', 'executed': '#66BB6A'}\n",
"\n",
"for col, (a_label, a_counts, b_label, b_counts) in enumerate(stages):\n",
" for row, (label, counts) in enumerate([(a_label, a_counts), (b_label, b_counts)]):\n",
" ax = axes[row, col]\n",
" statuses = list(counts.keys())\n",
" values = list(counts.values())\n",
" bars = ax.bar(statuses, values,\n",
" color=[status_colors[s] for s in statuses])\n",
" ax.set_title(label, fontsize=9)\n",
" ax.set_ylim(0, 11)\n",
" for bar, v in zip(bars, values):\n",
" if v > 0:\n",
" ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.2,\n",
" str(v), ha='center', fontsize=8)\n",
"\n",
"axes[0, 0].set_ylabel('Replica A')\n",
"axes[1, 0].set_ylabel('Replica B')\n",
"fig.suptitle('CRDT DCA Schedule Lifecycle: Create → Materialize → Merge', fontsize=13)\n",
"fig.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. CRDT Network Simulation\n",
"\n",
"Multi-peer gossip network with partitions. Tracks divergence over time\n",
"and merge events as they propagate state across peers."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from src.crdt.bridge import Network, NetworkConfig, EventType\n",
"from src.crdt.labor_crdt import AttestationEntry, submit_attestation\n",
"from src.crdt.intent_matching import Intent, add_intent\n",
"\n",
"# Create a 5-peer network with partitions\n",
"config = NetworkConfig(\n",
" partition_probability=0.15,\n",
" reconnect_delay=5.0,\n",
" seed=42,\n",
")\n",
"net = Network.create(['p1', 'p2', 'p3', 'p4', 'p5'], config)\n",
"\n",
"# Inject mutations at different peers\n",
"def add_labor(contributor, entry_id):\n",
" entry = AttestationEntry(\n",
" entry_id=entry_id, contribution_type='code',\n",
" units=5.0, timestamp=1.0, attester='admin',\n",
" )\n",
" def mutate(state):\n",
" state.labor = submit_attestation(state.labor, contributor, entry)\n",
" return state\n",
" return mutate\n",
"\n",
"def add_an_intent(intent_id):\n",
" intent = Intent(\n",
" intent_id=intent_id, maker='alice',\n",
" sell_token='USDC', sell_amount=100.0,\n",
" buy_token='MYCO', min_buy_amount=80.0,\n",
" valid_until=999.0,\n",
" )\n",
" def mutate(state):\n",
" state.intents = add_intent(state.intents, intent)\n",
" return state\n",
" return mutate\n",
"\n",
"# Spread mutations across peers\n",
"net.mutate_peer('p1', add_labor('alice', 'e1'))\n",
"net.mutate_peer('p2', add_labor('bob', 'e2'))\n",
"net.mutate_peer('p3', add_an_intent('i1'))\n",
"net.mutate_peer('p4', add_an_intent('i2'))\n",
"net.mutate_peer('p5', add_labor('carol', 'e3'))\n",
"\n",
"# Simulate\n",
"sim = net.simulate(steps=40)\n",
"\n",
"# Plot\n",
"fig, axes = plt.subplots(2, 1, figsize=(14, 8), sharex=True)\n",
"\n",
"# Divergence timeline\n",
"ax1 = axes[0]\n",
"ax1.plot(sim['times'], sim['divergence'], 'b-', linewidth=2, label='Distinct signatures')\n",
"ax1.axhline(1, color='green', linestyle='--', alpha=0.5, label='Converged')\n",
"\n",
"# Shade partition periods\n",
"for i, active in enumerate(sim['partition_active']):\n",
" if active:\n",
" ax1.axvspan(sim['times'][i] - 0.5, sim['times'][i] + 0.5,\n",
" alpha=0.2, color='red')\n",
"\n",
"ax1.set_ylabel('Divergence (# unique states)')\n",
"ax1.set_title('CRDT Network: Peer Divergence Over Time')\n",
"ax1.legend()\n",
"ax1.set_ylim(0, max(sim['divergence']) + 1)\n",
"\n",
"# Merge event scatter\n",
"ax2 = axes[1]\n",
"merge_events = [e for e in sim['events'] if e.event_type == EventType.MERGE]\n",
"partition_events = [e for e in sim['events'] if e.event_type == EventType.PARTITION]\n",
"reconnect_events = [e for e in sim['events'] if e.event_type == EventType.RECONNECT]\n",
"\n",
"peer_ids = list(net.peers.keys())\n",
"peer_y = {pid: i for i, pid in enumerate(peer_ids)}\n",
"\n",
"if merge_events:\n",
" ax2.scatter(\n",
" [e.time for e in merge_events],\n",
" [peer_y[e.source_peer] for e in merge_events],\n",
" c='blue', marker='o', s=20, alpha=0.6, label='Merge',\n",
" )\n",
"if partition_events:\n",
" ax2.scatter(\n",
" [e.time for e in partition_events],\n",
" [peer_y[e.source_peer] for e in partition_events],\n",
" c='red', marker='x', s=60, label='Partition',\n",
" )\n",
"if reconnect_events:\n",
" ax2.scatter(\n",
" [e.time for e in reconnect_events],\n",
" [peer_y[e.source_peer] for e in reconnect_events],\n",
" c='green', marker='^', s=60, label='Reconnect',\n",
" )\n",
"\n",
"ax2.set_yticks(range(len(peer_ids)))\n",
"ax2.set_yticklabels(peer_ids)\n",
"ax2.set_xlabel('Time')\n",
"ax2.set_ylabel('Peer')\n",
"ax2.set_title('Merge & Partition Events')\n",
"ax2.legend()\n",
"\n",
"fig.tight_layout()\n",
"plt.show()\n",
"\n",
"ct = net.convergence_time()\n",
"print(f\"Convergence time: {ct}\")\n",
"print(f\"Final divergence: {sim['divergence'][-1]}\")\n",
"print(f\"Total merges: {len(merge_events)}\")\n",
"print(f\"Total partitions: {len(partition_events)}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}