Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 29 additions & 24 deletions tools/tide-chart/README.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,64 @@
# Tide Chart

> Interactive dashboard comparing 24-hour probability cones for 5 equities using Synth forecasting data.
> Interactive Flask dashboard comparing probability cones for equities and crypto using Synth forecasting data.

## Overview

Tide Chart overlays probabilistic price forecasts for SPY, NVDA, TSLA, AAPL, and GOOGL into a single comparison view. It normalizes all forecasts to percentage change, enabling direct comparison across different price levels, and generates a ranked summary table with key metrics.
Tide Chart overlays probabilistic price forecasts into a single comparison view with an interactive web interface. It supports both equities (SPY, NVDA, TSLA, AAPL, GOOGL) on the 24h horizon and crypto/commodities (BTC, ETH, SOL, XAU) on both 1h and 24h horizons. All forecasts are normalized to percentage change for direct comparison across different price levels.

The tool addresses three questions from the forecast data:
- **Directional alignment** - Are all equities moving the same way?
- **Relative magnitude** - Which equity has the widest expected range?
- **Asymmetric skew** - Is the upside or downside tail larger, individually and relative to SPY?
The tool provides:
- **Probability cones** - Interactive Plotly chart with 5th-95th percentile bands
- **Probability calculator** - Enter a target price to see the exact probability of an asset reaching it
- **Variable time horizons** - Toggle between Intraday (1H) and Next Day (24H) views
- **Live auto-refresh** - Manual refresh button and configurable 5-minute auto-refresh
- **Ranked metrics table** - Sortable table with directional alignment, skew, and relative benchmarks

## How It Works

1. Fetches `get_prediction_percentiles` and `get_volatility` for each of the 5 equities (24h horizon)
2. Normalizes all 289 time steps from raw price to `% change = (percentile - current_price) / current_price * 100`
3. Computes metrics from the final time step (end of 24h window):
1. Starts a Flask server serving the interactive dashboard at `http://localhost:5000`
2. Fetches `get_prediction_percentiles` and `get_volatility` for assets in the selected horizon
3. Normalizes time steps from raw price to `% change = (percentile - current_price) / current_price * 100`
4. Computes metrics from the final time step (end of forecast window):
- **Median Move** - 50th percentile % change
- **Upside/Downside** - 95th and 5th percentile distances
- **Directional Skew** - upside minus downside (positive = bullish asymmetry)
- **Range** - total 5th-to-95th percentile width
- **Relative to SPY** - each metric minus SPY's value
4. Ranks equities by median expected move (table columns are sortable by click)
5. Generates an interactive Plotly HTML dashboard and opens it in the browser
- **Relative to Benchmark** - each metric minus benchmark (SPY for equities, BTC for crypto)
5. Ranks assets by median expected move (table columns are sortable by click)
6. Probability calculator uses linear interpolation across 9 percentile levels to estimate P(price <= target)

## Synth Endpoints Used

- `get_prediction_percentiles(asset, horizon="24h")` - Provides 289 time-step probabilistic forecast with 9 percentile levels (0.5% to 99.5%). Used for the probability cone overlay and all derived metrics.
- `get_volatility(asset, horizon="24h")` - Provides forecasted average volatility. Displayed in the ranking table as an independent risk measure.
- `get_prediction_percentiles(asset, horizon)` - Provides time-step probabilistic forecast with 9 percentile levels (0.5% to 99.5%). Used for probability cones, metrics, and the probability calculator.
- `get_volatility(asset, horizon)` - Provides forecasted average volatility. Displayed in the ranking table as an independent risk measure.

## Usage

```bash
# Install dependencies
pip install -r requirements.txt

# Run the tool (opens dashboard in browser)
# Run the dashboard server (opens browser automatically)
python main.py

# Custom port
TIDE_CHART_PORT=8080 python main.py

# Run tests
python -m pytest tests/ -v
```

## Example Output

The dashboard contains two sections:

**Probability Cone Comparison** - Interactive Plotly chart with semi-transparent bands (5th-95th percentile) and median lines for each equity. Hover to see exact values at any time step.
## API Endpoints

**Equity Rankings** - Sortable table showing price, median move (% and $), forecasted volatility, directional skew (% and $), probability range (% and $), median vs SPY, and skew vs SPY. Click any column header to re-sort. Values are color-coded green (positive) or red (negative), with nominal dollar amounts shown alongside percentages for immediate context.
- `GET /` - Serves the interactive dashboard HTML
- `GET /api/data?horizon=24h` - Returns chart traces, table rows, and insights as JSON
- `POST /api/probability` - Calculates target price probability (body: `{"asset": "SPY", "target_price": 600, "horizon": "24h"}`)

## Technical Details

- **Language:** Python 3.10+
- **Dependencies:** plotly (for chart generation)
- **Synth Assets Used:** SPY, NVDA, TSLA, AAPL, GOOGL
- **Output:** Single HTML file (requires internet for Plotly CDN and fonts; no server needed)
- **Dependencies:** plotly, flask
- **Equities (24h only):** SPY, NVDA, TSLA, AAPL, GOOGL
- **Crypto + Commodities (1h & 24h):** BTC, ETH, SOL, XAU
- **Output:** Flask web server with Plotly CDN (requires internet for fonts/plotly)
- **Mock Mode:** Works without API key using bundled mock data
106 changes: 87 additions & 19 deletions tools/tide-chart/chart.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,52 @@
"""
Data processing module for the Tide Chart dashboard.

Fetches prediction percentiles and volatility for 5 equities,
Fetches prediction percentiles and volatility for supported assets,
normalizes to percentage change, calculates comparison metrics,
and ranks equities by forecast outlook.
ranks assets by forecast outlook, and computes target price probabilities.
"""

import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))

from synth_client import SynthClient

EQUITIES = ["SPY", "NVDA", "TSLA", "AAPL", "GOOGL"]
CRYPTO_ASSETS = ["BTC", "ETH", "SOL", "XAU"]
PERCENTILE_KEYS = ["0.005", "0.05", "0.2", "0.35", "0.5", "0.65", "0.8", "0.95", "0.995"]
PERCENTILE_LEVELS = [0.005, 0.05, 0.2, 0.35, 0.5, 0.65, 0.8, 0.95, 0.995]


ALL_ASSETS = EQUITIES + CRYPTO_ASSETS


def get_assets_for_horizon(horizon: str) -> list[str]:
"""Return the list of supported assets for a given time horizon.

Equities (SPY, NVDA, TSLA, AAPL, GOOGL) only support 24h.
Crypto + XAU (BTC, ETH, SOL, XAU) support both 1h and 24h.
The 24h horizon includes all assets.
"""
if horizon == "1h":
return list(CRYPTO_ASSETS)
return list(ALL_ASSETS)


def fetch_all_data(client, horizon: str = "24h") -> dict:
"""Fetch prediction percentiles and volatility for all assets in a horizon.

def fetch_all_data(client):
"""Fetch prediction percentiles and volatility for all 5 equities.
Args:
client: SynthClient instance.
horizon: "1h" or "24h".

Returns:
dict: {asset: {"percentiles": ..., "volatility": ..., "current_price": float}}
"""
assets = get_assets_for_horizon(horizon)
data = {}
for asset in EQUITIES:
forecast = client.get_prediction_percentiles(asset, horizon="24h")
vol = client.get_volatility(asset, horizon="24h")
for asset in assets:
forecast = client.get_prediction_percentiles(asset, horizon=horizon)
vol = client.get_volatility(asset, horizon=horizon)
data[asset] = {
"current_price": forecast["current_price"],
"percentiles": forecast["forecast_future"]["percentiles"],
Expand Down Expand Up @@ -56,9 +76,9 @@ def normalize_percentiles(percentiles, current_price):


def calculate_metrics(data):
"""Calculate comparison metrics for each equity.
"""Calculate comparison metrics for each asset.

Uses the final time step (end of 24h window) for metric computation.
Uses the final time step (end of forecast window) for metric computation.

Args:
data: Dict from fetch_all_data().
Expand Down Expand Up @@ -102,20 +122,30 @@ def calculate_metrics(data):
return metrics


def add_relative_to_spy(metrics):
"""Add relative-to-SPY fields for each equity.
def add_relative_to_benchmark(metrics) -> dict:
"""Add relative-to-benchmark fields for each asset.

Uses SPY as benchmark for equities, BTC for crypto assets.

Args:
metrics: Dict from calculate_metrics().

Returns:
Same dict with added relative_median and relative_skew fields.
Same dict with added relative_median, relative_skew, and benchmark fields.
"""
spy = metrics["SPY"]
assets = list(metrics.keys())
benchmark = "SPY" if "SPY" in metrics else assets[0]
bench_m = metrics[benchmark]
for asset, m in metrics.items():
m["relative_median"] = m["median_move"] - spy["median_move"]
m["relative_skew"] = m["skew"] - spy["skew"]
return metrics
m["relative_median"] = m["median_move"] - bench_m["median_move"]
m["relative_skew"] = m["skew"] - bench_m["skew"]
return metrics, benchmark


def add_relative_to_spy(metrics):
"""Add relative-to-SPY fields for each equity (legacy wrapper)."""
result, _ = add_relative_to_benchmark(metrics)
return result


def rank_equities(metrics, sort_by="median_move", ascending=False):
Expand All @@ -135,7 +165,7 @@ def rank_equities(metrics, sort_by="median_move", ascending=False):


def get_normalized_series(data):
"""Get full normalized time series for all equities (for charting).
"""Get full normalized time series for all assets (for charting).

Args:
data: Dict from fetch_all_data().
Expand All @@ -149,3 +179,41 @@ def get_normalized_series(data):
info["percentiles"], info["current_price"]
)
return series


def calculate_target_probability(percentiles: list[dict], target_price: float) -> float:
"""Calculate the probability of an asset reaching a target price.

Uses the final time step's percentile distribution and linear interpolation
to estimate P(price <= target). Returns the probability as a percentage (0-100).

Args:
percentiles: List of percentile dicts (time steps). Uses the final step.
target_price: The target price to evaluate.

Returns:
float: Probability (0-100) that the price will be at or below the target.
"""
final_step = percentiles[-1]
prices = [final_step[k] for k in PERCENTILE_KEYS]
levels = PERCENTILE_LEVELS

# Target below the lowest percentile
if target_price <= prices[0]:
return levels[0] * 100

# Target above the highest percentile
if target_price >= prices[-1]:
return levels[-1] * 100

# Linear interpolation between bracketing percentiles
for i in range(len(prices) - 1):
if prices[i] <= target_price <= prices[i + 1]:
price_range = prices[i + 1] - prices[i]
if price_range == 0:
return levels[i] * 100
fraction = (target_price - prices[i]) / price_range
prob = levels[i] + fraction * (levels[i + 1] - levels[i])
return prob * 100

return 50.0
Loading