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
59 changes: 59 additions & 0 deletions tools/tide-chart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Tide Chart

> Interactive dashboard comparing 24-hour probability cones for 5 equities 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.

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?

## 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):
- **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

## 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.

## Usage

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

# Run the tool (opens dashboard in browser)
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.

**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.

## 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)
- **Mock Mode:** Works without API key using bundled mock data
151 changes: 151 additions & 0 deletions tools/tide-chart/chart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""
Data processing module for the Tide Chart dashboard.

Fetches prediction percentiles and volatility for 5 equities,
normalizes to percentage change, calculates comparison metrics,
and ranks equities by forecast outlook.
"""

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"]
PERCENTILE_KEYS = ["0.005", "0.05", "0.2", "0.35", "0.5", "0.65", "0.8", "0.95", "0.995"]


def fetch_all_data(client):
"""Fetch prediction percentiles and volatility for all 5 equities.

Returns:
dict: {asset: {"percentiles": ..., "volatility": ..., "current_price": float}}
"""
data = {}
for asset in EQUITIES:
forecast = client.get_prediction_percentiles(asset, horizon="24h")
vol = client.get_volatility(asset, horizon="24h")
data[asset] = {
"current_price": forecast["current_price"],
"percentiles": forecast["forecast_future"]["percentiles"],
"average_volatility": vol["forecast_future"]["average_volatility"],
}
return data


def normalize_percentiles(percentiles, current_price):
"""Convert raw price percentiles to percentage change from current price.

Args:
percentiles: List of dicts (289 time steps), each with percentile keys.
current_price: Current asset price.

Returns:
List of dicts with same keys but values as % change.
"""
normalized = []
for step in percentiles:
norm_step = {}
for key in PERCENTILE_KEYS:
if key in step:
norm_step[key] = (step[key] - current_price) / current_price * 100
normalized.append(norm_step)
return normalized


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

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

Args:
data: Dict from fetch_all_data().

Returns:
dict: {asset: {median_move, upside, downside, skew, range_pct,
volatility, current_price}}
"""
metrics = {}
for asset, info in data.items():
current_price = info["current_price"]
final = info["percentiles"][-1]

median_move = (final["0.5"] - current_price) / current_price * 100
upside = (final["0.95"] - current_price) / current_price * 100
downside = (current_price - final["0.05"]) / current_price * 100
skew = upside - downside
range_pct = upside + downside

# Nominal (dollar) values
median_move_nominal = final["0.5"] - current_price
upside_nominal = final["0.95"] - current_price
downside_nominal = current_price - final["0.05"]

metrics[asset] = {
"median_move": median_move,
"upside": upside,
"downside": downside,
"skew": skew,
"range_pct": range_pct,
"volatility": info["average_volatility"],
"current_price": current_price,
"median_move_nominal": median_move_nominal,
"upside_nominal": upside_nominal,
"downside_nominal": downside_nominal,
"skew_nominal": upside_nominal - downside_nominal,
"range_nominal": upside_nominal + downside_nominal,
"price_high": current_price + upside_nominal,
"price_low": current_price - downside_nominal,
}
return metrics


def add_relative_to_spy(metrics):
"""Add relative-to-SPY fields for each equity.

Args:
metrics: Dict from calculate_metrics().

Returns:
Same dict with added relative_median and relative_skew fields.
"""
spy = metrics["SPY"]
for asset, m in metrics.items():
m["relative_median"] = m["median_move"] - spy["median_move"]
m["relative_skew"] = m["skew"] - spy["skew"]
return metrics


def rank_equities(metrics, sort_by="median_move", ascending=False):
"""Rank equities by a given metric.

Args:
metrics: Dict from calculate_metrics() with relative fields.
sort_by: Metric key to sort by.
ascending: Sort direction.

Returns:
List of (asset, metrics_dict) tuples, sorted by sort_by.
"""
items = list(metrics.items())
items.sort(key=lambda x: x[1][sort_by], reverse=not ascending)
return items


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

Args:
data: Dict from fetch_all_data().

Returns:
dict: {asset: list of normalized percentile dicts}
"""
series = {}
for asset, info in data.items():
series[asset] = normalize_percentiles(
info["percentiles"], info["current_price"]
)
return series
Loading