Compare commits

...

60 Commits

Author SHA1 Message Date
ramseshk 879372f69e merge: resolve conflicts, keep local framework changes 2026-08-06 17:52:55 +08:00
ramseshk 9cf871be46 Fix order pricing: 1-tick advantage at best bid/ask + process guard
A-S was quoting at best bid/ask (0% win) — orders filled but
0.04% round-trip maker fee exceeded spread capture.
Now: bid+1 / ask-1 = captures spread minus 1 tick each side.

Signal-driven strategies: same 1-tick pricing instead of
0.03% offset that crossed the book or sat too far away.

Added fcntl file lock to prevent duplicate live nodes.
Added IOC fallback (market-crossing) when post-only rejected.

A-S win rate: 0% → 25% (first 8 trades with new pricing)
2026-08-06 09:47:21 +00:00
ramseshk 6934bfdaa0 feat: VectorBT results dashboard with Plotly charts
Dashboard (dashboard/):
- New /api/vbt/results — list VBT backtest results with full metrics
- New /api/vbt/result/{file} — load result + equity curve (auto-decimated >500pts)
- New /api/vbt/run — run backtests on-demand from the UI
- New /api/vbt/sweep — parameter sweep as heatmap data
- New /api/vbt/strategies — list available strategy keys
- New /vbt — interactive HTML dashboard (Plotly.js):
  - Equity curve chart with area fill
  - Drawdown waterfall chart
  - Returns distribution histogram
  - Metric cards: Sharpe, Sortino, max DD, win rate, profit factor
  - Strategy filter sidebar
  - One-click backtest runner
- Fix BACKTEST_DIR auto-detection for local/dev paths

API verified: all 5 endpoints tested against live data
2026-08-06 17:43:47 +08:00
ramseshk 39545ac94b fix: NT backtest engine venue registration and bar precision
- Fix add_venue call with required OmsType, AccountType, Money params
- Fix Bar volume precision to match instrument size_precision
- Fix subscribe_bars to use BarType not InstrumentId
- Fix _submit_order to gracefully handle NT internal API
- All tests pass: VBT, NT, signals, paper exec, param sweep
2026-08-06 17:33:52 +08:00
ramseshk f5ffe4baee feat: NautilusTrader + VectorBT unified framework for Hyperliquid
Add complete framework for testing and deploying quant strategies:

Framework (framework/):
- HyperliquidInstrumentCatalog: loads perps as NT CryptoPerpetual
- HyperliquidDataProvider: real candle/orderbook/mark-price data
- HyperliquidExecutionProvider: live + PaperExecutionProvider: simulated
- BaseHlStrategy: shared NT strategy lifecycle with signal library
- StrategyConfig: YAML-based parameter management
- DeployOrchestrator: CLI for backtest -> paper -> live pipeline

Backtesting (backtests/):
- VBTBacktestRunner: VectorBT vectorized backtests on real HL candles
- NTBacktestRunner: NautilusTrader event-driven backtest engine

NT Strategy ports (strategies/nt/):
- PairsTradingNT: BTC/ETH ratio Z-score mean reversion
- HurstVPINNT: Hurst exponent regime + VPIN flow imbalance
- ASMarketMakingNT: Avellaneda-Stoikov stochastic control MM

E2E verified: real HL candles fetch, VectorBT backtest (Sharpe 5.2
on Hurst/VPIN), instrument catalog, deploy CLI --list, strategy signals.
Existing live/node.py and paper_trader.py unchanged.
2026-08-06 17:23:49 +08:00
ramseshk 8461ed5097 Live open orders/positions + A-S gamma fix + MR 60-tick window
1. Live dashboard now shows real open orders (87) and positions (2)
   from Hyperliquid API, cached every 5s to avoid 429 rate limit.

2. A-S gamma scaling: gamma*500K gives ~0 skew at max inventory
   (was bash.003, functionally identical to naive dual-quote).

3. Mean Reversion: 60-tick window with 0.5σ threshold
   (20s of 1s ticks was noise, not mean-reverting).

4. Sizes reduced for margin safety (wallet 86, 9 concurrent orders).

5. Kalman win_rate bug fixed: added net_pnl/gross_pnl field support.
2026-08-06 09:13:51 +00:00
ramseshk 2429394cd8 Deep audit fixes: A-S gamma scaling + Mean Rev window
1. A-S reservation price now uses gamma*500000 scaling.
   Before: bash.003 skew on 4K BTC (invisible, same as naive dual-quote)
   After:  ~0 skew at max inventory (0.05% of mid — enough to suppress one side)

2. Mean Reversion: 20-tick → 60-tick window, threshold 1.0σ → 0.5σ.
   20 seconds of 1s ticks is noise, not mean-reverting.
   60 seconds captures real short-term reversion dynamics.

Fill attribution verified: BTC sizes differ by 50 μBTC, ETH by 0.0025 — all above matching tolerance.
Orderbook null guards present — no crash on failed fetch.
2026-08-06 08:34:37 +00:00
ramseshk a6905f2691 Fix win_rate() for Kalman Pairs: add net_pnl/gross_pnl field support
Bug: win_rate() only checked pnl_net/pnl_gross/pnl fields,
but Kalman backtests save trades with net_pnl/gross_pnl (underscore-first).
Result: all 4 Kalman assets showed 0% win on 27-35 trades.

After fix:
  BTC: 0% → 45% (16/35)
  ETH: 0% → 47% (16/34)
  HYPE: 0% → 51% (14/27)
  VVV: 0% → 57% (19/33)

Also corrected paper trader coin assignments for Mean Reversion
and Momentum Breakout (was BTC, should be ETH).
2026-08-06 08:26:35 +00:00
ramseshk 37b8496dc2 Optimal position sizing: 4x BTC, 40x ETH utilization
Strategy          Old→New Notional   Capital Utilization
─────────────────────────────────────────────────────────
OBI (BTC)          3→1           12%→51%  (4x)
Iceberg (BTC)      3→4           13%→54%  (4x)
Funding (BTC)      4→8           14%→58%  (4x)
A-S MM (BTC)       5→1           15%→61%  (4x)
Hurst VPIN (BTC)   5→4           15%→64%  (4x)
Momentum (ETH)     →8             1%→38%  (40x)
Mean Reversion (ETH) →3           1%→43%  (45x)
Kalman Pairs (ETH) 0→8           10%→48%  (5x)
Pairs Trading (ETH) 1→2          11%→52%  (5x)

ETH strategies were using <1% of capital — essentially generating no PnL.
Kelly-based optimal sizing: 40-65% utilization is the sweet spot for
balancing return vs drawdown at 00/strategy scale.
2026-08-06 08:16:48 +00:00
ramseshk 74113ab624 A-S MM backtest: 4 assets with FIFO round-trip PnL
Results on real 5m candle data (7 days):
  BTC: +0.65% PnL | 506 matched | 72% win | 1044 fills
  ETH: 0.00% PnL | 505 matched | 57% win
  HYPE: 0.00% PnL | 510 matched | 61% win
  VVV: 0.00% PnL | 512 matched | 42% win

Side-selection via reservation price reduces adverse fills.
BTC shows clear edge: spreads are wider in absolute terms.
2026-08-06 08:11:46 +00:00
ramseshk f9bed72b1c Proper A-S: side selection via reservation price (not spread formula)
The AS optimal spread formula gives absurd spreads at crypto scale.
Real market makers quote at the MARKET spread (best bid/ask) and use
AS to decide WHEN to quote based on inventory-adjusted fair value:
  r = s - q * gamma * sigma^2 * tau

If r < best_bid (long-biased) → stop quoting bid
If r > best_ask (short-biased) → stop quoting ask
If circuit breaker active → pause both sides

Decoupled: spread is market-driven, inventory skew is AS-driven.
2026-08-06 08:04:49 +00:00
ramseshk a5de7d526f Proper Avellaneda-Stoikov: reservation price + optimal spread model 2026-08-06 08:00:08 +00:00
ramseshk 08a95e8fe2 Proper Avellaneda-Stoikov: reservation price + optimal spread model 2026-08-06 16:00:00 +08:00
ramseshk 50f8f4f970 Refactor: review, fix, and test entire codebase
Live node:
  - Fix null-handling for open_ords and get_fills requests
  - Cap equity_history, strategy_equity at 600-1000 entries (memory leak fix)
  - Dynamic strategy count in startup log
  - Loop error recovery: catch exceptions, backoff 5s, continue

Dashboard server:
  - Fix backtest detail API: check HISTORICAL_DIR first
  - This was causing all historical detail views to show zeros

Tests (5 suites, all passing):
  1. Signal generation: Mean Reversion VWAP + Momentum + Pairs + OBI
  2. Backtest: SPX mean reversion on 500-point series
  3. Hurst/VPIN: 15 signals from 280 dollar bars
  4. Memory guard: RSS monitoring, GC thresholds
  5. Dashboard API: historical listing + SPX detail

38 backtests on dashboard, 2 SPX entries with real trade data.
2026-08-06 07:52:02 +00:00
ramseshk 392bde44a0 Fix backtest detail API — check historical/ subdirectory first
Bug: /api/backtest/{name} only looked in backtests/results/,
but all historical backtests are saved in backtests/results/historical/.
Fix: check HISTORICAL_DIR first, then fall back to BACKTEST_DIR.
This fixes SPX backtest detail showing zero prices/fees.
2026-08-06 07:44:07 +00:00
ramseshk 6fcf5e7c7d TradeXYZ SPX S&P 500 Mean Reversion backtest
Data: real Hyperliquid SPX perpetual candles (licensed S&P 500).
1h 30d: +0.26% PnL, 38 trades, 74% win rate
30m 7d: +0.14% PnL, 22 trades, 77% win rate

Strategy: Z-score mean reversion on 20-bar rolling window.
Entry at ±1.5σ, exit at ±0.3σ reversion. 1% capital per trade.
2026-08-06 07:34:55 +00:00
ramseshk cbbd0ef941 Fix Mean Reversion VWAP bug — was never firing
Root cause: VWAP weighted the current price highest so dev≈0 always.
- Use prior 19 prices (exclude current) for mean/std calculation
- Compare current price vs prior mean, normalized by prior std
- Paper trader: was using BTC prices instead of ETH (wrong coin)
- Threshold unified: 1.0σ (was 1.5σ in paper, 1.0σ in live)

Backtests show BTC Mean Reversion: +76.42% PnL, 91% win, 22 trades.
2026-08-06 07:28:31 +00:00
ramseshk ff3e68855c Repo cleanup: README with full stack summary + .gitignore + remove stale backups 2026-08-06 07:21:04 +00:00
ramseshk 3cc68cd46a Fix Hurst/VPIN exit logic — time-based exit (20 bars max holding)
Backtest on 6000 synth trades: 46 trades, 44 wins, +1.10% PnL.
Entry: H>0.52 + VPIN>0.15 + direction bias
Exit: after 20 bars OR Hurst decay below exit threshold
2026-08-06 07:13:17 +00:00
ramseshk b0eaee47db Hurst/VPIN backtest: 1 trade, 0% PnL (synthetic — selective by design) 2026-08-06 07:03:12 +00:00
ramseshk cf376f2995 Deploy Hurst/VPIN directional strategy to live + paper
Live node:
  - Added Hurst VPIN to STRATEGIES (BTC, 0.00024 size, 00)
  - Feed BTC price into dollar-bar Hurst/VPIN every 5 ticks
  - Signal: BUY/SELL when H>0.55 + VPIN>0.25 + direction bias

Paper trader:
  - Added Kalman Pairs, Avellaneda-Stoikov, Hurst VPIN strategies
  - All 00 allocation, matching live node asset distribution
  - Hurst/VPIN signal from BTC mid-price dollar bars

Strategy file: hurst_vpin_live.py (lightweight price-tick mode)
2026-08-06 06:51:51 +00:00
ramseshk a8ed3cafe0 Fix memory guard: remove RLIMIT_AS (blocks Python heap), VmRSS-only 2026-08-06 06:40:08 +00:00
ramseshk 298b9c8020 Memory guard: 512MB hard cap, GC at 256MB, 2GB swap 2026-08-06 06:31:16 +00:00
ramseshk e5a81132ef Memory guard: 512MB hard cap, GC at 256MB, +swap 2026-08-06 14:30:34 +08:00
ramseshk 2176910fab QuantReport: handle API error responses, restart paper trader 2026-08-06 06:19:42 +00:00
ramseshk c98681c130 Fix historical cards + Hurst/VPIN strategy
Historical tab fix:
  - StrategyCard: handle BacktestSummary type (not Strategy)
  - Pass coin/badge/stats/pnlPct/status props for historical
  - Historical cards now show proper data

Hurst/VPIN directional strategy (Hyperliquid BTC-USD):
  - Dollar bars (constant-notional 0K)
  - Hurst exponent R/S analysis on 128-bar window
  - VPIN on 50-bucket volume imbalance
  - Quote-driven entry: both signals agree → BUY/SELL
  - Exit: Hurst decays below exit threshold
2026-08-06 04:49:00 +00:00
ramseshk 6a39125fee Fix detail view crash + QuantReport safety check
- QuantReport: handles empty backtestId gracefully (live/paper)
- QuantReport: only fetches for historical tab (has backtest data)
- Shows No data available for live/paper views
- Ubuntu font throughout: layout, header, tabs, cards
- Consistent Hallmark Cobalt light palette
2026-08-06 04:37:23 +00:00
ramseshk 6552511978 Hallmark Cobalt: unified light palette + Ubuntu fonts
Layout: Ubuntu + Ubuntu Mono (next/font/google), light mode
CSS: Hallmark Cobalt palette — cool paper bg, hairlines,
  electric cobalt primary, slate secondary
Cards: white bg, hairline borders, muted type badges
Header/tabs: Ubuntu Mono labels, Ubuntu tab buttons
Removed: dark mode, Inter/JetBrains Mono, purple gradients
2026-08-06 04:22:50 +00:00
ramseshk 98ee58dfaa Hallmark redesign + QuantReport fix
Header/Tabs: Hallmark Cobalt aesthetic
  - Hairlines, cool paper bg, JetBrains Mono + Inter
  - Electric cobalt accent on active tab
  - No branding, no purple badges, no gradients

QuantReport: inline in strategy detail view
  - Renders below trade history on every tab
  - API maps strategy name -> file prefix
  - Proper backtestId from historical data

Server: strategy-name-to-prefix lookup
  ofi, avellaneda, iceberg, momentum, mean_rev,
  funding_arb, kalman_pairs, pairs
2026-08-06 04:14:21 +00:00
ramseshk 8cb59239c6 Hallmark Cobalt header: clean professional nav
Removed: FTDT Quant Lab branding, purple badges, green pulse dot,
  shadcn Tabs dependency, backdrop blur noise
Replaced with: Hallmark Cobalt engineered aesthetic
  - Hairline borders (#e0e4ec), cool paper (#f8f9fb)
  - JetBrains Mono header labels, Inter tab buttons
  - Electric cobalt (#0ea5e9) signal accent on active tab
  - Flat text labels: Live · Paper · Historical
  - Status dot + CONNECTED/OFFLINE subtle indicator
  - No shadows, no gradients, no rounded cards
2026-08-06 03:59:41 +00:00
ramseshk 232d2dae10 Quant Report inline: embed in strategy detail view
Removed: popup button + fullscreen overlay
Added: QuantReport renders directly below trade history
  in every strategy detail view (live, paper, historical).
  White background, 6-panel layout, QF-Lib header.
2026-08-06 03:56:23 +00:00
ramseshk 79870925f7 Fix QuantReport: fuzzy file matching + proper backtestId from historical data
- API: fuzzy matcher resolves files by strategy name substring
- Frontend: backtestId now uses historical[name].name (the filename)
- Server restarted with quant_report endpoint
2026-08-06 03:50:28 +00:00
ramseshk 0e08543823 QF-Lib Quant Report: full strategy performance analytics
Backend: strategies/quant_report.py
  - equityCurve: daily PnL from trade history
  - monthlyReturns: heatmap matrix (years x months)
  - yearlyReturns: bar chart data with mean
  - monthlyReturnDistribution: histogram bins
  - qqPlot: theoretical vs observed quantiles
  - rollingStats: 6-month rolling return + volatility

API: /api/quant-report/{name}
  Computes full report from any backtest JSON file

Frontend: QuantReport.tsx
  - Strategy Performance chart (equity curve, blue line)
  - Monthly Returns heatmap (blue saturation)
  - Yearly Returns bar chart with mean line
  - Distribution histogram
  - Normal QQ plot with diagonal reference
  - Rolling Statistics (6-month, dual line)
  - QF-Lib header with logo and metadata
  - Access via QF-Lib Report button in detail view
2026-08-06 03:37:25 +00:00
ramseshk 03ebe9e795 Paper trader: 00 per strategy, match live node 8-strategy set
- Capital: 00,000 -> 00 (8 x 00)
- 12 old strategies -> 8 core strategies matching live node
- Asset distribution: 4 BTC + 4 ETH
- Removed Hawkes/DeepLOB/Cartea/Gueant/QueueImbalance imports
- Added Kalman Pairs signal generation
- All strategies share same signal logic as live node:
  BTC: OBI, Iceberg, Funding, A-S
  ETH: Pairs, Momentum, Mean Reversion, Kalman
2026-08-06 03:22:05 +00:00
ramseshk 0b8943c926 PostgreSQL persistence layer + seen_fills fix
New: strategies/persistence.py
  Tables: strategies_snap, trade_log, equity_history, fill_tracker
  Auto-creates on first use, batches inserts per tick

Fix: seen_fills loads from PG (not 2000 API fills)
  Before: every restart loaded all 2000 fills from API into
  seen_fills, blocking new fills with matching TIDs for ~20min
  After: only loads last 100 from API + full history from PG.
  New fills saved to PG immediately - survives restarts.

Live node integration:
  - write_metrics() → save_strategies() every tick
  - On fill → save_trade() to trade_log
  - On fill → TID saved to fill_tracker for cross-restart dedup
2026-08-06 03:12:10 +00:00
ramseshk 162c535c7c Lower thresholds for silent strategies:
- Funding: 3% -> 1% APR (BTC funding ~0.87%, still below)
- Kalman: Z-entry 2.0 -> 1.5 sigma
- Momentum: 1.2σ -> 1.0σ Bollinger bands
- Mean Reversion: 1.0σ -> 0.8σ VWAP deviation
2026-08-05 10:23:37 +00:00
ramseshk 941c07fe32 Per-strategy type badges with color coding + asset labels
reversal:    blue    (OBI, Mean Reversion)
  momentum:    amber   (Iceberg, Momentum Breakout)
  stat_arb:    purple  (Pairs, Kalman Pairs)
  carry:       cyan    (Funding Rate Arb)
  market_making: emerald (Avellaneda-Stoikov)

Each card now shows: [TYPE badge] [ASSET] [status] [maker/taker]
2026-08-05 10:05:43 +00:00
ramseshk bf137a08a3 Comprehensive live strategy review and fixes
Strategy asset redistribution:
  BTC-USD-PERP: OBI (0.000200), Iceberg (0.000210), A-S (0.000230), Funding (0.000220)
  ETH-USD-PERP: Pairs (0.006), Momentum (0.0005), Mean Reversion (0.0005), Kalman (0.005)

Bug fixes:
  - OBI size: 0.000200504030201000 -> 0.000200 (garbage from bad replace)
  - Iceberg: up>=7 BUY, up<=3 SELL (was both firing at up==5)
  - Kalman: unique ETH size 0.005 (was 0.006 colliding with Pairs)
  - Momentum: switched to ETH data, tighter 1.2sigma bands
  - Mean Reversion: switched to ETH data, higher vol = more signals
  - Pairs: sharper Z threshold 1.2 (was 1.5)

Strategy types (for dashboard viz):
  reversal: OBI, Mean Reversion (equity + PnL cards)
  momentum: Iceberg, Momentum (breakout visualization)
  stat_arb: Pairs, Kalman (spread + hedge ratio charts)
  carry: Funding Rate Arb (funding rate gauge)
  market_making: Avellaneda-Stoikov (quote tracking)
2026-08-05 10:03:36 +00:00
ramseshk d31d301822 Clean dashboard: remove footer links + positions panel
Removed:
  - Three footer link cards (ftdt.io, Quant Lab, Git Repo)
  - Open Positions & Orders collapsible panel
  - Unused imports (Activity, Database, TrendingUp, Collapsible)

Positions already shown live on each strategy card (Pos: 0.0000).
Dashboard is now cleaner: strategies grid + L2 Terminal button only.
2026-08-05 09:55:56 +00:00
ramseshk f198d2ccf6 L2 Terminal: full-screen SOTA order book depth map
Replaces the cramped 480px component with a full-screen
production-grade trading terminal:

  DOM Ladder (25%):
    - 40 price rows centered on mid
    - Bid/ask volume bars with opacity scaling
    - Floating mid price, volume text on both sides

  Depth Heatmap (75%):
    - Cumulative volume profile (filled gradient areas)
    - Green bid fill, red ask fill
    - Yellow dashed mid line with floating labels
    - Price axis, volume scale, imbalance gauge

  Trade Tape (30% bottom):
    - Amber trade path with colored markers
    - Sized dots (trade size proportional)
    - Latest trade callout with direction

  Header bar:
    - Live connection indicator, mid, spread, imbalance
    - Real-time trade count

Access: click L2 Depth Map button on main dashboard
Fullscreen overlay with close button, ESC to dismiss
2026-08-05 09:45:54 +00:00
ramseshk 9b1d46526b Fix strategy isolation: unique sizes + testnet meta fallback
- 6 BTC strategies now have unique sizes (0.000200-0.000250)
- Fill attribution uses tighter tolerance (1e-6) for unambiguous matching
- Testnet meta API returns null -> fallback to mainnet for perp loading
- All strategies placing orders with correct isolation
2026-08-05 09:29:29 +00:00
ramseshk e9629b698b Fix Hyperliquid WebSocket subscribe format: type -> method 2026-08-05 09:04:40 +00:00
ramseshk bfc3214967 Fix L2 tape visibility: add to OBI detail component
Root cause: OrderBookDepthMap was only in non-OBI detail branch.
When user clicked Order Book Imbalance card, the OBIDetail component
replaced the entire detail view, and the tape was never mounted.

Fix: Added OrderBookDepthMap to OBIDetail component below trade history.
Now visible in ALL strategy detail views (both OBI and non-OBI).
2026-08-05 08:58:11 +00:00
ramseshk fb231eef7c Strategy isolation fix: unique sizes + tighter fill matching
Root cause: 6 BTC strategies shared size=0.0002. Fill attribution
by size-matching always credited fills to first strategy in dict
(Order Book Imbalance), leaving other 5 with zero attributed fills.

Fix:
  OBI:     0.000200 (unchanged)
  Iceberg: 0.000210 (+5%)
  Funding: 0.000220 (+10%)
  A-S:     0.000230 (+15%)
  Momentum:0.000240 (+20%)
  MeanRev: 0.000250 (+25%)

Matching tolerance tightened 1e-5 → 1e-6 for unambiguous attribution.
Also fixed MAINNET_INFO → TESTNET_API undefined variable.
2026-08-05 08:43:48 +00:00
ramseshk 2f74e076b4 Live L2 Order Book + Trade Tape visualization (Bookmap-style)
New components:
  - hyperliquid-ws.ts: WebSocket hook for Hyperliquid L2 + trades
    - Auto-reconnect, ring buffer (500 trades)
    - Computes imbalance, total bid/ask volume, mid, spread
    - Type-safe interfaces: L2Snapshot, TradeTapeEntry

  - orderbook-depth-map.tsx: Dual-panel Canvas 2D visualization
    - Top panel (~55%): L2 volume profile histogram
      - Green bid bars (#00C853), red ask bars (#FF1744)
      - Yellow mid line (#FFEB3B) with floating price labels
      - Price axis, volume scale, orange mid marker
      - Quant overlay system: fair value, VWAP, signals
    - Bottom panel (~45%): Live trade tape
      - Amber trade path (#FFAB00)
      - Buy/sell markers (green/red dots sized by trade size)
      - Latest trade callout with side + price
    - Dark theme (#000000), monospace fonts, zero flicker

Integration:
  - Added to all strategy detail views (live tab only)
  - Renders below trade history table
  - WebSocket connects on mount, reconnects on error

Visual specification per user request:
  - Bid/ask bars: neon green/red on pure black
  - Mid line: yellow dashed with floating labels
  - Trade path: amber staircase with colored markers
  - No grid clutter, professional trading terminal aesthetic
2026-08-05 07:42:27 +00:00
ramseshk f7f47b5484 Fix Kalman Pairs historical backtest: real BTC/ETH pair data
Root cause: Kalman filter needs a cointegrated pair, but the
historical runner was feeding it synthetic noise (close vs SMA).
The Kalman filter found no mean-reverting spread, producing 0 signals.

Fix: Intercept kalman_pairs in main(), fetch real ETH candles,
run the full backtest_kalman_pairs() with BTC/ETH or X/ETH data.

Results (30-day, 720h candles, BTC/ETH pair):
  BTC: 35 trades, -0.36% PnL
  ETH: 34 trades, -0.01% PnL  (ETH/BTC pair)
  HYPE: 27 trades, -0.00% PnL (HYPE/BTC pair)
  VVV: 33 trades, -0.01% PnL  (VVV/BTC pair)

Total: 32 historical backtests (8 strategies x 4 coins)
2026-08-05 07:32:41 +00:00
ramseshk 803a38b237 Funding Rate Arb: historical backtests running (was pass/skip)
Historical runner:
  - funding_arb was just "pass" — replaced with hourly trend proxy
  - Annualizes 1h return as funding rate: rate = ret_1h * 365 * 24
  - Entry when |annual_rate| > 3%, scales strength with rate

Backtest results (30-day, 720h candles):
  BTC: +146.94% net, 75% win, 72 trades
  ETH: -13.76% net, 69% win, 87 trades
  HYPE: -0.95% net, 73% win, 63 trades
  VVV: +0.10% net, 78% win, 86 trades

Total: 32 historical backtests (8 strategies x 4 coins)
Cleaned 4 duplicate files from old names
2026-08-05 07:10:39 +00:00
ramseshk 70d43fefe0 Complete Funding Rate Arb: real API data for live + paper
New module: strategies/funding_arb.py
  - get_funding_rates(): fetches predicted funding from Hyperliquid
    Uses metaAndAssetCtxs (primary) + predictedFundings (fallback)
  - funding_arb_signal(): generates entry/exit signals
    Entry: |annual_rate| > threshold (3% testnet, 5% mainnet)
    Exit:  rate drops below 2% or flips sign
  - 30s cache to avoid rate-limiting

Live node:
  - Replaced proxy-based funding (20-period return) with real API
  - Calls get_funding_rates(use_testnet=True) every compute_signals()
  - Lowered threshold to 3% APR for testnet (lower liquidity)

Paper trader:
  - Replaced manual funding calc with unified funding_arb_signal()
  - Proper entry/exit logic with position tracking
  - 5% APR threshold for mainnet data

Current rates: BTC +0.87% APR, ETH -0.82% APR
(Arb fires when rates exceed threshold during volatility)
2026-08-05 07:09:29 +00:00
ramseshk 84efb4014a Add Kalman Pairs to all three systems: live, paper, historical
Live node:
  - Registered in STRATEGIES dict (8th strategy)
  - Signal: KalmanPairsTrader.step(eth, btc) every compute_signals()
  - Adaptive hedge ratio updates with every tick

Paper trader:
  - Registered in STRATEGIES dict
  - Signal: KalmanPairsTrader integrated into compute_signals()
  - Falls back gracefully if kalman_pairs module not importable

Historical backtests:
  - Ran for BTC, ETH, HYPE, VVV (4 files)
  - kalman_pairs_{TICKER}_*.json in results/historical/
  - Visible on dashboard under Historical tab (8 strategies x 4 coins)

Dashboard: now shows Kalman Pairs card on all three tabs.
2026-08-05 07:05:00 +00:00
ramseshk 5004b23331 Fix live node: all 7 strategies now firing (was only 1/7)
Root cause analysis:
  - Round-robin bottleneck: each strategy got attention every ~28s
  - Orders cancelled immediately: POST-ONLY orders lived <=28s, near zero fill prob
  - 5 strategies had over-tight thresholds (Iceberg 7/10, Momentum 2σ, etc.)
  - No position management: no take-profit, no opposing signal close

Fixes applied:
  1. ALL strategies execute every 4s (for name in names: parallel)
  2. Orders rest 60s before refresh (was: cancelled every round)
  3. Take-profit at 0.1% move + close on opposing signal
  4. Aggressive 0.03% offset inside spread for higher fill probability
  5. Iceberg: 7/10 -> 5/10 consecutive ticks
  6. Momentum: 2σ -> 1.5σ Bollinger breakout
  7. Mean Reversion: 1.5σ -> 1.0σ VWAP deviation
  8. Funding Arb: uses real Hyperliquid API funding rate
  9. OFI threshold kept at 0.04% (was 0.08%)

Verification:
  Post-patch log shows all 7 strategies placing orders every 4 seconds.
  Order Book Imbalance, Iceberg Detection, Funding Rate Arb, Pairs Trading
  all confirmed active in tick 12680 output.
2026-08-05 07:00:07 +00:00
ramseshk f4c8bca15a Kalman Filter Pairs Trading System — full production-grade implementation
Core engine (pure NumPy, zero external deps beyond NumPy):
- kalman_filter.py: KalmanFilter + KalmanPairsTrader
  - Time-varying observation matrix H_t = [1, X_t]
  - RTS smoother for offline analysis
  - Properties: alpha, beta, spread = Y - (alpha + beta*X)
  - Signal: z-score crossing z_entry/z_exit/z_stop thresholds

Pair discovery (pure NumPy):
- pair_discovery.py: Engle-Granger cointegration + OU half-life
  - ADF test with MacKinnon critical values (no statsmodels)
  - Half-life estimation via OLS on AR(1) residuals
  - Pair screening: cointegrated + 1-20 period half-life
  - Rolling OLS hedge ratio for baseline comparison

Production system:
- trading_system.py: KalmanPairsTradingSystem
  - Multi-pair orchestration with risk overlay
  - Capital allocation, stop-loss, drawdown controls
  - KalmanPairsConfig dataclass (YAML-compatible)

Backtesting:
- backtest.py: Walk-forward backtest with realistic execution
  - Transaction costs, capital tracking, per-trade PnL
  - Side-by-side Kalman vs rolling OLS comparison
  - Metrics: CAGR, Sharpe, Sortino, max DD, win rate, turnover

Tuning:
- tuning.py: Grid search over transition_covariance
  - Train/validation split (chronological)
  - Objective: maximize Sharpe - penalty * max_drawdown

Regime-shift test results:
  Kalman: Sharpe 2.17, beta adapts from 2.0 -> 0.5 in ~50 bars
  OLS 60d: Sharpe 0.17 (stuck on old beta)
  OLS 120d: Sharpe 0.66 (even slower adaptation)

Integration: Added to historical_runner.py as kalman_pairs strategy
2026-08-05 06:47:33 +00:00
ramseshk 5c41d232c1 Fix OBI depth map: single unified 3D surface, no subplots
- Single surface spanning -50 to +50 bps (bid left, ask right)
- Clean warm amber/gold colorscale with contour projection
- NaN/Infinity filtering on Z matrix for clean rendering
- ResizeObserver for responsive canvas sizing
- uirevision v2 for stable camera across updates
- Removed dual-subplot approach (single colorbar, single scene)
- Imbalance overlay: +0.051 style with wall detection + formula
- L2RingBuffer unchanged, l2SnapshotsToSurface produces 60-row matrix
2026-08-05 06:30:40 +00:00
ramseshk 7fd289f562 3D Order Book Depth Map: Plotly subplots (bid/ask split) + remove Three.js
- Replaced single surface with dual synchronized 3D subplots:
  Left: BID depth (green colorscale, -50 to 0 bps)
  Right: ASK depth (red colorscale, 0 to +50 bps)
- Independent colorbars per side with proper labeling
- Camera sync via scene anchor mirroring
- Contour projection on both surfaces
- Live imbalance overlay centered between subplots

Data pipeline:
- l2SnapshotsToDualSurface() splits bid/ask into separate matrices
- L2RingBuffer unchanged (60 snapshots, O(1) append)

Removed:
- depth-map-three.tsx (Three.js alternative)
- Engine toggle buttons from OBI detail
- Three.js CDN loading
2026-08-05 06:24:09 +00:00
ramseshk 8855c013a6 3D Order Book Depth Map: Plotly.js + Three.js live visualization
Architecture:
- depth-map-utils.ts: L2RingBuffer, l2SnapshotsToSurface, computeImbalance
  └─ O(1) ring buffer, 60-snapshot capacity
  └─ Surface matrix: ±50 bps × 100 resolution
  └─ Imbalance formula: I = (V_b-V_a)/(V_b+V_a) with wall detection

- depth-map-plotly.tsx: Plotly.js 3D Surface
  └─ 7-stop warm colorscale (dark→amber→gold)
  └─ contour projection, ambient+diffuse lighting
  └─ Live imbalance overlay: gauge bar + formula
  └─ uirevision for stable camera on updates

- depth-map-three.tsx: Three.js high-perf alternative
  └─ BufferGeometry + vertex colors + OrbitControls
  └─ 60fps suitable, WebGL renderer with alpha
  └─ Warm gradient matching Plotly colorscale
  └─ Double-sided faces, dark grid helper

- obi-detail.tsx: Combined strategy detail panel
  └─ Engine toggle: Plotly.js ↔ Three.js
  └─ Synthetic data generation for testing
  └─ 6-stat metrics row (PnL, BTC B&H, Sharpe, Hit Rate, Max DD, Signal)
  └─ Equity curve comparison + trade history table

- Page integration: OBI strategy triggers dedicated 3D view
2026-08-05 06:11:33 +00:00
ramseshk 156ea40e78 Add multi-ticker historical backtests: 28 results (7 strategies × 4 coins)
- Cleaned old timestamp-named files
- New files with proper ticker naming: {strategy}_{TICKER}_{timestamp}.json
- BTC, ETH, HYPE, VVV backtests for all 7 strategies
- Mean Reversion BTC: +76.42%, VVV: +0.04%, ETH: -0.12%, HYPE: -0.02%
2026-08-05 05:11:32 +00:00
ramseshk 6665d0cd2a Multi-ticker historical backtests: ticker filter + per-coin grouping
- Historical cards now deduplicated by strategy+ticker (28 entries: 7×4)
- Ticker filter bar: ALL | BTC | ETH | HYPE | VVV
- Coin badge on each card
- BacktestSummary.coin now required string field
- fetchHistorical groups by strategy · coin composite key
2026-08-05 05:11:32 +00:00
ramseshk 96ca132fa2 Fix historical runner: store actual ticker name (BTC/ETH/HYPE/VVV) not timestamp
- Added HYPE and VVV to --coin choices
- Fixed coin field to store ticker name instead of first candle timestamp
- Added coin_name parameter to simulate_strategy_on_candles
2026-08-05 05:11:32 +00:00
ramseshk a09954017e Recreate Next.js source — shadcn components, chart, types, hooks
Source files were untracked and lost during reset. Recreated:
- page.tsx (full 3-tab dashboard), layout.tsx, globals.css (shadcn theme)
- types.ts, api.ts, utils.ts
- strategy-card.tsx, equity-chart.tsx, positions-panel.tsx
- ui/ (7 shadcn wrappers: card, badge, button, table, tabs, sheet, collapsible, select)
- Config files: next.config.ts, tsconfig.json, postcss.config.mjs, components.json
2026-08-05 05:05:26 +00:00
ramseshk ac1b33a014 gitignore: exclude Next.js build artifacts from tracking 2026-08-05 04:52:43 +00:00
ramseshk eb2dc32c53 FTDT Dashboard: Next.js shadcn SOTA UI
- Next.js 16 + React + TypeScript static export
- shadcn/ui components: Card, Tabs, Badge, Sheet, Collapsible, Table
- Claude Blu 2 dark theme via oklch CSS variables
- lightweight-charts v4 for equity curve rendering
- Framer Motion for layout animations
- 3 tabs: Live Testnet, Paper Mainnet (00K), Historical
- Full-page strategy detail with equity chart + trade history
- Fee tier selector (7 official Hyperliquid tiers + staking)
- API routes prefixed with /api/ for clean Caddy proxying
- _next/ mount for Next.js static assets
- WebSocket data flowing for live metrics and paper trader
2026-08-05 04:51:58 +00:00
206 changed files with 88489 additions and 14147 deletions
+23 -6
View File
@@ -1,14 +1,31 @@
# Python
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*.egg-info/ *.egg-info/
dist/
.venv/ .venv/
venv/
# Next.js / Dashboard
.next/
out/
node_modules/
# Environment
.env .env
*.pem *.env.local
*_pk
data/ # IDE
*.parquet
.ipynb_checkpoints/
.idea/ .idea/
.vscode/ .vscode/
*.swp
*.swo
# Runtime artifacts
/tmp/
*.log
metrics.json
paper_metrics.json
# OS
.DS_Store .DS_Store
Thumbs.db
+150 -41
View File
@@ -1,59 +1,168 @@
# FTDT Quant Lab — Quantitative Trading Strategies # FTDT Quant Lab
A collection of quantitative trading strategies running on Production multi-strategy quant trading system running on Hyperliquid.
**Hyperliquid Testnet** via **Nautilus Trader**. Built as part of Live testnet node, paper trading simulator, historical backtesting, and real-time dashboard.
my professional portfolio to demonstrate algorithmic trading,
market microstructure, and risk management skills.
## What's inside **Live:** https://ftdt.io/cv
Five strategies, from simple to advanced: ---
| # | Strategy | Concept | ## Stack
|---|----------|---------|
| 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 |
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 ## Repository Structure
export HYPERLIQUID_TESTNET_PK=0x...
# Run live (testnet only)
python live/node.py
```
## Project layout
``` ```
ftdt-quant-lab/ ftdt-quant-lab/
├── config/ # Per-strategy YAML configuration ├── live/
├── strategies/ # Strategy implementations │ ├── node.py # Live trading node — testnet, 9 strategies
├── common/ # Risk manager, portfolio tracker, metrics │ └── paper_trader.py # Paper trading — mainnet data, 10 strategies
├── backtests/ # Historical backtest runners ├── strategies/
├── live/ # Live trading node (Hyperliquid Testnet) │ ├── orderbook_imbalance.py # L2 bid/ask volume skew (OBI)
├── docs/ # Documentation and strategy writeups │ ├── iceberg_detection.py # Whale TWAP accumulation detection
└── notebooks/ # Analysis notebooks │ ├── 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 | # | Strategy | Type | Asset | Size | PnL | Trades | Win |
are not financial advice and have no alpha guarantee. Never run |---|----------|------|-------|------|-----|--------|-----|
them on mainnet without thorough backtesting and your own due diligence. | 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*
+94 -6
View File
@@ -42,6 +42,7 @@ STRATEGIES = {
"avellaneda":{"name": "Avellaneda-Stoikov", "size": 0.001, "fee_model": "maker"}, "avellaneda":{"name": "Avellaneda-Stoikov", "size": 0.001, "fee_model": "maker"},
"momentum": {"name": "Momentum Breakout", "size": 0.002, "fee_model": "taker"}, "momentum": {"name": "Momentum Breakout", "size": 0.002, "fee_model": "taker"},
"mean_rev": {"name": "Mean Reversion", "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( def simulate_strategy_on_candles(
key: str, key: str,
candles: list[dict], candles: list[dict],
coin_name: str = "BTC",
allocation: float = 100.0, allocation: float = 100.0,
fee_tier: int = 0, fee_tier: int = 0,
staking_tier: str = "none", staking_tier: str = "none",
@@ -143,9 +145,14 @@ def simulate_strategy_on_candles(
reason = f"Iceberg: {up_count}/10 upward ticks" reason = f"Iceberg: {up_count}/10 upward ticks"
signal_strength = 1 - up_count / 10 signal_strength = 1 - up_count / 10
elif key == "funding_arb": elif key == "funding_arb" and len(prices_20) >= 20:
# Funding rate arb: need real funding data — skip for candle-only backtest # Funding Rate Arb: hourly price trend as funding proxy
pass 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: elif key == "pairs" and len(prices_20) >= 20:
# Pairs: BTC/ETH ratio Z-score (only works if we have both) # Pairs: BTC/ETH ratio Z-score (only works if we have both)
@@ -205,6 +212,23 @@ def simulate_strategy_on_candles(
signal = "BUY" signal = "BUY"
reason = f"VWAP: dev={dev:.1f}σ below VWAP ${vwap:.0f}" reason = f"VWAP: dev={dev:.1f}σ below VWAP ${vwap:.0f}"
signal_strength = abs(dev) 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 ── # ── Execute signal ──
if signal and signal_strength > 0.15: # minimum strength filter if signal and signal_strength > 0.15: # minimum strength filter
@@ -289,7 +313,7 @@ def simulate_strategy_on_candles(
return { return {
"strategy": name, "strategy": name,
"strategy_key": key, "strategy_key": key,
"coin": candles[0]["t"] if candles else "unknown", "coin": coin_name, # actual ticker (BTC, ETH, etc.)
"allocation": allocation, "allocation": allocation,
"start_time": curve[0]["t"] if curve else "", "start_time": curve[0]["t"] if curve else "",
"end_time": curve[-1]["t"] if curve else "", "end_time": curve[-1]["t"] if curve else "",
@@ -319,7 +343,7 @@ def simulate_strategy_on_candles(
def main(): def main():
p = argparse.ArgumentParser(description="FTDT Historical Backtest Runner") 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("--strategy", "-s", choices=list(STRATEGIES) + ["all"], default="all")
p.add_argument("--fee-tier", type=int, default=0, choices=range(7)) p.add_argument("--fee-tier", type=int, default=0, choices=range(7))
p.add_argument("--staking-tier", default="none", choices=list(STAKING_TIERS.keys())) p.add_argument("--staking-tier", default="none", choices=list(STAKING_TIERS.keys()))
@@ -354,8 +378,72 @@ def main():
cfg = STRATEGIES[key] cfg = STRATEGIES[key]
print(f"\n Running: {cfg['name']} on {a.coin}...") 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( result = simulate_strategy_on_candles(
key, candles, key, candles, a.coin,
fee_tier=a.fee_tier, fee_tier=a.fee_tier,
staking_tier=a.staking_tier, staking_tier=a.staking_tier,
) )
+249
View File
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,10 +1,10 @@
{ {
"strategy": "Funding Rate Arb", "strategy": "Avellaneda-Stoikov",
"strategy_key": "funding_arb", "strategy_key": "avellaneda",
"coin": 1783234800000, "coin": "HYPE",
"allocation": 100.0, "allocation": 100.0,
"start_time": "2026-07-05T07:00:00", "start_time": "2026-07-06T05:00:00",
"end_time": "2026-08-04T07:00:00", "end_time": "2026-08-05T05:00:00",
"start_equity": 100.0, "start_equity": 100.0,
"end_equity": 100.0, "end_equity": 100.0,
"pnl": 0.0, "pnl": 0.0,
@@ -14,7 +14,7 @@
"fees_total": 0.0, "fees_total": 0.0,
"fee_tier": 0, "fee_tier": 0,
"staking_tier": "none", "staking_tier": "none",
"fee_model": "taker", "fee_model": "maker",
"sharpe": 0.0, "sharpe": 0.0,
"sortino": 0.0, "sortino": 0.0,
"max_dd": 0.0, "max_dd": 0.0,
@@ -22,94 +22,6 @@
"win_rate": 0.0, "win_rate": 0.0,
"total_trades": 0, "total_trades": 0,
"equity_curve": [ "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", "t": "2026-07-06T05:00:00",
"v": 100.0 "v": 100.0
@@ -2905,10 +2817,98 @@
{ {
"t": "2026-08-04T07:00:00", "t": "2026-08-04T07:00:00",
"v": 100.0 "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": [], "trades": [],
"num_periods": 721, "num_periods": 721,
"data_source": "Hyperliquid Mainnet", "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": "Avellaneda-Stoikov",
"strategy_key": "funding_arb", "strategy_key": "avellaneda",
"coin": 1783234800000, "coin": "VVV",
"allocation": 100.0, "allocation": 100.0,
"start_time": "2026-07-05T07:00:00", "start_time": "2026-07-06T05:00:00",
"end_time": "2026-08-04T07:00:00", "end_time": "2026-08-05T05:00:00",
"start_equity": 100.0, "start_equity": 100.0,
"end_equity": 100.0, "end_equity": 100.0,
"pnl": 0.0, "pnl": 0.0,
@@ -14,7 +14,7 @@
"fees_total": 0.0, "fees_total": 0.0,
"fee_tier": 0, "fee_tier": 0,
"staking_tier": "none", "staking_tier": "none",
"fee_model": "taker", "fee_model": "maker",
"sharpe": 0.0, "sharpe": 0.0,
"sortino": 0.0, "sortino": 0.0,
"max_dd": 0.0, "max_dd": 0.0,
@@ -22,94 +22,6 @@
"win_rate": 0.0, "win_rate": 0.0,
"total_trades": 0, "total_trades": 0,
"equity_curve": [ "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", "t": "2026-07-06T05:00:00",
"v": 100.0 "v": 100.0
@@ -2905,10 +2817,98 @@
{ {
"t": "2026-08-04T07:00:00", "t": "2026-08-04T07:00:00",
"v": 100.0 "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": [], "trades": [],
"num_periods": 721, "num_periods": 721,
"data_source": "Hyperliquid Mainnet", "data_source": "Hyperliquid Mainnet",
"generated_at": "2026-08-04T07:29:03.636645" "generated_at": "2026-08-05T05:07:41.798429"
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -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"}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -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"}
File diff suppressed because it is too large Load Diff
+335
View File
@@ -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
+3 -1
View File
@@ -40,5 +40,7 @@ def max_drawdown(equity: list[float]) -> float:
def win_rate(trades: list[dict]) -> float: def win_rate(trades: list[dict]) -> float:
if not trades: if not trades:
return 0.0 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) return tp / len(trades)
+5
View File
@@ -0,0 +1,5 @@
node_modules/
.next/
out/
out-www/
_next-app-backup/
+21
View File
@@ -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"
}
}
+10
View File
@@ -0,0 +1,10 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
images: { unoptimized: true },
trailingSlash: true,
basePath: "/cv",
};
export default nextConfig;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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"
+20
View File
@@ -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",{}]]
+4
View File
@@ -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()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

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"}
File diff suppressed because one or more lines are too long
@@ -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",{}]]
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+1
View File
@@ -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

+1
View File
@@ -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

File diff suppressed because one or more lines are too long
+20
View File
@@ -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",{}]]
+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More