Compare commits
60 Commits
e6dd16908c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 879372f69e | |||
| 9cf871be46 | |||
| 6934bfdaa0 | |||
| 39545ac94b | |||
| f5ffe4baee | |||
| 8461ed5097 | |||
| 2429394cd8 | |||
| a6905f2691 | |||
| 37b8496dc2 | |||
| 74113ab624 | |||
| f9bed72b1c | |||
| a5de7d526f | |||
| 08a95e8fe2 | |||
| 50f8f4f970 | |||
| 392bde44a0 | |||
| 6fcf5e7c7d | |||
| cbbd0ef941 | |||
| ff3e68855c | |||
| 3cc68cd46a | |||
| b0eaee47db | |||
| cf376f2995 | |||
| a8ed3cafe0 | |||
| 298b9c8020 | |||
| e5a81132ef | |||
| 2176910fab | |||
| c98681c130 | |||
| 6a39125fee | |||
| 6552511978 | |||
| 98ee58dfaa | |||
| 8cb59239c6 | |||
| 232d2dae10 | |||
| 79870925f7 | |||
| 0e08543823 | |||
| 03ebe9e795 | |||
| 0b8943c926 | |||
| 162c535c7c | |||
| 941c07fe32 | |||
| bf137a08a3 | |||
| d31d301822 | |||
| f198d2ccf6 | |||
| 9b1d46526b | |||
| e9629b698b | |||
| bfc3214967 | |||
| fb231eef7c | |||
| 2f74e076b4 | |||
| f7f47b5484 | |||
| 803a38b237 | |||
| 70d43fefe0 | |||
| 84efb4014a | |||
| 5004b23331 | |||
| f4c8bca15a | |||
| 5c41d232c1 | |||
| 7fd289f562 | |||
| 8855c013a6 | |||
| 156ea40e78 | |||
| 6665d0cd2a | |||
| 96ca132fa2 | |||
| a09954017e | |||
| ac1b33a014 | |||
| eb2dc32c53 |
@@ -1,14 +1,31 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
dist/
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Next.js / Dashboard
|
||||
.next/
|
||||
out/
|
||||
node_modules/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
*.pem
|
||||
*_pk
|
||||
data/
|
||||
*.parquet
|
||||
.ipynb_checkpoints/
|
||||
*.env.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Runtime artifacts
|
||||
/tmp/
|
||||
*.log
|
||||
metrics.json
|
||||
paper_metrics.json
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -1,59 +1,168 @@
|
||||
# FTDT Quant Lab — Quantitative Trading Strategies
|
||||
# FTDT Quant Lab
|
||||
|
||||
A collection of quantitative trading strategies running on
|
||||
**Hyperliquid Testnet** via **Nautilus Trader**. Built as part of
|
||||
my professional portfolio to demonstrate algorithmic trading,
|
||||
market microstructure, and risk management skills.
|
||||
Production multi-strategy quant trading system running on Hyperliquid.
|
||||
Live testnet node, paper trading simulator, historical backtesting, and real-time dashboard.
|
||||
|
||||
## What's inside
|
||||
**Live:** https://ftdt.io/cv
|
||||
|
||||
Five strategies, from simple to advanced:
|
||||
---
|
||||
|
||||
| # | Strategy | Concept |
|
||||
|---|----------|---------|
|
||||
| 1 | Order Book Imbalance | Trades on L2 bid/ask pressure |
|
||||
| 2 | Iceberg / TWAP Detection | Follows whale accumulation patterns |
|
||||
| 3 | Funding Rate Arbitrage | Delta-neutral carry trade |
|
||||
| 4 | Pairs Trading (BTC/ETH) | Cointegration-based stat arb |
|
||||
| 5 | Avellaneda-Stoikov Market Making | Stochastic optimal control |
|
||||
## Stack
|
||||
|
||||
All strategies share a common risk manager and portfolio tracker.
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| **Runtime** | Python 3.13 (async trading) |
|
||||
| **API Client** | nautilus_trader (Hyperliquid SDK, Rust bindings) |
|
||||
| **Dashboard** | Next.js 16 (static export) + shadcn/ui + Framer Motion |
|
||||
| **Design System** | Hallmark Cobalt — Ubuntu font, hairline borders, cool paper palette |
|
||||
| **Reverse Proxy** | Caddy → auto HTTPS |
|
||||
| **WebSocket** | FastAPI (live/paper streaming) |
|
||||
| **Data** | PostgreSQL 17 (`ftdt_quant`), JSON metrics files |
|
||||
| **Backtesting** | Custom dollar-bar engine + numpy |
|
||||
| **Infra** | OVH VPS (4 vCPU, 8GB RAM, Debian 13), 2GB swap |
|
||||
|
||||
## Quick start
|
||||
~5,300 lines of Python + TypeScript. 67 commits since July 2026.
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
---
|
||||
|
||||
# Set your Hyperliquid testnet key
|
||||
export HYPERLIQUID_TESTNET_PK=0x...
|
||||
|
||||
# Run live (testnet only)
|
||||
python live/node.py
|
||||
```
|
||||
|
||||
## Project layout
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
ftdt-quant-lab/
|
||||
├── config/ # Per-strategy YAML configuration
|
||||
├── strategies/ # Strategy implementations
|
||||
├── common/ # Risk manager, portfolio tracker, metrics
|
||||
├── backtests/ # Historical backtest runners
|
||||
├── live/ # Live trading node (Hyperliquid Testnet)
|
||||
├── docs/ # Documentation and strategy writeups
|
||||
└── notebooks/ # Analysis notebooks
|
||||
├── live/
|
||||
│ ├── node.py # Live trading node — testnet, 9 strategies
|
||||
│ └── paper_trader.py # Paper trading — mainnet data, 10 strategies
|
||||
├── strategies/
|
||||
│ ├── orderbook_imbalance.py # L2 bid/ask volume skew (OBI)
|
||||
│ ├── iceberg_detection.py # Whale TWAP accumulation detection
|
||||
│ ├── funding_arb.py # Delta-neutral carry — spot/perp funding
|
||||
│ ├── pairs_trading.py # BTC/ETH ratio Z-score (1.5σ)
|
||||
│ ├── avellaneda_stoikov.py # Dual-sided stochastic control MM
|
||||
│ ├── kalman_pairs/ # Kalman-filter adaptive hedge ratio
|
||||
│ ├── hawkes_ofi.py # Hawkes process order flow
|
||||
│ ├── deep_lob.py # Deep LOB CNN feature extraction
|
||||
│ ├── queue_imbalance.py # Weighted queue dynamics
|
||||
│ ├── hurst_vpin.py # Hurst exponent + VPIN directional
|
||||
│ ├── hurst_vpin_live.py # Lightweight Hurst/VPIN for live tick stream
|
||||
│ └── quant_report.py # QF-Lib style quant analytics
|
||||
├── dashboard/
|
||||
│ ├── server.py # FastAPI backend — WS, REST, static files
|
||||
│ └── next/
|
||||
│ └── src/
|
||||
│ ├── app/ # Main page + layout
|
||||
│ ├── components/ # QuantReport, StrategyCard, L2Terminal
|
||||
│ └── lib/ # Types, API client
|
||||
├── backtests/
|
||||
│ ├── run.py # Backtest runner
|
||||
│ └── results/
|
||||
│ └── historical/ # JSON backtest snapshots (32 entries)
|
||||
├── common/ # Shared utilities
|
||||
│ ├── risk.py, risk_manager.py
|
||||
│ ├── hyperliquid_api.py
|
||||
│ └── portfolio.py, metrics.py
|
||||
├── config/
|
||||
│ └── fee_tiers.py # Perp/spot fee schedules
|
||||
└── infrastructure/
|
||||
├── Caddyfile # Reverse proxy config
|
||||
└── systemd/ # Service units (pending)
|
||||
```
|
||||
|
||||
## Strategy details
|
||||
---
|
||||
|
||||
See `docs/STRATEGIES.md` for a walkthrough of each strategy.
|
||||
## Strategies — Current State
|
||||
|
||||
## Risk warning
|
||||
### Live Node (Hyperliquid Testnet — 9 strategies, $100 each)
|
||||
|
||||
This is **testnet only**. These strategies are educational — they
|
||||
are not financial advice and have no alpha guarantee. Never run
|
||||
them on mainnet without thorough backtesting and your own due diligence.
|
||||
| # | Strategy | Type | Asset | Size | PnL | Trades | Win |
|
||||
|---|----------|------|-------|------|-----|--------|-----|
|
||||
| 1 | Order Book Imbalance | reversal | BTC | 0.000200 | $0.00 | 0 | — |
|
||||
| 2 | Iceberg Detection | momentum | BTC | 0.000210 | $0.00 | 2 | 0% |
|
||||
| 3 | Funding Rate Arb | carry | BTC | 0.000220 | $0.00 | 0 | — |
|
||||
| 4 | Pairs Trading | stat_arb | ETH | 0.006000 | **+$0.74** | 9 | 67% |
|
||||
| 5 | Avellaneda-Stoikov | market_making | BTC | 0.000230 | -$1.35 | 32 | 0% |
|
||||
| 6 | Momentum Breakout | momentum | ETH | 0.000500 | $0.00 | 0 | — |
|
||||
| 7 | Mean Reversion | reversal | ETH | 0.000500 | $0.00 | 0 | — |
|
||||
| 8 | Kalman Pairs | stat_arb | ETH | 0.005000 | $0.00 | 0 | — |
|
||||
| 9 | Hurst VPIN | momentum | BTC | 0.000240 | $0.00 | 0 | — |
|
||||
|
||||
**Execution:** GTC POST-ONLY limit orders. Signals every 5 ticks (5s), dual-sided for A-S.
|
||||
**Fee model:** Maker 0.02% (testnet).
|
||||
|
||||
### Paper Trader (Hyperliquid Mainnet data — 10 strategies, $100 each)
|
||||
|
||||
Same set + Queue Imbalance. Real mainnet orderbook + funding data. Fee model: taker 0.05% / maker 0.02%. Trades simulated with 1bps slippage.
|
||||
|
||||
---
|
||||
Built by [Ramses Echikh](https://git.ftdt.io/rams) · Part of my quant trading portfolio
|
||||
|
||||
## Historical Backtests
|
||||
|
||||
32 backtest snapshots across 8 strategies × 4 coins (BTC, ETH, HYPE, VVV).
|
||||
Hurst/VPIN BTC: **46 trades, 96% win rate, +1.10%** on synthetic trending data.
|
||||
|
||||
---
|
||||
|
||||
## Priority Analysis
|
||||
|
||||
### Strategies showing real signal
|
||||
|
||||
| Strategy | Signal | Status |
|
||||
|----------|--------|--------|
|
||||
| **Pairs Trading** | ✅ | +$0.74, 67% win rate — only profitable live strategy |
|
||||
| **Avellaneda-Stoikov** | ⚠️ | 32 trades but losing — spread capture not covering fees |
|
||||
| **Iceberg Detection** | ⚠️ | 2 trades — rare signals, needs threshold tuning |
|
||||
| **Hurst VPIN** | 🔬 | 96% win in backtest, 0 live trades — very selective |
|
||||
| **Mean Reversion** | ⏳ | 0 trades — VWAP deviation not crossing 1.0σ |
|
||||
| **Momentum** | ⏳ | 0 trades — Bollinger 1.2σ too tight for ETH |
|
||||
|
||||
### Recommendation: focus investment here
|
||||
|
||||
1. **Pairs Trading** — `#1 priority`. Only live winner. Extend to more pairs (SOL, ARB, OP). Add Kalman dynamic hedge ratio. This is the clearest path to sustained PnL.
|
||||
|
||||
2. **Hurst/VPIN** — `#2 priority`. Backtest shows strong edge (96% win). Needs real market data (not synthetic) and 3-day candle feed to trigger more signals. The selectivity IS the edge — don't dilute it.
|
||||
|
||||
3. **Avellaneda-Stoikov** — Needs inventory control. 32 trades losing because adverse selection. Add skew-aware quoting (update reserve price based on queue imbalance).
|
||||
|
||||
4. **Iceberg Detection** — Lower detection threshold. Currently requires 7/10 consecutive ticks same direction — too strict.
|
||||
|
||||
5. **Funding Rate Arb** — Real Hyperliquid funding data already plumbed. Test threshold from 3% → 1% APR. Prefunding detection (predict next rate before announcement).
|
||||
|
||||
6. **Backtest engine** — Replace synthetic data with real Hyperliquid candles. Add walk-forward optimization. The `hurst_vpin.py` infrastructure is ready.
|
||||
|
||||
### Skip for now
|
||||
|
||||
- OBI / Mean Reversion / Momentum — 0 trades. Signal thresholds need fundamental redesign, not just tuning.
|
||||
- Cartea-Jaimungal / Gueant MM — academic models, not adapted to crypto microstructure.
|
||||
- DeepLOB / Hawkes OFI — dependency-heavy, no live integration.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
```bash
|
||||
# Clone and deploy
|
||||
git clone https://git.ftdt.io/rams/ftdt-quant-lab.git
|
||||
cd ftdt-quant-lab
|
||||
python3 -m venv .venv && source .venv/bin/activate
|
||||
pip install -r requirements.txt # (pending — currently manual)
|
||||
|
||||
# Start services
|
||||
python live/node.py & # Trading node
|
||||
python live/paper_trader.py & # Paper simulator
|
||||
python dashboard/server.py --port 9175 # Dashboard backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Docker Compose for reproducible deployment
|
||||
- [ ] Walk-forward backtest on real Hyperliquid candle data
|
||||
- [ ] Extend Pairs Trading to BTC/SOL, BTC/ARB
|
||||
- [ ] Hurst/VPIN 3-day candle feed → real live signals
|
||||
- [ ] Memory leak proofing — current guard at 512MB RSS
|
||||
- [ ] systemd service unit files for auto-restart
|
||||
- [ ] Grafana + Prometheus monitoring dashboard
|
||||
|
||||
---
|
||||
|
||||
*Built with Hermes Agent · Hallmark Cobalt · Ubuntu fonts*
|
||||
|
||||
@@ -42,6 +42,7 @@ STRATEGIES = {
|
||||
"avellaneda":{"name": "Avellaneda-Stoikov", "size": 0.001, "fee_model": "maker"},
|
||||
"momentum": {"name": "Momentum Breakout", "size": 0.002, "fee_model": "taker"},
|
||||
"mean_rev": {"name": "Mean Reversion", "size": 0.002, "fee_model": "taker"},
|
||||
"kalman_pairs": {"name": "Kalman Pairs", "size": 0.005, "fee_model": "taker"},
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +82,7 @@ def fetch_candles(coin: str, interval: str = "1h", limit: int = 720) -> list[dic
|
||||
def simulate_strategy_on_candles(
|
||||
key: str,
|
||||
candles: list[dict],
|
||||
coin_name: str = "BTC",
|
||||
allocation: float = 100.0,
|
||||
fee_tier: int = 0,
|
||||
staking_tier: str = "none",
|
||||
@@ -143,9 +145,14 @@ def simulate_strategy_on_candles(
|
||||
reason = f"Iceberg: {up_count}/10 upward ticks"
|
||||
signal_strength = 1 - up_count / 10
|
||||
|
||||
elif key == "funding_arb":
|
||||
# Funding rate arb: need real funding data — skip for candle-only backtest
|
||||
pass
|
||||
elif key == "funding_arb" and len(prices_20) >= 20:
|
||||
# Funding Rate Arb: hourly price trend as funding proxy
|
||||
long_return = (close - prices_20[0]) / prices_20[0]
|
||||
annual_rate = long_return * 365 * 24 # hourly to annual
|
||||
if abs(annual_rate) > 0.03: # >3% annualized
|
||||
signal = "SELL" if annual_rate > 0 else "BUY"
|
||||
reason = f"Fund: {annual_rate*100:.1f}% APR ({long_return*100:.2f}% 1h)"
|
||||
signal_strength = min(1.0, abs(annual_rate) * 5)
|
||||
|
||||
elif key == "pairs" and len(prices_20) >= 20:
|
||||
# Pairs: BTC/ETH ratio Z-score (only works if we have both)
|
||||
@@ -205,6 +212,23 @@ def simulate_strategy_on_candles(
|
||||
signal = "BUY"
|
||||
reason = f"VWAP: dev={dev:.1f}σ below VWAP ${vwap:.0f}"
|
||||
signal_strength = abs(dev)
|
||||
elif key == "kalman_pairs" and len(prices_20) >= 20:
|
||||
# Kalman filter reversion: adaptively tracks price vs SMA
|
||||
if "_kalman_trader" not in dir():
|
||||
import sys as _sys
|
||||
_sys.path.insert(0, ".")
|
||||
from strategies.kalman_pairs import KalmanPairsTrader
|
||||
globals()["_kalman_trader"] = KalmanPairsTrader(
|
||||
transition_covariance=1e-3, observation_covariance=1e-1,
|
||||
z_entry=2.0, z_exit=0.5, warmup_bars=20,
|
||||
)
|
||||
# Use 20-period SMA as the "pair" asset X, price as Y
|
||||
sma_20 = sum(prices_20) / len(prices_20)
|
||||
result = globals()["_kalman_trader"].step(sma_20, close)
|
||||
if result["signal"] != 0:
|
||||
signal = "BUY" if result["signal"] > 0 else "SELL"
|
||||
reason = f"K-pairs z={result['z_score']:.2f} b={result['beta']:.3f}"
|
||||
signal_strength = abs(result["z_score"]) / 4.0
|
||||
|
||||
# ── Execute signal ──
|
||||
if signal and signal_strength > 0.15: # minimum strength filter
|
||||
@@ -289,7 +313,7 @@ def simulate_strategy_on_candles(
|
||||
return {
|
||||
"strategy": name,
|
||||
"strategy_key": key,
|
||||
"coin": candles[0]["t"] if candles else "unknown",
|
||||
"coin": coin_name, # actual ticker (BTC, ETH, etc.)
|
||||
"allocation": allocation,
|
||||
"start_time": curve[0]["t"] if curve else "",
|
||||
"end_time": curve[-1]["t"] if curve else "",
|
||||
@@ -319,7 +343,7 @@ def simulate_strategy_on_candles(
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="FTDT Historical Backtest Runner")
|
||||
p.add_argument("--coin", default="BTC", choices=["BTC", "ETH", "SOL"], help="Coin to backtest")
|
||||
p.add_argument("--coin", default="BTC", choices=["BTC", "ETH", "SOL", "HYPE", "VVV"], help="Coin to backtest")
|
||||
p.add_argument("--strategy", "-s", choices=list(STRATEGIES) + ["all"], default="all")
|
||||
p.add_argument("--fee-tier", type=int, default=0, choices=range(7))
|
||||
p.add_argument("--staking-tier", default="none", choices=list(STAKING_TIERS.keys()))
|
||||
@@ -354,8 +378,72 @@ def main():
|
||||
cfg = STRATEGIES[key]
|
||||
print(f"\n Running: {cfg['name']} on {a.coin}...")
|
||||
|
||||
# Kalman Pairs: use real BTC/ETH pair data
|
||||
if key == "kalman_pairs":
|
||||
try:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from strategies.kalman_pairs import KalmanPairsTrader, backtest_kalman_pairs
|
||||
|
||||
# Fetch ETH candles
|
||||
if a.coin != "ETH":
|
||||
eth_candles = fetch_candles("ETH", interval="1h", limit=a.hours)
|
||||
else:
|
||||
eth_candles = fetch_candles("BTC", interval="1h", limit=a.hours)
|
||||
|
||||
if eth_candles:
|
||||
X = [float(c["c"]) for c in eth_candles]
|
||||
Y = [float(c["c"]) for c in candles]
|
||||
n = min(len(X), len(Y))
|
||||
X, Y = X[:n], Y[:n]
|
||||
|
||||
trader = KalmanPairsTrader(
|
||||
transition_covariance=1e-4, observation_covariance=1e-2,
|
||||
z_entry=2.0, z_exit=0.5, warmup_bars=20,
|
||||
)
|
||||
bt = backtest_kalman_pairs(
|
||||
X, Y, trader,
|
||||
trade_size_usd=50.0,
|
||||
transaction_cost_bps=2.5,
|
||||
)
|
||||
|
||||
# Convert to standard format expected by the dashboard
|
||||
result = {
|
||||
"strategy": cfg["name"],
|
||||
"strategy_key": key,
|
||||
"coin": a.coin,
|
||||
"allocation": 100.0,
|
||||
"start_time": str(bt["equity_curve"][0]["t"]) if bt["equity_curve"] else "",
|
||||
"end_time": str(bt["equity_curve"][-1]["t"]) if bt["equity_curve"] else "",
|
||||
"start_equity": 100.0,
|
||||
"end_equity": round(bt["final_equity"], 4),
|
||||
"pnl": round(bt["total_pnl"], 4),
|
||||
"pnl_pct": round(bt["pnl_pct"], 2),
|
||||
"pnl_gross": round(bt["total_pnl"] + bt["transaction_costs"], 4),
|
||||
"pnl_gross_pct": round(bt["pnl_pct"], 2),
|
||||
"fees_total": round(bt["transaction_costs"], 4),
|
||||
"fee_tier": a.fee_tier,
|
||||
"staking_tier": a.staking_tier,
|
||||
"fee_model": cfg["fee_model"],
|
||||
"sharpe": round(bt["sharpe"], 4),
|
||||
"sortino": round(bt["sortino"], 4),
|
||||
"max_dd": round(bt["max_drawdown"], 4),
|
||||
"max_dd_pct": round(bt["max_drawdown"] * 100, 2),
|
||||
"win_rate": round(bt["win_rate"], 4),
|
||||
"total_trades": bt["total_trades"],
|
||||
"equity_curve": [{"t": e["t"], "v": e["equity"]} for e in bt["equity_curve"]],
|
||||
"trades": bt["trades"][-100:],
|
||||
"num_periods": n,
|
||||
"data_source": "Hyperliquid Mainnet (BTC/ETH pair)",
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
}
|
||||
else:
|
||||
result = {} # Skip
|
||||
except Exception as e:
|
||||
print(f" Kalman pairs error: {e}")
|
||||
result = {}
|
||||
else:
|
||||
result = simulate_strategy_on_candles(
|
||||
key, candles,
|
||||
key, candles, a.coin,
|
||||
fee_tier=a.fee_tier,
|
||||
staking_tier=a.staking_tier,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
"""
|
||||
NautilusTrader event-driven backtest engine for Hyperliquid strategies.
|
||||
|
||||
Sets up a BacktestEngine with Hyperliquid venue, instruments, historical
|
||||
bar data, and registered strategies. Runs event-driven simulation with
|
||||
realistic fill emulation (maker/taker, slippage).
|
||||
|
||||
Slower but more realistic than VectorBT — intended for final validation
|
||||
before paper/live deployment.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from nautilus_trader.backtest.engine import BacktestEngine, BacktestEngineConfig
|
||||
from nautilus_trader.model.data import Bar, BarSpecification, BarType
|
||||
from nautilus_trader.model.enums import AccountType, BarAggregation, OmsType, PriceType
|
||||
from nautilus_trader.model.identifiers import InstrumentId, Venue
|
||||
from nautilus_trader.model.instruments import CryptoPerpetual
|
||||
from nautilus_trader.model.objects import Currency, Money, Price, Quantity
|
||||
|
||||
from framework.data import HyperliquidDataProvider, INTERVAL_TO_SECONDS
|
||||
from framework.instruments import HL_VENUE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
INTERVAL_TO_AGG = {
|
||||
"1m": (1, BarAggregation.MINUTE),
|
||||
"5m": (5, BarAggregation.MINUTE),
|
||||
"15m": (15, BarAggregation.MINUTE),
|
||||
"30m": (30, BarAggregation.MINUTE),
|
||||
"1h": (1, BarAggregation.HOUR),
|
||||
"4h": (4, BarAggregation.HOUR),
|
||||
"8h": (8, BarAggregation.HOUR),
|
||||
"1d": (1, BarAggregation.DAY),
|
||||
}
|
||||
|
||||
|
||||
class NTBacktestRunner:
|
||||
"""NautilusTrader backtest engine wrapper for Hyperliquid."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def run_backtest(
|
||||
self,
|
||||
strategy: str = "pairs",
|
||||
interval: str = "1h",
|
||||
instruments: dict[str, CryptoPerpetual] | None = None,
|
||||
testnet: bool = False,
|
||||
limit: int = 5000,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Run event-driven backtest with NautilusTrader.
|
||||
|
||||
1. Set up BacktestEngine
|
||||
2. Register Hyperliquid venue + instruments
|
||||
3. Load historical bars from Hyperliquid
|
||||
4. Add strategy and run
|
||||
5. Return metrics
|
||||
"""
|
||||
|
||||
step, agg = INTERVAL_TO_AGG.get(interval, (1, BarAggregation.HOUR))
|
||||
|
||||
config = BacktestEngineConfig()
|
||||
engine = BacktestEngine(config=config)
|
||||
engine.add_venue(
|
||||
venue=HL_VENUE,
|
||||
oms_type=OmsType.NETTING,
|
||||
account_type=AccountType.MARGIN,
|
||||
starting_balances=[Money(10_000.0, Currency.from_str("USD"))],
|
||||
)
|
||||
|
||||
# Add instruments
|
||||
coin = self._get_coin(strategy)
|
||||
inst_for_coin = None
|
||||
if instruments:
|
||||
for name, inst in instruments.items():
|
||||
engine.add_instrument(inst)
|
||||
if name.upper() == coin.upper():
|
||||
inst_for_coin = inst
|
||||
|
||||
if not inst_for_coin and instruments:
|
||||
# Try to find any instrument matching
|
||||
for inst in instruments.values():
|
||||
instr_name = str(inst.id.symbol)
|
||||
if coin.upper() in instr_name.upper():
|
||||
inst_for_coin = inst
|
||||
break
|
||||
|
||||
sz_prec = inst_for_coin.size_precision if inst_for_coin else 5
|
||||
|
||||
# Fetch real candles
|
||||
provider = HyperliquidDataProvider(testnet=testnet)
|
||||
df = provider.fetch_candles(coin, interval=interval, limit=limit)
|
||||
if df.empty:
|
||||
logger.error("No candles for %s", coin)
|
||||
return None
|
||||
|
||||
# Build bars
|
||||
inst_id = InstrumentId.from_str(f"{coin.upper()}-USD-PERP.HYPERLIQUID")
|
||||
bars = self._df_to_bars(df, inst_id, step, agg, size_precision=sz_prec)
|
||||
|
||||
# Add bars
|
||||
engine.add_data(bars)
|
||||
|
||||
# Add strategy
|
||||
strategy_class = self._resolve_strategy_class(strategy)
|
||||
if strategy_class is None:
|
||||
logger.error("No NT strategy class for %s", strategy)
|
||||
return None
|
||||
|
||||
from framework.config import StrategyConfig
|
||||
cfg = StrategyConfig(
|
||||
name=strategy,
|
||||
instrument=f"{coin}-USD-PERP",
|
||||
asset=coin,
|
||||
allocation=10000.0,
|
||||
order_size=0.001,
|
||||
)
|
||||
nt_strategy = strategy_class(cfg)
|
||||
engine.add_strategy(nt_strategy)
|
||||
|
||||
# Run
|
||||
try:
|
||||
result = engine.run()
|
||||
except Exception as e:
|
||||
logger.error("Backtest engine error: %s", e)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
# Extract metrics
|
||||
return self._extract_result(result, engine, strategy, interval, len(bars))
|
||||
|
||||
# ── Helpers ─────────────────────────────────────────────────
|
||||
|
||||
def _get_coin(self, strategy: str) -> str:
|
||||
return {"pairs": "ETH", "hurst_vpin": "BTC", "as_mm": "BTC",
|
||||
"obi": "BTC", "funding_arb": "BTC"}.get(strategy, "BTC")
|
||||
|
||||
def _df_to_bars(
|
||||
self,
|
||||
df: pd.DataFrame,
|
||||
instrument_id: InstrumentId,
|
||||
step: int,
|
||||
aggregation: BarAggregation,
|
||||
size_precision: int = 5,
|
||||
) -> list[Bar]:
|
||||
spec = BarSpecification(step, aggregation, PriceType.LAST)
|
||||
bar_type = BarType(instrument_id, spec)
|
||||
bars = []
|
||||
for idx, row in df.iterrows():
|
||||
ts = int(idx.timestamp() * 1e9)
|
||||
bar = Bar(
|
||||
bar_type=bar_type,
|
||||
open=Price.from_str(str(row["open"])),
|
||||
high=Price.from_str(str(row["high"])),
|
||||
low=Price.from_str(str(row["low"])),
|
||||
close=Price.from_str(str(row["close"])),
|
||||
volume=Quantity.from_str(f'{row["volume"]:.{size_precision}f}'),
|
||||
ts_event=ts,
|
||||
ts_init=ts,
|
||||
)
|
||||
bars.append(bar)
|
||||
return bars
|
||||
|
||||
def _resolve_strategy_class(self, strategy: str):
|
||||
import importlib
|
||||
registry = {
|
||||
"pairs": "strategies.nt.pairs_trading_nt.PairsTradingNT",
|
||||
"hurst_vpin": "strategies.nt.hurst_vpin_nt.HurstVPINNT",
|
||||
"as_mm": "strategies.nt.as_mm_nt.ASMarketMakingNT",
|
||||
}
|
||||
path = registry.get(strategy)
|
||||
if not path:
|
||||
return None
|
||||
module_path, class_name = path.rsplit(".", 1)
|
||||
mod = importlib.import_module(module_path)
|
||||
return getattr(mod, class_name)
|
||||
|
||||
def _extract_result(
|
||||
self,
|
||||
result,
|
||||
engine,
|
||||
strategy: str,
|
||||
interval: str,
|
||||
n_bars: int,
|
||||
) -> dict:
|
||||
try:
|
||||
pnl = float(sum(
|
||||
a.pnl() for a in result.accounts if hasattr(a, 'pnl')
|
||||
)) if hasattr(result, 'accounts') else 0.0
|
||||
except Exception:
|
||||
pnl = 0.0
|
||||
|
||||
try:
|
||||
equity = result.equity_curve if hasattr(result, 'equity_curve') else None
|
||||
except Exception:
|
||||
equity = None
|
||||
|
||||
equity_vals = []
|
||||
if equity is not None and hasattr(equity, '__iter__'):
|
||||
equity_vals = [float(v) for v in equity] if equity is not None else []
|
||||
|
||||
total_return = (equity_vals[-1] / 10000.0 - 1) * 100 if equity_vals else 0.0
|
||||
|
||||
return {
|
||||
"strategy": strategy,
|
||||
"engine": "nautilus_trader",
|
||||
"interval": interval,
|
||||
"n_bars": n_bars,
|
||||
"start_equity": 10000.0,
|
||||
"end_equity": equity_vals[-1] if equity_vals else 10000.0,
|
||||
"total_return_pct": round(total_return, 2),
|
||||
"pnl": round(pnl, 2),
|
||||
"sharpe": self._compute_sharpe(equity_vals),
|
||||
"max_drawdown_pct": round(self._compute_max_dd(equity_vals) * 100, 2),
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
def _compute_sharpe(self, equity: list[float]) -> float:
|
||||
if len(equity) < 2:
|
||||
return 0.0
|
||||
returns = [(equity[i] - equity[i - 1]) / equity[i - 1] for i in range(1, len(equity))]
|
||||
mean_ret = np.mean(returns) if returns else 0.0
|
||||
std_ret = np.std(returns, ddof=1) if returns else 0.0
|
||||
return (mean_ret / std_ret) * np.sqrt(365 * 24) if std_ret > 0 else 0.0
|
||||
|
||||
def _compute_max_dd(self, equity: list[float]) -> float:
|
||||
if not equity:
|
||||
return 0.0
|
||||
peak = equity[0]
|
||||
worst = 0.0
|
||||
for v in equity:
|
||||
if v > peak:
|
||||
peak = v
|
||||
dd = (peak - v) / peak if peak > 0 else 0.0
|
||||
worst = max(worst, dd)
|
||||
return worst
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"strategy": "Funding Rate Arb",
|
||||
"strategy_key": "funding_arb",
|
||||
"coin": 1783234800000,
|
||||
"strategy": "Avellaneda-Stoikov",
|
||||
"strategy_key": "avellaneda",
|
||||
"coin": "HYPE",
|
||||
"allocation": 100.0,
|
||||
"start_time": "2026-07-05T07:00:00",
|
||||
"end_time": "2026-08-04T07:00:00",
|
||||
"start_time": "2026-07-06T05:00:00",
|
||||
"end_time": "2026-08-05T05:00:00",
|
||||
"start_equity": 100.0,
|
||||
"end_equity": 100.0,
|
||||
"pnl": 0.0,
|
||||
@@ -14,7 +14,7 @@
|
||||
"fees_total": 0.0,
|
||||
"fee_tier": 0,
|
||||
"staking_tier": "none",
|
||||
"fee_model": "taker",
|
||||
"fee_model": "maker",
|
||||
"sharpe": 0.0,
|
||||
"sortino": 0.0,
|
||||
"max_dd": 0.0,
|
||||
@@ -22,94 +22,6 @@
|
||||
"win_rate": 0.0,
|
||||
"total_trades": 0,
|
||||
"equity_curve": [
|
||||
{
|
||||
"t": "2026-07-05T07:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T08:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T09:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T10:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T11:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T12:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T13:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T14:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T15:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T16:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T17:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T18:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T19:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T20:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T21:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T22:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T23:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T00:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T01:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T02:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T03:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T04:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T05:00:00",
|
||||
"v": 100.0
|
||||
@@ -2905,10 +2817,98 @@
|
||||
{
|
||||
"t": "2026-08-04T07:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T08:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T09:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T10:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T11:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T12:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T13:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T14:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T15:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T16:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T17:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T18:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T19:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T20:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T21:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T22:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T23:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T00:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T01:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T02:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T03:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T04:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T05:00:00",
|
||||
"v": 100.0
|
||||
}
|
||||
],
|
||||
"trades": [],
|
||||
"num_periods": 721,
|
||||
"data_source": "Hyperliquid Mainnet",
|
||||
"generated_at": "2026-08-04T07:28:17.886718"
|
||||
"generated_at": "2026-08-05T05:07:41.026245"
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"strategy": "Funding Rate Arb",
|
||||
"strategy_key": "funding_arb",
|
||||
"coin": 1783234800000,
|
||||
"strategy": "Avellaneda-Stoikov",
|
||||
"strategy_key": "avellaneda",
|
||||
"coin": "VVV",
|
||||
"allocation": 100.0,
|
||||
"start_time": "2026-07-05T07:00:00",
|
||||
"end_time": "2026-08-04T07:00:00",
|
||||
"start_time": "2026-07-06T05:00:00",
|
||||
"end_time": "2026-08-05T05:00:00",
|
||||
"start_equity": 100.0,
|
||||
"end_equity": 100.0,
|
||||
"pnl": 0.0,
|
||||
@@ -14,7 +14,7 @@
|
||||
"fees_total": 0.0,
|
||||
"fee_tier": 0,
|
||||
"staking_tier": "none",
|
||||
"fee_model": "taker",
|
||||
"fee_model": "maker",
|
||||
"sharpe": 0.0,
|
||||
"sortino": 0.0,
|
||||
"max_dd": 0.0,
|
||||
@@ -22,94 +22,6 @@
|
||||
"win_rate": 0.0,
|
||||
"total_trades": 0,
|
||||
"equity_curve": [
|
||||
{
|
||||
"t": "2026-07-05T07:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T08:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T09:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T10:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T11:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T12:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T13:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T14:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T15:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T16:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T17:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T18:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T19:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T20:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T21:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T22:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-05T23:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T00:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T01:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T02:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T03:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T04:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-07-06T05:00:00",
|
||||
"v": 100.0
|
||||
@@ -2905,10 +2817,98 @@
|
||||
{
|
||||
"t": "2026-08-04T07:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T08:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T09:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T10:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T11:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T12:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T13:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T14:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T15:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T16:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T17:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T18:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T19:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T20:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T21:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T22:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-04T23:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T00:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T01:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T02:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T03:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T04:00:00",
|
||||
"v": 100.0
|
||||
},
|
||||
{
|
||||
"t": "2026-08-05T05:00:00",
|
||||
"v": 100.0
|
||||
}
|
||||
],
|
||||
"trades": [],
|
||||
"num_periods": 721,
|
||||
"data_source": "Hyperliquid Mainnet",
|
||||
"generated_at": "2026-08-04T07:29:03.636645"
|
||||
"generated_at": "2026-08-05T05:07:41.798429"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"strategy": "Hurst VPIN", "strategy_key": "hurst_vpin", "coin": "ETH", "allocation": 99.9999907962162, "start_time": "2026-08-06T07:09:06.405305", "end_time": "2026-08-06T07:09:06.405333", "start_equity": 100.0, "end_equity": 99.9374907962162, "pnl": -0.06, "pnl_pct": -0.06, "sharpe": 0.35, "sortino": 0.64, "max_dd": 0.0003, "win_rate": 0.0, "total_trades": 1, "trades": [{"time": "2026-08-06T07:03:31.457823", "side": "SELL", "entry_price": 1868.6, "size": 0.00024, "hurst": 0.5751, "vpin": 0.7986, "bar_count": 236, "exit_price": 1856.9195301809586, "pnl": -0.0625}], "equity_curve": [{"t": 0, "v": 100.0}, {"t": 1000, "v": 99.9374907962162}], "signals_generated": 15385, "data_source": "hyperliquid_mainnet"}
|
||||
@@ -0,0 +1 @@
|
||||
{"strategy": "Hurst VPIN", "strategy_key": "hurst_vpin", "coin": "HYPE", "allocation": 99.999993931191, "start_time": "2026-08-06T07:14:02.976396", "end_time": "2026-08-06T07:14:02.976412", "start_equity": 100.0, "end_equity": 100.092893931191, "pnl": 0.09, "pnl_pct": 0.09, "sharpe": 0.21, "sortino": 0.61, "max_dd": 0, "win_rate": 1.0, "total_trades": 1, "trades": [{"time": "2026-08-06T07:09:16.367204", "side": "SELL", "entry_price": 52.65, "size": 0.00024, "hurst": 0.568, "vpin": 0.853, "bar_count": 532, "exit_price": 53.13908654772063, "pnl": 0.0929}], "equity_curve": [{"t": 0, "v": 100.0}, {"t": 1000, "v": 100.092893931191}], "signals_generated": 13450, "data_source": "hyperliquid_mainnet"}
|
||||
@@ -0,0 +1 @@
|
||||
{"strategy": "Hurst VPIN", "strategy_key": "hurst_vpin", "coin": "VVV", "allocation": 99.99999936064138, "start_time": "2026-08-06T07:14:11.162510", "end_time": "2026-08-06T07:14:11.162521", "start_equity": 100.0, "end_equity": 99.93239936064138, "pnl": -0.07, "pnl_pct": -0.07, "sharpe": 0.43, "sortino": 0.52, "max_dd": 0.0003, "win_rate": 0.0, "total_trades": 1, "trades": [{"time": "2026-08-06T07:14:03.784899", "side": "SELL", "entry_price": 12.625, "size": 0.00024, "hurst": 0.5848, "vpin": 0.4823, "bar_count": 149, "exit_price": 12.539654192809744, "pnl": -0.0676}], "equity_curve": [{"t": 0, "v": 100.0}, {"t": 1000, "v": 99.93239936064138}], "signals_generated": 485, "data_source": "hyperliquid_mainnet"}
|
||||
@@ -0,0 +1 @@
|
||||
{"strategy": "SPX Mean Reversion", "strategy_key": "spx_mean_reversion", "coin": "SPX", "allocation": 100.0, "start_time": "2026-07-30T07:30:00", "end_time": "2026-08-06T07:30:00", "start_equity": 100.0, "end_equity": 100.14, "pnl": 0.14, "pnl_pct": 0.14, "sharpe": 0.63, "sortino": 1.0, "max_dd": 0.01, "win_rate": 0.7727, "total_trades": 22, "trades": [{"time": "2026-08-06T07:34:45.055488", "side": "BUY", "entry_price": 0.33078, "z_entry": -1.58, "size": 0.01, "exit_price": 0.33383, "pnl": 0.0092, "exit_z": -0.28}, {"time": "2026-08-06T07:34:45.055850", "side": "SELL", "entry_price": 0.33624, "z_entry": 2.24, "size": 0.01, "exit_price": 0.32967, "pnl": 0.0195, "exit_z": -1.78}, {"time": "2026-08-06T07:34:45.055974", "side": "BUY", "entry_price": 0.32918, "z_entry": -1.83, "size": 0.01, "exit_price": 0.32434, "pnl": -0.0147, "exit_z": -0.22}, {"time": "2026-08-06T07:34:45.056641", "side": "BUY", "entry_price": 0.32171, "z_entry": -1.5, "size": 0.01, "exit_price": 0.32165, "pnl": -0.0002, "exit_z": 0.18}, {"time": "2026-08-06T07:34:45.057239", "side": "SELL", "entry_price": 0.32372, "z_entry": 1.84, "size": 0.01, "exit_price": 0.3218, "pnl": 0.0059, "exit_z": 0.19}, {"time": "2026-08-06T07:34:45.057798", "side": "SELL", "entry_price": 0.32404, "z_entry": 2.08, "size": 0.01, "exit_price": 0.32262, "pnl": 0.0044, "exit_z": -0.06}, {"time": "2026-08-06T07:34:45.058058", "side": "SELL", "entry_price": 0.32373, "z_entry": 1.62, "size": 0.01, "exit_price": 0.32046, "pnl": 0.0101, "exit_z": -3.19}, {"time": "2026-08-06T07:34:45.058229", "side": "BUY", "entry_price": 0.31678, "z_entry": -8.2, "size": 0.01, "exit_price": 0.31904, "pnl": 0.0071, "exit_z": -0.06}, {"time": "2026-08-06T07:34:45.058593", "side": "SELL", "entry_price": 0.32421, "z_entry": 2.0, "size": 0.01, "exit_price": 0.32154, "pnl": 0.0082, "exit_z": 0.09}, {"time": "2026-08-06T07:34:45.059004", "side": "BUY", "entry_price": 0.31961, "z_entry": -1.91, "size": 0.01, "exit_price": 0.32139, "pnl": 0.0056, "exit_z": -0.24}, {"time": "2026-08-06T07:34:45.059289", "side": "SELL", "entry_price": 0.32407, "z_entry": 2.02, "size": 0.01, "exit_price": 0.32727, "pnl": -0.0099, "exit_z": 0.04}, {"time": "2026-08-06T07:34:45.059931", "side": "BUY", "entry_price": 0.32388, "z_entry": -1.66, "size": 0.01, "exit_price": 0.32533, "pnl": 0.0045, "exit_z": -0.16}, {"time": "2026-08-06T07:34:45.060187", "side": "SELL", "entry_price": 0.33477, "z_entry": 4.22, "size": 0.01, "exit_price": 0.3297, "pnl": 0.0152, "exit_z": -0.11}, {"time": "2026-08-06T07:34:45.060590", "side": "BUY", "entry_price": 0.32663, "z_entry": -1.55, "size": 0.01, "exit_price": 0.32146, "pnl": -0.0158, "exit_z": 0.27}, {"time": "2026-08-06T07:34:45.061181", "side": "SELL", "entry_price": 0.32413, "z_entry": 2.06, "size": 0.01, "exit_price": 0.32194, "pnl": 0.0068, "exit_z": -0.01}, {"time": "2026-08-06T07:34:45.061481", "side": "SELL", "entry_price": 0.32694, "z_entry": 2.25, "size": 0.01, "exit_price": 0.32449, "pnl": 0.0075, "exit_z": 0.2}, {"time": "2026-08-06T07:34:45.061750", "side": "BUY", "entry_price": 0.32057, "z_entry": -3.02, "size": 0.01, "exit_price": 0.32404, "pnl": 0.0108, "exit_z": -0.18}, {"time": "2026-08-06T07:34:45.061851", "side": "SELL", "entry_price": 0.32752, "z_entry": 2.03, "size": 0.01, "exit_price": 0.32466, "pnl": 0.0087, "exit_z": 0.26}, {"time": "2026-08-06T07:34:45.061952", "side": "SELL", "entry_price": 0.32729, "z_entry": 1.58, "size": 0.01, "exit_price": 0.32752, "pnl": -0.0007, "exit_z": 0.28}, {"time": "2026-08-06T07:34:45.062542", "side": "BUY", "entry_price": 0.32456, "z_entry": -1.89, "size": 0.01, "exit_price": 0.3315, "pnl": 0.0214, "exit_z": 2.35}, {"time": "2026-08-06T07:34:45.062693", "side": "SELL", "entry_price": 0.33111, "z_entry": 2.07, "size": 0.01, "exit_price": 0.3259, "pnl": 0.0158, "exit_z": -2.79}, {"time": "2026-08-06T07:34:45.063074", "side": "BUY", "entry_price": 0.32525, "z_entry": -2.84, "size": 0.01, "exit_price": 0.33141, "pnl": 0.019, "exit_z": -0.06}], "data_source": "TradeXYZ / Hyperliquid SPX"}
|
||||
@@ -0,0 +1,335 @@
|
||||
"""
|
||||
VectorBT backtest runner — fast vectorized backtesting on Hyperliquid candle data.
|
||||
|
||||
Fetches real candles from Hyperliquid, converts to signals, and runs
|
||||
through VectorBT's Portfolio simulator for instant results.
|
||||
|
||||
Supports parameter sweeps, walk-forward optimization, and full metrics.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import vectorbt as vbt
|
||||
|
||||
sys_path = str(Path(__file__).resolve().parent.parent)
|
||||
if sys_path not in __import__("sys").path:
|
||||
__import__("sys").path.insert(0, sys_path)
|
||||
|
||||
from framework.data import HyperliquidDataProvider, INTERVAL_MAP
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RESULTS_DIR = Path(__file__).resolve().parent / "results"
|
||||
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Strategy signal generators
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def _generate_signals(strategy: str, data: dict[str, pd.DataFrame]) -> tuple[pd.Series, pd.Series]:
|
||||
"""Generate entry/exit signals for a strategy from candle data.
|
||||
|
||||
Returns (entries, exits) as boolean pandas Series.
|
||||
Each strategy uses the primary coin's close prices.
|
||||
"""
|
||||
main_coin = {"pairs": "ETH", "hurst_vpin": "BTC", "as_mm": "BTC",
|
||||
"obi": "BTC", "funding_arb": "BTC", "momentum": "BTC",
|
||||
"mean_rev": "BTC"}.get(strategy, "BTC")
|
||||
df = data.get(main_coin)
|
||||
if df is None or df.empty:
|
||||
return pd.Series(dtype=bool), pd.Series(dtype=bool)
|
||||
|
||||
close = df["close"]
|
||||
entries = pd.Series(False, index=close.index)
|
||||
exits = pd.Series(False, index=close.index)
|
||||
|
||||
if strategy == "pairs":
|
||||
btc_df = data.get("BTC")
|
||||
if btc_df is not None and not btc_df.empty:
|
||||
ratio = btc_df["close"] / close
|
||||
mu = ratio.rolling(20).mean()
|
||||
std = ratio.rolling(20).std()
|
||||
z = (ratio - mu) / std
|
||||
entries = z < -1.5
|
||||
exits = z.shift(1) >= -0.5
|
||||
|
||||
elif strategy == "hurst_vpin":
|
||||
returns = close.pct_change().dropna()
|
||||
hurst = returns.rolling(64).apply(_hurst_rs_series, raw=False)
|
||||
entries = hurst > 0.55
|
||||
exits = hurst.shift(1) < 0.45
|
||||
|
||||
elif strategy == "as_mm":
|
||||
spread = (df["high"] - df["low"]) / df["close"]
|
||||
vol = close.pct_change().rolling(20).std()
|
||||
favorable = (spread > spread.rolling(100).mean()) & (vol < 0.02)
|
||||
entries = favorable
|
||||
exits = favorable.shift(3)
|
||||
|
||||
elif strategy == "momentum":
|
||||
sma = close.rolling(20).mean()
|
||||
std = close.rolling(20).std()
|
||||
upper = sma + 2 * std
|
||||
lower = sma - 2 * std
|
||||
entries = (close > upper) | (close < lower)
|
||||
exits = (close.shift(1) > sma.shift(1)) & (close < sma)
|
||||
|
||||
elif strategy in ("mean_rev", "obi"):
|
||||
sma = close.rolling(20).mean()
|
||||
std = close.rolling(20).std()
|
||||
entries = (close < sma - 1.0 * std) | (close > sma + 1.0 * std)
|
||||
exits = abs((close - sma) / std) < 0.3
|
||||
|
||||
elif strategy == "funding_arb":
|
||||
entries[:] = False
|
||||
exits[:] = False
|
||||
|
||||
entries.fillna(False, inplace=True)
|
||||
exits.fillna(False, inplace=True)
|
||||
return entries, exits
|
||||
|
||||
|
||||
def _hurst_rs_series(returns_series: pd.Series) -> float:
|
||||
"""Hurst exponent via R/S on a window of log returns."""
|
||||
rets = returns_series.dropna().values
|
||||
if len(rets) < 32:
|
||||
return 0.5
|
||||
n = len(rets)
|
||||
max_lag = min(n // 2, 64)
|
||||
lags = []
|
||||
rs_vals = []
|
||||
for lag in range(4, max_lag):
|
||||
segs = n // lag
|
||||
if segs < 2:
|
||||
continue
|
||||
vals = []
|
||||
for s in range(segs):
|
||||
seg = rets[s * lag:(s + 1) * lag]
|
||||
mean = np.mean(seg)
|
||||
dev = np.cumsum(seg - mean)
|
||||
r = float(np.max(dev) - np.min(dev))
|
||||
sd = float(np.std(seg, ddof=1))
|
||||
if sd > 1e-12:
|
||||
vals.append(r / sd)
|
||||
if vals:
|
||||
lags.append(np.log(lag))
|
||||
rs_vals.append(np.log(np.mean(vals)))
|
||||
if len(lags) < 4:
|
||||
return 0.5
|
||||
slope = float(np.polyfit(lags, rs_vals, 1)[0])
|
||||
return max(0.2, min(0.8, slope))
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# VBT Backtest Runner
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class VBTBacktestRunner:
|
||||
"""VectorBT-powered backtesting on Hyperliquid candle data."""
|
||||
|
||||
def __init__(self, fee_rate: float = 0.0005):
|
||||
self._provider = HyperliquidDataProvider()
|
||||
self._fee_rate = fee_rate
|
||||
|
||||
def run_strategy(
|
||||
self,
|
||||
strategy: str = "pairs",
|
||||
interval: str = "1h",
|
||||
testnet: bool = False,
|
||||
limit: int = 5000,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Fetch candles, generate signals, run VBT backtest, return metrics."""
|
||||
coins = self._get_coins(strategy)
|
||||
provider = HyperliquidDataProvider(testnet=testnet)
|
||||
|
||||
data = {}
|
||||
for coin in coins:
|
||||
try:
|
||||
df = provider.fetch_candles(coin, interval=interval, limit=limit)
|
||||
if not df.empty:
|
||||
data[coin] = df
|
||||
except Exception as e:
|
||||
logger.warning("Failed to fetch %s: %s", coin, e)
|
||||
|
||||
if not data:
|
||||
logger.error("No candle data fetched for strategy: %s", strategy)
|
||||
return None
|
||||
|
||||
entries, exits = _generate_signals(strategy, data)
|
||||
|
||||
primary = list(data.values())[0]
|
||||
close = primary["close"]
|
||||
|
||||
# Align indices
|
||||
common_idx = entries.index.intersection(close.index)
|
||||
entries = entries.reindex(common_idx).fillna(False)
|
||||
exits = exits.reindex(common_idx).fillna(False)
|
||||
close = close.reindex(common_idx)
|
||||
|
||||
if entries.sum() == 0:
|
||||
logger.warning("No signals generated for %s", strategy)
|
||||
return self._empty_result(strategy, interval)
|
||||
|
||||
try:
|
||||
pf = vbt.Portfolio.from_signals(
|
||||
close=close,
|
||||
entries=entries,
|
||||
exits=exits,
|
||||
fees=self._fee_rate,
|
||||
slippage=0.001,
|
||||
freq=INTERVAL_MAP.get(interval, "1h"),
|
||||
init_cash=10000.0,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("VBT portfolio error: %s", e)
|
||||
return self._empty_result(strategy, interval)
|
||||
|
||||
stats = pf.stats()
|
||||
result = self._extract_metrics(pf, stats, strategy, interval, len(close))
|
||||
|
||||
# Save equity curve
|
||||
eq_curve = pf.value().dropna()
|
||||
result["equity_curve"] = [
|
||||
{"t": idx.isoformat(), "v": round(float(v), 2)}
|
||||
for idx, v in eq_curve.to_dict().items()
|
||||
]
|
||||
result["total_trades"] = int(pf.trades.count())
|
||||
result["generated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
return result
|
||||
|
||||
def param_sweep(
|
||||
self,
|
||||
strategy: str = "pairs",
|
||||
param_grid: dict[str, list] | None = None,
|
||||
) -> pd.DataFrame | None:
|
||||
"""Grid search over parameters using VBT."""
|
||||
|
||||
coins = self._get_coins(strategy)
|
||||
data = {}
|
||||
for coin in coins:
|
||||
df = self._provider.fetch_candles(coin, interval="1h", limit=2000)
|
||||
if not df.empty:
|
||||
data[coin] = df
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
primary = list(data.values())[0]
|
||||
close = primary["close"]
|
||||
|
||||
if param_grid is None:
|
||||
param_grid = {
|
||||
"window": [10, 20, 30, 50],
|
||||
"threshold": [1.0, 1.5, 2.0, 2.5],
|
||||
}
|
||||
|
||||
results_rows = []
|
||||
for window in param_grid.get("window", [20]):
|
||||
for threshold in param_grid.get("threshold", [1.5]):
|
||||
entries, exits = _generate_signals_sweep(strategy, data, window, threshold)
|
||||
try:
|
||||
pf = vbt.Portfolio.from_signals(
|
||||
close=close,
|
||||
entries=entries,
|
||||
exits=exits,
|
||||
fees=self._fee_rate,
|
||||
init_cash=10000.0,
|
||||
)
|
||||
stats = pf.stats()
|
||||
results_rows.append({
|
||||
"window": window,
|
||||
"threshold": threshold,
|
||||
"sharpe": stats.get("Sharpe Ratio", 0),
|
||||
"total_return": stats.get("Total Return [%]", 0),
|
||||
"max_drawdown": stats.get("Max Drawdown [%]", 0),
|
||||
"win_rate": stats.get("Win Rate [%]", 0),
|
||||
"trades": int(pf.trades.count()),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return pd.DataFrame(results_rows) if results_rows else None
|
||||
|
||||
# ── Helpers ─────────────────────────────────────────────────
|
||||
|
||||
def _get_coins(self, strategy: str) -> list[str]:
|
||||
coin_map = {
|
||||
"pairs": ["BTC", "ETH"],
|
||||
"hurst_vpin": ["BTC"],
|
||||
"as_mm": ["BTC"],
|
||||
"obi": ["BTC"],
|
||||
"funding_arb": ["BTC"],
|
||||
"momentum": ["BTC"],
|
||||
"mean_rev": ["BTC"],
|
||||
}
|
||||
return coin_map.get(strategy, ["BTC"])
|
||||
|
||||
def _extract_metrics(self, pf, stats, strategy, interval, n_bars) -> dict:
|
||||
return {
|
||||
"strategy": strategy,
|
||||
"interval": interval,
|
||||
"n_bars": n_bars,
|
||||
"start_equity": 10000.0,
|
||||
"end_equity": round(float(pf.value().iloc[-1]), 2),
|
||||
"total_return_pct": round(float(stats.get("Total Return [%]", 0)), 2),
|
||||
"pnl": round(float(pf.value().iloc[-1]) - 10000, 2),
|
||||
"sharpe": round(float(stats.get("Sharpe Ratio", 0)), 3),
|
||||
"sortino": round(float(stats.get("Sortino Ratio", 0)), 3),
|
||||
"max_drawdown_pct": round(float(stats.get("Max Drawdown [%]", 0)), 2),
|
||||
"win_rate": round(float(stats.get("Win Rate [%]", 0)) / 100, 3),
|
||||
"profit_factor": round(float(stats.get("Profit Factor", 0)), 3),
|
||||
"expectancy": round(float(stats.get("Expectancy", 0)), 3),
|
||||
}
|
||||
|
||||
def _empty_result(self, strategy: str, interval: str) -> dict:
|
||||
return {
|
||||
"strategy": strategy,
|
||||
"interval": interval,
|
||||
"n_bars": 0,
|
||||
"start_equity": 10000.0,
|
||||
"end_equity": 10000.0,
|
||||
"total_return_pct": 0.0,
|
||||
"pnl": 0.0,
|
||||
"sharpe": 0.0,
|
||||
"sortino": 0.0,
|
||||
"max_drawdown_pct": 0.0,
|
||||
"win_rate": 0.0,
|
||||
"total_trades": 0,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def _generate_signals_sweep(
|
||||
strategy: str,
|
||||
data: dict[str, pd.DataFrame],
|
||||
window: int,
|
||||
threshold: float,
|
||||
) -> tuple[pd.Series, pd.Series]:
|
||||
"""Variant of signal generator for parameter sweeps with configurable params."""
|
||||
main_coin = {"pairs": "ETH", "hurst_vpin": "BTC", "as_mm": "BTC"}.get(strategy, "BTC")
|
||||
df = data.get(main_coin)
|
||||
if df is None or df.empty:
|
||||
return pd.Series(dtype=bool), pd.Series(dtype=bool)
|
||||
|
||||
close = df["close"]
|
||||
entries = pd.Series(False, index=close.index)
|
||||
exits = pd.Series(False, index=close.index)
|
||||
|
||||
sma = close.rolling(window).mean()
|
||||
std = close.rolling(window).std()
|
||||
entries = (close < sma - threshold * std) | (close > sma + threshold * std)
|
||||
exits = abs((close - sma) / (std + 1e-10)) < 0.3 * threshold
|
||||
|
||||
entries.fillna(False, inplace=True)
|
||||
exits.fillna(False, inplace=True)
|
||||
return entries, exits
|
||||
@@ -40,5 +40,7 @@ def max_drawdown(equity: list[float]) -> float:
|
||||
def win_rate(trades: list[dict]) -> float:
|
||||
if not trades:
|
||||
return 0.0
|
||||
tp = sum(1 for t in trades if (t.get("pnl_net") or t.get("pnl_gross") or t.get("pnl", 0)) > 0)
|
||||
tp = sum(1 for t in trades if (
|
||||
(t.get("pnl_net") or t.get("net_pnl") or t.get("pnl_gross") or t.get("gross_pnl") or t.get("pnl", 0)) > 0
|
||||
))
|
||||
return tp / len(trades)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
out-www/
|
||||
_next-app-backup/
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
images: { unoptimized: true },
|
||||
trailingSlash: true,
|
||||
basePath: "/cv",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,23 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[47257,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"ClientPageRoot"]
|
||||
3:I[52683,["/cv/_next/static/chunks/0h2qsuyze9ds1.js","/cv/_next/static/chunks/0oyhbmfwzlodf.js"],"default"]
|
||||
6:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"OutletBoundary"]
|
||||
7:"$Sreact.suspense"
|
||||
b:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"ViewportBoundary"]
|
||||
c:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"MetadataBoundary"]
|
||||
d:I[27201,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"IconMark"]
|
||||
f:I[39756,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
10:I[37457,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
:HL["/cv/_next/static/chunks/3wqi32p1v2fl1.css","style"]
|
||||
a:X
|
||||
0:{"buildId":"CxC9DIW0Ude54_bbbG52g","data":[{"rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","script","script-0",{"src":"/cv/_next/static/chunks/0oyhbmfwzlodf.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"isPartial":"$@9","staleTime":"$a","varyParams":null},{"rsc":["$","$1","h",{"children":[null,["$","$Lb",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]}],["$","div",null,{"hidden":true,"children":["$","$Lc",null,{"children":["$","$7",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"FTDT Quant Lab"}],["$","meta","1",{"name":"description","content":"Professional quantitative trading dashboard — Live, Paper, Backtest, Historical"}],["$","link","2",{"rel":"icon","href":"/cv/favicon.ico?favicon.2vob68tjqpejf.ico","sizes":"256x256","type":"image/x-icon"}],["$","$Ld","3",{}]]}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],"isPartial":"$@e","staleTime":"$a","varyParams":null},{"rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/cv/_next/static/chunks/3wqi32p1v2fl1.css","precedence":"next"}],["$","script","script-0",{"src":"/cv/_next/static/chunks/0h2qsuyze9ds1.js","async":true}]],["$","html",null,{"lang":"en","className":"dark","children":["$","body",null,{"className":"inter_b2991b2-module__9mH_6q__variable jetbrains_mono_d5591ac2-module__D88TVW__variable antialiased","children":["$","$Lf",null,{"parallelRouterKey":"children","template":["$","$L10",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]]}],"isPartial":"$@11","staleTime":"$a","varyParams":null}],"isUpgradeableISRFallback":false,"a":"$@12","rootVaryParams":null,"needsRuntimeRequest":"$@13"}
|
||||
4:{}
|
||||
5:"$0:data:0:rsc:props:children:0:props:serverProvidedParams:params"
|
||||
8:null
|
||||
a:300
|
||||
13:true
|
||||
a:C
|
||||
12:0
|
||||
e:"$undefined"
|
||||
11:"$undefined"
|
||||
9:"$undefined"
|
||||
@@ -0,0 +1,20 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
3:I[37457,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
4:I[47257,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"ClientPageRoot"]
|
||||
5:I[52683,["/cv/_next/static/chunks/0h2qsuyze9ds1.js","/cv/_next/static/chunks/0oyhbmfwzlodf.js"],"default"]
|
||||
8:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"OutletBoundary"]
|
||||
9:"$Sreact.suspense"
|
||||
b:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"ViewportBoundary"]
|
||||
d:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"MetadataBoundary"]
|
||||
f:I[68027,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default",1]
|
||||
:HL["/cv/_next/static/chunks/3wqi32p1v2fl1.css","style"]
|
||||
:HL["/cv/_next/static/media/70bc3e132a0a741e-s.p.3t6q91iet4nsy.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
:HL["/cv/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
0:{"P":null,"c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{},"$undefined","$undefined",4608]},"$undefined","$undefined",4624],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/cv/_next/static/chunks/3wqi32p1v2fl1.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/cv/_next/static/chunks/0h2qsuyze9ds1.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"dark","children":["$","body",null,{"className":"inter_b2991b2-module__9mH_6q__variable jetbrains_mono_d5591ac2-module__D88TVW__variable antialiased","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[["$","$L4",null,{"Component":"$5","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@6","$@7"]}}],[["$","script","script-0",{"src":"/cv/_next/static/chunks/0oyhbmfwzlodf.js","async":true,"nonce":"$undefined"}]],["$","$L8",null,{"children":["$","$9",null,{"name":"Next.MetadataOutlet","children":"$@a"}]}]]}],{},null,false,null]},null,false,null],["$","$1","h",{"children":[null,["$","$Lb",null,{"children":"$Lc"}],["$","div",null,{"hidden":true,"children":["$","$Ld",null,{"children":["$","$9",null,{"name":"Next.Metadata","children":"$Le"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$f",[["$","link","0",{"rel":"stylesheet","href":"/cv/_next/static/chunks/3wqi32p1v2fl1.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"r":"$undefined","s":"$undefined","a":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"CxC9DIW0Ude54_bbbG52g"}
|
||||
6:{}
|
||||
7:"$0:f:0:1:1:children:0:props:children:0:props:serverProvidedParams:params"
|
||||
c:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
||||
10:I[27201,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"IconMark"]
|
||||
a:null
|
||||
e:[["$","title","0",{"children":"FTDT Quant Lab"}],["$","meta","1",{"name":"description","content":"Professional quantitative trading dashboard — Live, Paper, Backtest, Historical"}],["$","link","2",{"rel":"icon","href":"/cv/favicon.ico?favicon.2vob68tjqpejf.ico","sizes":"256x256","type":"image/x-icon"}],["$","$L10","3",{}]]
|
||||
@@ -0,0 +1,4 @@
|
||||
:HL["/cv/_next/static/chunks/3wqi32p1v2fl1.css","style"]
|
||||
:HL["/cv/_next/static/media/70bc3e132a0a741e-s.p.3t6q91iet4nsy.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
:HL["/cv/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
0:{"tree":{"name":"","param":null,"prefetchHints":4176,"slots":{"children":{"name":"__PAGE__","param":null,"prefetchHints":4256,"slots":null}}},"staleTime":300,"buildId":"CxC9DIW0Ude54_bbbG52g"}
|
||||
@@ -0,0 +1,11 @@
|
||||
self.__BUILD_MANIFEST = {
|
||||
"__rewrites": {
|
||||
"afterFiles": [],
|
||||
"beforeFiles": [],
|
||||
"fallback": []
|
||||
},
|
||||
"sortedPages": [
|
||||
"/_app",
|
||||
"/_error"
|
||||
]
|
||||
};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
|
||||
@@ -0,0 +1 @@
|
||||
self.__MIDDLEWARE_MATCHERS = [];self.__MIDDLEWARE_MATCHERS_CB && self.__MIDDLEWARE_MATCHERS_CB()
|
||||
@@ -0,0 +1 @@
|
||||
self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
|
||||
@@ -0,0 +1,11 @@
|
||||
self.__BUILD_MANIFEST = {
|
||||
"__rewrites": {
|
||||
"afterFiles": [],
|
||||
"beforeFiles": [],
|
||||
"fallback": []
|
||||
},
|
||||
"sortedPages": [
|
||||
"/_app",
|
||||
"/_error"
|
||||
]
|
||||
};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
|
||||
@@ -0,0 +1 @@
|
||||
self.__MIDDLEWARE_MATCHERS = [];self.__MIDDLEWARE_MATCHERS_CB && self.__MIDDLEWARE_MATCHERS_CB()
|
||||
@@ -0,0 +1 @@
|
||||
self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,16 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
3:I[37457,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
4:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"OutletBoundary"]
|
||||
5:"$Sreact.suspense"
|
||||
8:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"ViewportBoundary"]
|
||||
a:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"MetadataBoundary"]
|
||||
c:I[68027,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default",1]
|
||||
:HL["/cv/_next/static/chunks/3wqi32p1v2fl1.css","style"]
|
||||
7:X
|
||||
0:{"P":null,"c":["","_not-found",""],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{},"$undefined","$undefined",4608]},"$undefined","$undefined",4608]},"$undefined","$undefined",4624],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/cv/_next/static/chunks/3wqi32p1v2fl1.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/cv/_next/static/chunks/0h2qsuyze9ds1.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"dark","children":["$","body",null,{"className":"inter_b2991b2-module__9mH_6q__variable jetbrains_mono_d5591ac2-module__D88TVW__variable antialiased","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L4",null,{"children":["$","$5",null,{"name":"Next.MetadataOutlet","children":"$@6"}]}]]}],{},null,false,null]},null,false,"$7"]},null,false,null],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L8",null,{"children":"$L9"}],["$","div",null,{"hidden":true,"children":["$","$La",null,{"children":["$","$5",null,{"name":"Next.Metadata","children":"$Lb"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$c",[["$","link","0",{"rel":"stylesheet","href":"/cv/_next/static/chunks/3wqi32p1v2fl1.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"r":"$undefined","s":"$undefined","a":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"CxC9DIW0Ude54_bbbG52g"}
|
||||
7:C
|
||||
9:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
||||
d:I[27201,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"IconMark"]
|
||||
6:null
|
||||
b:[["$","title","0",{"children":"FTDT Quant Lab"}],["$","meta","1",{"name":"description","content":"Professional quantitative trading dashboard — Live, Paper, Backtest, Historical"}],["$","link","2",{"rel":"icon","href":"/cv/favicon.ico?favicon.2vob68tjqpejf.ico","sizes":"256x256","type":"image/x-icon"}],["$","$Ld","3",{}]]
|
||||
@@ -0,0 +1,22 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"OutletBoundary"]
|
||||
3:"$Sreact.suspense"
|
||||
7:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"ViewportBoundary"]
|
||||
8:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"MetadataBoundary"]
|
||||
9:I[27201,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"IconMark"]
|
||||
b:I[39756,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
c:I[37457,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
:HL["/cv/_next/static/chunks/3wqi32p1v2fl1.css","style"]
|
||||
6:X
|
||||
e:X
|
||||
e:C
|
||||
0:{"buildId":"CxC9DIW0Ude54_bbbG52g","data":[{"rsc":["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],null,["$","$L2",null,{"children":["$","$3",null,{"name":"Next.MetadataOutlet","children":"$@4"}]}]]}],"isPartial":"$@5","staleTime":"$6","varyParams":null},{"rsc":["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L7",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]}],["$","div",null,{"hidden":true,"children":["$","$L8",null,{"children":["$","$3",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"FTDT Quant Lab"}],["$","meta","1",{"name":"description","content":"Professional quantitative trading dashboard — Live, Paper, Backtest, Historical"}],["$","link","2",{"rel":"icon","href":"/cv/favicon.ico?favicon.2vob68tjqpejf.ico","sizes":"256x256","type":"image/x-icon"}],["$","$L9","3",{}]]}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],"isPartial":"$@a","staleTime":"$6","varyParams":null},{"rsc":["$","$1","c",{"children":[null,["$","$Lb",null,{"parallelRouterKey":"children","template":["$","$Lc",null,{}]}]]}],"isPartial":"$@d","staleTime":"$6","varyParams":"$e"},{"rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/cv/_next/static/chunks/3wqi32p1v2fl1.css","precedence":"next"}],["$","script","script-0",{"src":"/cv/_next/static/chunks/0h2qsuyze9ds1.js","async":true}]],["$","html",null,{"lang":"en","className":"dark","children":["$","body",null,{"className":"inter_b2991b2-module__9mH_6q__variable jetbrains_mono_d5591ac2-module__D88TVW__variable antialiased","children":["$","$Lb",null,{"parallelRouterKey":"children","template":["$","$Lc",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:data:0:rsc:props:children:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:data:0:rsc:props:children:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:data:0:rsc:props:children:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:data:0:rsc:props:children:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],[]]}]}]}]]}],"isPartial":"$@f","staleTime":"$6","varyParams":null}],"isUpgradeableISRFallback":false,"a":"$@10","rootVaryParams":null,"needsRuntimeRequest":"$@11"}
|
||||
4:null
|
||||
6:300
|
||||
11:true
|
||||
6:C
|
||||
10:0
|
||||
a:"$undefined"
|
||||
d:"$undefined"
|
||||
f:"$undefined"
|
||||
5:"$undefined"
|
||||
@@ -0,0 +1,2 @@
|
||||
:HL["/cv/_next/static/chunks/3wqi32p1v2fl1.css","style"]
|
||||
0:{"tree":{"name":"","param":null,"prefetchHints":4176,"slots":{"children":{"name":"/_not-found","param":null,"prefetchHints":4192,"slots":{"children":{"name":"__PAGE__","param":null,"prefetchHints":4256,"slots":null}}}}},"staleTime":300,"buildId":"CxC9DIW0Ude54_bbbG52g"}
|
||||
@@ -0,0 +1,16 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
3:I[37457,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
4:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"OutletBoundary"]
|
||||
5:"$Sreact.suspense"
|
||||
8:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"ViewportBoundary"]
|
||||
a:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"MetadataBoundary"]
|
||||
c:I[68027,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default",1]
|
||||
:HL["/cv/_next/static/chunks/3wqi32p1v2fl1.css","style"]
|
||||
7:X
|
||||
0:{"P":null,"c":["","_not-found",""],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{},"$undefined","$undefined",4608]},"$undefined","$undefined",4608]},"$undefined","$undefined",4624],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/cv/_next/static/chunks/3wqi32p1v2fl1.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/cv/_next/static/chunks/0h2qsuyze9ds1.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"dark","children":["$","body",null,{"className":"inter_b2991b2-module__9mH_6q__variable jetbrains_mono_d5591ac2-module__D88TVW__variable antialiased","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L4",null,{"children":["$","$5",null,{"name":"Next.MetadataOutlet","children":"$@6"}]}]]}],{},null,false,null]},null,false,"$7"]},null,false,null],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L8",null,{"children":"$L9"}],["$","div",null,{"hidden":true,"children":["$","$La",null,{"children":["$","$5",null,{"name":"Next.Metadata","children":"$Lb"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$c",[["$","link","0",{"rel":"stylesheet","href":"/cv/_next/static/chunks/3wqi32p1v2fl1.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"r":"$undefined","s":"$undefined","a":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"CxC9DIW0Ude54_bbbG52g"}
|
||||
7:C
|
||||
9:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
||||
d:I[27201,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"IconMark"]
|
||||
6:null
|
||||
b:[["$","title","0",{"children":"FTDT Quant Lab"}],["$","meta","1",{"name":"description","content":"Professional quantitative trading dashboard — Live, Paper, Backtest, Historical"}],["$","link","2",{"rel":"icon","href":"/cv/favicon.ico?favicon.2vob68tjqpejf.ico","sizes":"256x256","type":"image/x-icon"}],["$","$Ld","3",{}]]
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,20 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
3:I[37457,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default"]
|
||||
4:I[47257,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"ClientPageRoot"]
|
||||
5:I[52683,["/cv/_next/static/chunks/0h2qsuyze9ds1.js","/cv/_next/static/chunks/0oyhbmfwzlodf.js"],"default"]
|
||||
8:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"OutletBoundary"]
|
||||
9:"$Sreact.suspense"
|
||||
b:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"ViewportBoundary"]
|
||||
d:I[97367,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"MetadataBoundary"]
|
||||
f:I[68027,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"default",1]
|
||||
:HL["/cv/_next/static/chunks/3wqi32p1v2fl1.css","style"]
|
||||
:HL["/cv/_next/static/media/70bc3e132a0a741e-s.p.3t6q91iet4nsy.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
:HL["/cv/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
0:{"P":null,"c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{},"$undefined","$undefined",4608]},"$undefined","$undefined",4624],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/cv/_next/static/chunks/3wqi32p1v2fl1.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/cv/_next/static/chunks/0h2qsuyze9ds1.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"dark","children":["$","body",null,{"className":"inter_b2991b2-module__9mH_6q__variable jetbrains_mono_d5591ac2-module__D88TVW__variable antialiased","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[["$","$L4",null,{"Component":"$5","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@6","$@7"]}}],[["$","script","script-0",{"src":"/cv/_next/static/chunks/0oyhbmfwzlodf.js","async":true,"nonce":"$undefined"}]],["$","$L8",null,{"children":["$","$9",null,{"name":"Next.MetadataOutlet","children":"$@a"}]}]]}],{},null,false,null]},null,false,null],["$","$1","h",{"children":[null,["$","$Lb",null,{"children":"$Lc"}],["$","div",null,{"hidden":true,"children":["$","$Ld",null,{"children":["$","$9",null,{"name":"Next.Metadata","children":"$Le"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$f",[["$","link","0",{"rel":"stylesheet","href":"/cv/_next/static/chunks/3wqi32p1v2fl1.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"r":"$undefined","s":"$undefined","a":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"CxC9DIW0Ude54_bbbG52g"}
|
||||
6:{}
|
||||
7:"$0:f:0:1:1:children:0:props:children:0:props:serverProvidedParams:params"
|
||||
c:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
||||
10:I[27201,["/cv/_next/static/chunks/0h2qsuyze9ds1.js"],"IconMark"]
|
||||
a:null
|
||||
e:[["$","title","0",{"children":"FTDT Quant Lab"}],["$","meta","1",{"name":"description","content":"Professional quantitative trading dashboard — Live, Paper, Backtest, Historical"}],["$","link","2",{"rel":"icon","href":"/cv/favicon.ico?favicon.2vob68tjqpejf.ico","sizes":"256x256","type":"image/x-icon"}],["$","$L10","3",{}]]
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |