# Sentiment forecast

- Method: `GET`
- Path: `/forecasts/sentiment`
- Credits: 5 (10 with refresh=true; free if not_forecastable, 30 an hour)
- HTML docs: https://docs.hiddensystems.ai/nebula/reference/get-forecasts-sentiment/
- Interactive docs: https://docs.hiddensystems.ai/nebula/api/#endpoint=get-forecasts-sentiment

## Description

Where an asset's sentiment is likely to go over the next 72 hours, as a range of outcomes with their chances. `series` gives, every 4 hours, the bullish, bearish, neutral, and total posts expected in that step and the `sentiment_signal` at its end, on the /sentiment scale. `chance_higher_24h` and `chance_higher_72h` give the chance the signal ends above `current_sentiment_signal`; their `_confident` flags mark when that chance leans clearly one way.

Forecasts update within a minute of new posts about the asset, and at least every 15 minutes. `refresh=true` recomputes on the latest data in about 5 seconds; if that takes over 20, you get the latest forecast with `refreshed: false`, charged at 5. Assets without a forecast return `status: not_forecastable` with a `reason`; /forecasts/assets lists the assets that have one.

Costs 5 credits, or 10 with `refresh=true`. `not_forecastable` answers are free, up to 30 an hour per account; past that this endpoint returns 429 until the hour clears.

## cURL Example

```bash
curl --get \
  --url 'https://nebula-api.hiddensystems.ai/api/v1/public/forecasts/sentiment' \
  --data-urlencode 'asset_id=NVDA' \
  --data-urlencode 'asset_class=equity' \
  --header 'X-API-Key: YOUR_API_KEY'
```

## Path Parameters

No path parameters for this endpoint.

## Query Parameters

- `asset_id` (string, required): Asset ID from /assets or /search, such as NVDA, GLD, SPX, or bitcoin. Send it with the asset_class returned alongside it.
- `asset_class` (string, required, allowed: equity, commodity, index, crypto): Class of the asset named by asset_id: equity (stocks and ETFs), commodity, index, or crypto. Send the asset_class that /assets or /search returned alongside the asset_id.
- `refresh` (boolean, optional): true recomputes the forecast on the latest data: 10 credits, about 5 seconds. Defaults to false.

## Response

OK

- `as_of` (string (date-time), required): UTC start of the forecast: the end of the last complete 15 minutes it was computed from. Null when the asset has never been scored.
- `chance_higher_24h` (number (double), required): The chance the sentiment signal is higher than current_sentiment_signal in 24 hours, from 0 to 1.
- `chance_higher_24h_confident` (boolean, required): True when chance_higher_24h is under 0.3 or over 0.7: the forecast leans clearly one way. Nearer 0.5, read it as no clear direction.
- `chance_higher_72h` (number (double), required): The chance the sentiment signal is higher than current_sentiment_signal in 72 hours, from 0 to 1.
- `chance_higher_72h_confident` (boolean, required): True when chance_higher_72h is under 0.3 or over 0.7: the forecast leans clearly one way. Nearer 0.5, read it as no clear direction.
- `current_sentiment_signal` (number (double), required): The sentiment signal at as_of, on the /sentiment scale: bullish minus bearish posts over the trailing 24 hours, recent posts weighted more. Every path starts here, and the chances are measured against it. It can differ slightly from the latest /sentiment point, which is hourly.
- `horizon_hours` (integer, required): How far ahead the forecast runs, in hours: always 72.
- `reason` (string, required, enum: too_little_history, stale_data, missing_price, ): Why the asset cannot be forecast: too_little_history, stale_data, or missing_price. Null when status is ok.
- `refreshed` (boolean, required): True when this request re-ran the forecast (refresh=true and the re-run finished in time); false when it is the latest scheduled forecast.
- `sample_paths` (array<array<number (double)>>, required): 24 possible paths for the sentiment signal, each aligned with series. Draw them to show the spread of outcomes.
- `series` (array<object>, required): The forecast every 4 hours from as_of: 18 steps, oldest first.
- `series[].bearish_posts` (object, required): The bearish posts expected in this 4-hour step alone, bots excluded: like /sentiment's interval posts, not its trailing 24-hour counts.
- `series[].bearish_posts.mean` (number (double), required): The expected number of bearish posts in the step.
- `series[].bearish_posts.p10` (number (double), required): The 10th percentile of the bearish posts in the step.
- `series[].bearish_posts.p25` (number (double), required): The 25th percentile of the bearish posts in the step.
- `series[].bearish_posts.p50` (number (double), required): The 50th percentile of the bearish posts in the step.
- `series[].bearish_posts.p75` (number (double), required): The 75th percentile of the bearish posts in the step.
- `series[].bearish_posts.p90` (number (double), required): The 90th percentile of the bearish posts in the step.
- `series[].bullish_posts` (object, required): The bullish posts expected in this 4-hour step alone, bots excluded: like /sentiment's interval posts, not its trailing 24-hour counts.
- `series[].bullish_posts.mean` (number (double), required): The expected number of bullish posts in the step.
- `series[].bullish_posts.p10` (number (double), required): The 10th percentile of the bullish posts in the step.
- `series[].bullish_posts.p25` (number (double), required): The 25th percentile of the bullish posts in the step.
- `series[].bullish_posts.p50` (number (double), required): The 50th percentile of the bullish posts in the step.
- `series[].bullish_posts.p75` (number (double), required): The 75th percentile of the bullish posts in the step.
- `series[].bullish_posts.p90` (number (double), required): The 90th percentile of the bullish posts in the step.
- `series[].neutral_posts` (object, required): The neutral posts expected in this 4-hour step alone, bots excluded: like /sentiment's interval posts, not its trailing 24-hour counts.
- `series[].neutral_posts.mean` (number (double), required): The expected number of neutral posts in the step.
- `series[].neutral_posts.p10` (number (double), required): The 10th percentile of the neutral posts in the step.
- `series[].neutral_posts.p25` (number (double), required): The 25th percentile of the neutral posts in the step.
- `series[].neutral_posts.p50` (number (double), required): The 50th percentile of the neutral posts in the step.
- `series[].neutral_posts.p75` (number (double), required): The 75th percentile of the neutral posts in the step.
- `series[].neutral_posts.p90` (number (double), required): The 90th percentile of the neutral posts in the step.
- `series[].sentiment_signal` (object, required): The sentiment signal at the end of the step, on the /sentiment scale: where the line is likely to be.
- `series[].sentiment_signal.mean` (number (double), required): The expected sentiment signal at the end of the step.
- `series[].sentiment_signal.p10` (number (double), required): The 10th percentile of the sentiment signal at the end of the step.
- `series[].sentiment_signal.p50` (number (double), required): The 50th percentile of the sentiment signal at the end of the step.
- `series[].sentiment_signal.p90` (number (double), required): The 90th percentile of the sentiment signal at the end of the step.
- `series[].sentiment_signal.quantiles` (array<number (double)>, required): 21 quantiles of the sentiment signal at the end of the step: the 0th, 5th, 10th, ... 100th percentiles, lowest first. Interpolate between them for the chance of landing in any range.
- `series[].timestamp` (string (date-time), required): UTC time the step ends: as_of plus 4, 8, ... 72 hours.
- `series[].total_posts` (object, required): The posts (bullish, bearish, and neutral together) expected in this 4-hour step alone, bots excluded: like /sentiment's interval posts, not its trailing 24-hour counts. Its percentiles are of the total itself, so they are not the sums of the three kinds' percentiles.
- `series[].total_posts.mean` (number (double), required): The expected number of posts (bullish, bearish, and neutral together) in the step.
- `series[].total_posts.p10` (number (double), required): The 10th percentile of the posts (bullish, bearish, and neutral together) in the step.
- `series[].total_posts.p25` (number (double), required): The 25th percentile of the posts (bullish, bearish, and neutral together) in the step.
- `series[].total_posts.p50` (number (double), required): The 50th percentile of the posts (bullish, bearish, and neutral together) in the step.
- `series[].total_posts.p75` (number (double), required): The 75th percentile of the posts (bullish, bearish, and neutral together) in the step.
- `series[].total_posts.p90` (number (double), required): The 90th percentile of the posts (bullish, bearish, and neutral together) in the step.
- `status` (string, required, enum: ok, not_forecastable): ok, or not_forecastable when the asset cannot be forecast (the summary fields are then null and the series empty; reason says why).
- `turning` (integer, required, enum: -1, 0, 1, ): 1 while sentiment is turning bullish (the last few hours lean clearly more bullish than the last day), -1 while it is turning bearish, 0 otherwise.

Media type: `application/json`

## JSON Response

```json
{
  "as_of": "2026-09-14T09:15:00Z",
  "chance_higher_24h": 0.74,
  "chance_higher_24h_confident": true,
  "chance_higher_72h": 0.61,
  "chance_higher_72h_confident": false,
  "current_sentiment_signal": 128.6,
  "horizon_hours": 72,
  "reason": null,
  "refreshed": false,
  "sample_paths": [
    [
      133.2,
      141.7
    ],
    [
      126.4,
      118.9
    ]
  ],
  "series": [
    {
      "bearish_posts": {
        "mean": 18,
        "p10": 9.9,
        "p25": 13.5,
        "p50": 17.1,
        "p75": 21.6,
        "p90": 27.9
      },
      "bullish_posts": {
        "mean": 46,
        "p10": 25.3,
        "p25": 34.5,
        "p50": 43.7,
        "p75": 55.2,
        "p90": 71.3
      },
      "neutral_posts": {
        "mean": 29,
        "p10": 16,
        "p25": 21.8,
        "p50": 27.6,
        "p75": 34.8,
        "p90": 45
      },
      "sentiment_signal": {
        "mean": 131.4,
        "p10": 92.9,
        "p50": 129.6,
        "p90": 175.5,
        "quantiles": [
          21.6,
          70.2,
          92.9,
          104.8,
          113.1,
          119.4,
          124.2,
          127.6,
          130.5,
          132.7,
          134.5,
          136.4,
          138.7,
          141.3,
          144.9,
          149.2,
          155.1,
          163.8,
          179.4,
          202.6,
          287.3
        ]
      },
      "timestamp": "2026-09-14T13:15:00Z",
      "total_posts": {
        "mean": 93,
        "p10": 51.2,
        "p25": 69.8,
        "p50": 88.4,
        "p75": 111.6,
        "p90": 144.2
      }
    },
    {
      "bearish_posts": {
        "mean": 19,
        "p10": 10.5,
        "p25": 14.3,
        "p50": 18.1,
        "p75": 22.8,
        "p90": 29.5
      },
      "bullish_posts": {
        "mean": 50,
        "p10": 27.5,
        "p25": 37.5,
        "p50": 47.5,
        "p75": 60,
        "p90": 77.5
      },
      "neutral_posts": {
        "mean": 31,
        "p10": 17.1,
        "p25": 23.3,
        "p50": 29.5,
        "p75": 37.2,
        "p90": 48.1
      },
      "sentiment_signal": {
        "mean": 137.6,
        "p10": 99.1,
        "p50": 135.79999999999998,
        "p90": 181.7,
        "quantiles": [
          21.6,
          70.2,
          92.9,
          104.8,
          113.1,
          119.4,
          124.2,
          127.6,
          130.5,
          132.7,
          134.5,
          136.4,
          138.7,
          141.3,
          144.9,
          149.2,
          155.1,
          163.8,
          179.4,
          202.6,
          287.3
        ]
      },
      "timestamp": "2026-09-14T17:15:00Z",
      "total_posts": {
        "mean": 100,
        "p10": 55,
        "p25": 75,
        "p50": 95,
        "p75": 120,
        "p90": 155
      }
    }
  ],
  "status": "ok",
  "turning": 0
}
```
