analyze_eth_nextgen_micro_leverage_units.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. from __future__ import annotations
  2. import json
  3. from pathlib import Path
  4. import pandas as pd
  5. REPORT_DIR = Path("reports/eth-exploration")
  6. INITIAL_EQUITY = 10_000.0
  7. def max_drawdown(values: list[float]) -> float:
  8. peak = values[0]
  9. drawdown = 0.0
  10. for value in values:
  11. peak = max(peak, value)
  12. drawdown = max(drawdown, (peak - value) / peak if peak else 0.0)
  13. return drawdown
  14. def metrics(equity: pd.Series) -> dict[str, float]:
  15. years = (equity.index[-1] - equity.index[0]).total_seconds() / 86_400 / 365
  16. total = float(equity.iloc[-1] / equity.iloc[0] - 1.0)
  17. annualized = (1.0 + total) ** (1.0 / years) - 1.0
  18. drawdown = max_drawdown([float(value) for value in equity])
  19. returns = equity.pct_change().dropna()
  20. std = float(returns.std(ddof=1)) if len(returns) > 1 else 0.0
  21. return {
  22. "total_return": total,
  23. "annualized_return": annualized,
  24. "max_drawdown": drawdown,
  25. "calmar": annualized / drawdown if drawdown else 0.0,
  26. "risk_reward_ratio": float(returns.mean()) / std * (365**0.5) if std else 0.0,
  27. }
  28. def trade_stats(frame: pd.DataFrame) -> dict[str, float]:
  29. values = frame["net_return"].astype(float)
  30. wins = values[values > 0.0]
  31. losses = values[values < 0.0]
  32. gross_profit = float(wins.sum())
  33. gross_loss = abs(float(losses.sum()))
  34. avg_win = gross_profit / len(wins) if len(wins) else 0.0
  35. avg_loss = gross_loss / len(losses) if len(losses) else 0.0
  36. return {
  37. "trades": float(len(frame)),
  38. "win_rate": float((values > 0.0).mean()) if len(values) else 0.0,
  39. "payoff_ratio": avg_win / avg_loss if avg_loss else 0.0,
  40. "profit_factor": gross_profit / gross_loss if gross_loss else 0.0,
  41. }
  42. def equity_from_grouped_trades(trades: pd.DataFrame) -> pd.Series:
  43. daily_return = trades.groupby("exit_date")["net_return"].sum().sort_index()
  44. index = pd.date_range(daily_return.index.min(), daily_return.index.max(), freq="D", tz="UTC")
  45. returns = daily_return.reindex(index.strftime("%Y-%m-%d")).fillna(0.0)
  46. returns.index = index
  47. equity = INITIAL_EQUITY * (1.0 + returns).cumprod()
  48. equity.iloc[0] = INITIAL_EQUITY * (1.0 + returns.iloc[0])
  49. return equity
  50. def markdown_table(frame: pd.DataFrame) -> str:
  51. rows = [list(frame.columns), ["---" for _ in frame.columns]]
  52. rows.extend(frame.astype(object).values.tolist())
  53. return "\n".join("| " + " | ".join(format_cell(value) for value in row) + " |" for row in rows)
  54. def format_cell(value: object) -> str:
  55. if isinstance(value, float):
  56. return f"{value:.6g}"
  57. return str(value).replace("|", "\\|")
  58. def main() -> int:
  59. raw = pd.read_csv(REPORT_DIR / "eth-nextgen-micro-selected-trades.csv")
  60. raw["entry_ts"] = pd.to_datetime(raw["entry_time"], utc=True)
  61. raw["exit_ts"] = pd.to_datetime(raw["exit_time"], utc=True)
  62. group_columns = ["source", "side", "entry_time", "exit_time", "entry_price", "exit_price", "exit_date"]
  63. grouped = (
  64. raw.groupby(group_columns, as_index=False)
  65. .agg(
  66. leg_count=("leg", "count"),
  67. legs=("leg", lambda values: ";".join(sorted(values))),
  68. net_return=("net_return", "sum"),
  69. )
  70. .sort_values(["entry_time", "exit_time", "source", "side"])
  71. )
  72. grouped["position_unit"] = grouped["leg_count"].astype(float)
  73. grouped.loc[grouped["source"] == "nextgen", "position_unit"] *= 0.5
  74. grouped.loc[grouped["source"] == "micro", "position_unit"] = 1.0
  75. grouped_path = REPORT_DIR / "eth-nextgen-micro-leverage-units-trades.csv"
  76. summary_path = REPORT_DIR / "eth-nextgen-micro-leverage-units-summary.json"
  77. report_path = REPORT_DIR / "eth-nextgen-micro-leverage-units.md"
  78. grouped.to_csv(grouped_path, index=False)
  79. equity = equity_from_grouped_trades(grouped)
  80. published = pd.read_csv(REPORT_DIR / "eth-nextgen-micro-portfolio-equity.csv")
  81. published = published[(published["name"] == "switch-l30-r96_q0.15_mf0.25_us") & (published["cost_model"] == "maker_taker")].copy()
  82. published["date"] = pd.to_datetime(published["date"], utc=True)
  83. published_equity = published.set_index("date")["equity"].sort_index()
  84. aligned = pd.concat([equity.rename("leverage_unit"), published_equity.rename("published")], axis=1).dropna()
  85. summary = {
  86. "raw_virtual_trade_events": int(len(raw)),
  87. "netted_trades": int(len(grouped)),
  88. "position_unit_counts": {str(key): int(value) for key, value in grouped["position_unit"].value_counts().sort_index().items()},
  89. "leg_count_counts": {str(key): int(value) for key, value in grouped["leg_count"].value_counts().sort_index().items()},
  90. "source_counts": {str(key): int(value) for key, value in grouped["source"].value_counts().items()},
  91. "side_counts": {str(key): int(value) for key, value in grouped["side"].value_counts().items()},
  92. "grouped_trade_stats": trade_stats(grouped),
  93. "grouped_equity_metrics": metrics(equity),
  94. "published_equity_metrics": metrics(published_equity),
  95. "max_abs_equity_diff_vs_published": float((aligned["leverage_unit"] - aligned["published"]).abs().max()),
  96. "max_abs_return_diff_vs_published": float((aligned["leverage_unit"].pct_change() - aligned["published"].pct_change()).abs().max()),
  97. }
  98. summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True), encoding="utf-8")
  99. top = grouped[grouped["leg_count"] > 1].head(12)[
  100. ["source", "side", "entry_time", "exit_time", "leg_count", "position_unit", "entry_price", "exit_price", "net_return"]
  101. ]
  102. lines = [
  103. "# ETH nextgen micro leverage-unit interpretation",
  104. "",
  105. "This report treats simultaneous same-price same-direction virtual legs as one netted trade with a larger position unit.",
  106. "",
  107. "## Summary",
  108. "",
  109. f"- Raw virtual trade events: `{summary['raw_virtual_trade_events']}`",
  110. f"- Netted trades: `{summary['netted_trades']}`",
  111. f"- Position unit counts: `{json.dumps(summary['position_unit_counts'], sort_keys=True)}`",
  112. f"- Source counts: `{json.dumps(summary['source_counts'], sort_keys=True)}`",
  113. f"- Side counts: `{json.dumps(summary['side_counts'], sort_keys=True)}`",
  114. f"- Grouped total return: `{summary['grouped_equity_metrics']['total_return']:.6g}`",
  115. f"- Published total return: `{summary['published_equity_metrics']['total_return']:.6g}`",
  116. f"- Max absolute equity difference vs published: `{summary['max_abs_equity_diff_vs_published']:.6g}`",
  117. "",
  118. "## Repeated-leg examples",
  119. "",
  120. markdown_table(top),
  121. "",
  122. "## Interpretation",
  123. "",
  124. "A nextgen trade with `leg_count=2` is not two unrelated discretionary orders. It is one ETH direction with `position_unit=1.0`, because each nextgen leg contributes 0.5 unit. A nextgen trade with `leg_count=1` is `position_unit=0.5`. Micro trades are `position_unit=1.0`.",
  125. "",
  126. ]
  127. report_path.write_text("\n".join(lines), encoding="utf-8")
  128. print(report_path)
  129. print(json.dumps(summary, indent=2, sort_keys=True))
  130. return 0
  131. if __name__ == "__main__":
  132. raise SystemExit(main())