myco-bonding-curve/src/cli.py

320 lines
11 KiB
Python

"""MYCO bonding surface CLI — run simulations from the terminal.
Thin argparse wrapper calling existing simulation functions. Stdlib only.
Usage:
myco simulate token-launch --n-assets 3 --duration 90
myco compare-dca --total 10000 --chunks 20
myco signal-routing --trajectory volatile --steps 100
myco stress-test --fractions 0.02 0.05 0.10 0.20
"""
import argparse
import json
import sys
import math
import numpy as np
def _price_trajectory(name: str, steps: int) -> list[float]:
"""Generate a named price trajectory."""
t = np.linspace(0, 1, steps)
if name == "stable":
return [1.0] * steps
elif name == "bull":
return (1.0 + t * 0.5).tolist()
elif name == "crash":
return (1.0 - 0.4 * t + 0.1 * np.sin(t * 20)).tolist()
elif name == "volatile":
return (1.0 + 0.3 * np.sin(t * 30) + 0.1 * np.cos(t * 7)).tolist()
else:
raise ValueError(f"Unknown trajectory: {name}")
def _save_chart(fig, path: str) -> None:
"""Save a matplotlib figure to disk."""
fig.savefig(path, dpi=150, bbox_inches="tight")
print(f"Chart saved to {path}")
def cmd_simulate(args: argparse.Namespace) -> int:
"""Run a simulation scenario."""
from src.composed.simulator import (
scenario_token_launch,
scenario_bank_run,
scenario_mixed_issuance,
)
scenario_map = {
"token-launch": lambda: scenario_token_launch(
n_assets=args.n_assets, duration=args.duration,
),
"bank-run": lambda: scenario_bank_run(
n_assets=args.n_assets, duration=args.duration,
),
"mixed-issuance": lambda: scenario_mixed_issuance(
n_assets=args.n_assets, duration=args.duration,
),
}
if args.scenario not in scenario_map:
print(f"Unknown scenario: {args.scenario}", file=sys.stderr)
print(f"Available: {', '.join(scenario_map.keys())}", file=sys.stderr)
return 1
result = scenario_map[args.scenario]()
if args.output == "json":
data = {
k: v.tolist() for k, v in vars(result).items()
}
print(json.dumps(data, indent=2))
else:
metrics = result
print(f"Simulation: {args.scenario}")
print(f" Duration: {args.duration}")
print(f" Final supply: {metrics.supply[-1]:.2f}")
print(f" Final reserve: {metrics.reserve_value[-1]:.2f}")
print(f" Backing ratio: {metrics.backing_ratio[-1]:.4f}")
print(f" Total minted: {metrics.financial_minted[-1] + metrics.commitment_minted[-1]:.2f}")
print(f" Total redeemed: {metrics.total_redeemed[-1]:.2f}")
if args.save_chart:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from src.utils.plotting import plot_time_series
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
plot_time_series(result.times, {"Supply": result.supply}, ax=axes[0, 0])
plot_time_series(result.times, {"Reserve": result.reserve_value}, ax=axes[0, 1])
plot_time_series(result.times, {"Backing Ratio": result.backing_ratio}, ax=axes[1, 0])
plot_time_series(
result.times,
{"Financial": result.financial_minted, "Commitment": result.commitment_minted},
ax=axes[1, 1],
)
fig.suptitle(f"MYCO Simulation: {args.scenario}")
fig.tight_layout()
_save_chart(fig, args.save_chart)
plt.close(fig)
return 0
def cmd_compare_dca(args: argparse.Namespace) -> int:
"""Compare DCA strategies."""
from src.composed.simulator import scenario_dca_comparison
results = scenario_dca_comparison(
total_amount=args.total,
n_chunks=args.chunks,
interval=args.interval,
)
if args.output == "json":
data = {}
for name, dca_result in results.items():
data[name] = {
"total_tokens": dca_result.order.total_tokens_received,
"total_spent": dca_result.order.total_spent,
"avg_price": dca_result.order.avg_price,
"twap_price": dca_result.twap_price,
"lump_sum_tokens": dca_result.lump_sum_tokens,
"dca_advantage": dca_result.dca_advantage,
}
print(json.dumps(data, indent=2))
else:
for name, dca_result in results.items():
print(f"\nStrategy: {name}")
print(f" Tokens received: {dca_result.order.total_tokens_received:.4f}")
print(f" Avg price: {dca_result.order.avg_price:.6f}")
print(f" TWAP price: {dca_result.twap_price:.6f}")
print(f" Lump sum tokens: {dca_result.lump_sum_tokens:.4f}")
print(f" DCA advantage: {dca_result.dca_advantage:+.4f}")
if args.save_chart:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
for name, dca_result in results.items():
history = dca_result.order.history
chunks_x = list(range(len(history)))
tokens = [h["tokens"] for h in history]
prices = [h["price"] for h in history]
axes[0].bar(
[x + (0.2 if name == "twap_aware" else -0.2) for x in chunks_x],
tokens, width=0.35, label=name, alpha=0.7,
)
axes[1].plot(chunks_x, prices, "-o", label=name, markersize=4)
axes[0].set_title("Tokens per Chunk")
axes[0].set_xlabel("Chunk")
axes[0].legend()
axes[1].set_title("Price per Chunk")
axes[1].set_xlabel("Chunk")
axes[1].legend()
fig.suptitle("DCA Strategy Comparison")
fig.tight_layout()
_save_chart(fig, args.save_chart)
plt.close(fig)
return 0
def cmd_signal_routing(args: argparse.Namespace) -> int:
"""Run signal routing simulation."""
from src.primitives.signal_router import (
AdaptiveParams,
SignalRouterConfig,
simulate_signal_routing,
)
base = AdaptiveParams(
flow_threshold=0.1,
pamm_alpha_bar=10.0,
surge_fee_rate=0.05,
oracle_multiplier_velocity=0.0,
)
config = SignalRouterConfig()
prices = _price_trajectory(args.trajectory, args.steps)
result = simulate_signal_routing(base, config, prices)
if args.output == "json":
print(json.dumps(result, indent=2))
else:
print(f"Signal Routing: {args.trajectory} ({args.steps} steps)")
print(f" Final flow_threshold: {result['flow_threshold'][-1]:.6f}")
print(f" Final pamm_alpha_bar: {result['pamm_alpha_bar'][-1]:.4f}")
print(f" Final surge_fee_rate: {result['surge_fee_rate'][-1]:.6f}")
print(f" Final oracle_velocity: {result['oracle_velocity'][-1]:.6f}")
print(f" Final volatility: {result['volatility'][-1]:.6f}")
if args.save_chart:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, axes = plt.subplots(3, 1, figsize=(12, 10), sharex=True)
t = result["times"]
axes[0].plot(t, prices, label="Spot Price")
axes[0].set_ylabel("Price")
axes[0].set_title(f"Signal Router: {args.trajectory}")
axes[0].legend()
axes[1].plot(t, result["twap_deviation"], label="TWAP Deviation")
axes[1].plot(t, result["volatility"], label="Volatility")
axes[1].set_ylabel("Signal Value")
axes[1].legend()
axes[2].plot(t, result["flow_threshold"], label="Flow Threshold")
axes[2].plot(t, result["surge_fee_rate"], label="Surge Fee")
axes[2].set_ylabel("Parameter Value")
axes[2].set_xlabel("Time")
axes[2].legend()
fig.tight_layout()
_save_chart(fig, args.save_chart)
plt.close(fig)
return 0
def cmd_stress_test(args: argparse.Namespace) -> int:
"""Run bank run stress tests at multiple redemption fractions."""
from src.composed.simulator import scenario_bank_run
results = {}
for frac in args.fractions:
result = scenario_bank_run(redemption_fraction=frac)
results[frac] = result
final_ratio = result.backing_ratio[-1]
final_reserve = result.reserve_value[-1]
survived = "YES" if final_ratio > 0.5 else "NO"
print(f" Fraction {frac:.2f}: backing={final_ratio:.4f} reserve={final_reserve:.0f} survived={survived}")
if args.save_chart:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 6))
for frac, result in results.items():
ax.plot(result.times, result.reserve_value, label=f"{frac:.0%} redemption")
ax.set_xlabel("Time")
ax.set_ylabel("Reserve Value")
ax.set_title("Bank Run Stress Test — Reserve Curves")
ax.legend()
fig.tight_layout()
_save_chart(fig, args.save_chart)
plt.close(fig)
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="myco",
description="MYCO bonding surface simulation toolkit",
)
sub = parser.add_subparsers(dest="command")
# simulate
p_sim = sub.add_parser("simulate", help="Run a simulation scenario")
p_sim.add_argument("scenario", choices=["token-launch", "bank-run", "mixed-issuance"])
p_sim.add_argument("--n-assets", type=int, default=3)
p_sim.add_argument("--duration", type=float, default=90.0)
p_sim.add_argument("--output", choices=["text", "json"], default="text")
p_sim.add_argument("--save-chart", type=str, default=None)
# compare-dca
p_dca = sub.add_parser("compare-dca", help="Compare DCA strategies")
p_dca.add_argument("--total", type=float, default=10_000.0)
p_dca.add_argument("--chunks", type=int, default=20)
p_dca.add_argument("--interval", type=float, default=1.0)
p_dca.add_argument("--output", choices=["text", "json"], default="text")
p_dca.add_argument("--save-chart", type=str, default=None)
# signal-routing
p_sig = sub.add_parser("signal-routing", help="Signal routing simulation")
p_sig.add_argument("--trajectory", choices=["stable", "bull", "crash", "volatile"], default="stable")
p_sig.add_argument("--steps", type=int, default=100)
p_sig.add_argument("--output", choices=["text", "json"], default="text")
p_sig.add_argument("--save-chart", type=str, default=None)
# stress-test
p_stress = sub.add_parser("stress-test", help="Bank run stress tests")
p_stress.add_argument(
"--fractions", type=float, nargs="+",
default=[0.02, 0.05, 0.10, 0.20],
)
p_stress.add_argument("--save-chart", type=str, default=None)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
if args.command is None:
parser.print_help()
return 0
handlers = {
"simulate": cmd_simulate,
"compare-dca": cmd_compare_dca,
"signal-routing": cmd_signal_routing,
"stress-test": cmd_stress_test,
}
return handlers[args.command](args)
if __name__ == "__main__":
sys.exit(main())