Skip to content
Open
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
49 changes: 49 additions & 0 deletions docs/api-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,55 @@ def store_price_data_sqlite(ticker, data, db_path):
conn.close()
```

## Adanos Market Sentiment Integration (Optional)

### Overview

Adanos provides optional market sentiment and attention features for equities
from Reddit, X / FinTwit, financial news, and Polymarket. AmpyFin does not
require Adanos for core training, backtesting, or execution. Use it only when
you want to add external sentiment features to a ticker universe before running
your own strategy or model pipeline.

### Setup and Configuration

```bash
# .env file or shell environment
ADANOS_API_KEY=your_adanos_api_key
```

### Feature Retrieval

```python
from utilities.adanos_sentiment import fetch_adanos_features

sentiment_features = fetch_adanos_features(
["AAPL", "MSFT", "NVDA"],
source="reddit",
start_date="2026-06-01",
end_date="2026-06-27",
)

print(sentiment_features.columns)
# Index([
# "Ticker", "adanos_source", "adanos_sentiment_score",
# "adanos_buzz_score", "adanos_bullish_pct", "adanos_bearish_pct",
# "adanos_mentions", "adanos_trend"
# ])
```

### Merging with Existing AmpyFin Data

```python
from utilities.common_utils import fetch_price_from_db

price_data = fetch_price_from_db(start_date, end_date, ["AAPL", "MSFT", "NVDA"])
price_with_sentiment = price_data.merge(sentiment_features, on="Ticker", how="left")
```

Keep sentiment features separate from order execution and broker credentials.
They are external research inputs, not trade instructions.

## Weights & Biases Integration

### Overview
Expand Down
106 changes: 106 additions & 0 deletions tests/test_adanos_sentiment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import json

import pandas as pd
import pytest

from utilities import adanos_sentiment as adanos


class DummyResponse:
def __init__(self, payload):
self.payload = payload

def __enter__(self):
return self

def __exit__(self, exc_type, exc, tb):
return False

def read(self):
return json.dumps(self.payload).encode("utf-8")


def test_normalize_adanos_stock_payload():
payload = {
"ticker": "AAPL",
"sentiment_score": "0.42",
"buzz_score": 73.5,
"bullish_pct": 61.2,
"bearish_pct": 18.4,
"mentions": "128",
"trend": "rising",
}

result = adanos.normalize_adanos_stock_payload("AAPL", payload, source="reddit")

assert result == {
"Ticker": "AAPL",
"adanos_source": "reddit",
"adanos_sentiment_score": 0.42,
"adanos_buzz_score": 73.5,
"adanos_bullish_pct": 61.2,
"adanos_bearish_pct": 18.4,
"adanos_mentions": 128,
"adanos_trend": "rising",
}


def test_adanos_features_from_payloads_preserves_columns():
payloads = {
"AAPL": {"ticker": "AAPL", "sentiment_score": 0.2, "mentions": 10},
"MSFT": {"ticker": "MSFT", "buzz_score": 66, "trend": "stable"},
}

result = adanos.adanos_features_from_payloads(payloads)

assert list(result.columns) == adanos.FEATURE_COLUMNS
assert result["Ticker"].tolist() == ["AAPL", "MSFT"]
assert pd.isna(result.loc[1, "adanos_sentiment_score"])


def test_error_payload_is_rejected():
with pytest.raises(adanos.AdanosSentimentError, match="error payload"):
adanos.normalize_adanos_stock_payload("AAPL", {"detail": "Invalid API key"})


def test_fetch_adanos_features_uses_api_key_and_date_params(monkeypatch):
requests = []

def fake_urlopen(request, timeout):
requests.append((request, timeout))
return DummyResponse(
{
"ticker": "AAPL",
"sentiment_score": 0.5,
"buzz_score": 70,
"mentions": 20,
}
)

monkeypatch.setattr(adanos, "urlopen", fake_urlopen)

result = adanos.fetch_adanos_features(
["AAPL"],
api_key="sk_test",
source="news",
start_date="2026-06-01",
end_date="2026-06-27",
base_url="https://example.test",
timeout=3,
)

request, timeout = requests[0]
assert (
request.full_url
== "https://example.test/news/stocks/v1/stock/AAPL?from=2026-06-01&to=2026-06-27"
)
assert request.headers["X-api-key"] == "sk_test"
assert timeout == 3
assert result.loc[0, "adanos_buzz_score"] == 70


def test_fetch_adanos_features_requires_api_key(monkeypatch):
monkeypatch.delenv(adanos.ADANOS_API_KEY_ENV, raising=False)

with pytest.raises(ValueError, match=adanos.ADANOS_API_KEY_ENV):
adanos.fetch_adanos_features(["AAPL"])
204 changes: 204 additions & 0 deletions utilities/adanos_sentiment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
"""Optional Adanos market sentiment feature helpers.

AmpyFin's core price and strategy pipelines remain independent from Adanos.
Use these helpers when you explicitly want to enrich a ticker universe with
external sentiment, buzz, and attention features.
"""

from __future__ import annotations

import json
import os
from datetime import date, datetime
from math import isfinite
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen

import pandas as pd

ADANOS_BASE_URL = "https://api.adanos.org"
ADANOS_API_KEY_ENV = "ADANOS_API_KEY"

SOURCE_ENDPOINTS = {
"reddit": "/reddit/stocks/v1/stock/{ticker}",
"x": "/x/stocks/v1/stock/{ticker}",
"news": "/news/stocks/v1/stock/{ticker}",
"polymarket": "/polymarket/stocks/v1/stock/{ticker}",
}

FEATURE_COLUMNS = [
"Ticker",
"adanos_source",
"adanos_sentiment_score",
"adanos_buzz_score",
"adanos_bullish_pct",
"adanos_bearish_pct",
"adanos_mentions",
"adanos_trend",
]


class AdanosSentimentError(RuntimeError):
"""Raised when Adanos sentiment data cannot be fetched or normalized."""


def _to_float(value: Any) -> float | None:
if isinstance(value, bool) or value is None:
return None
try:
parsed = float(value)
except (TypeError, ValueError):
return None
return parsed if isfinite(parsed) else None


def _to_int(value: Any) -> int | None:
if isinstance(value, bool) or value is None:
return None
try:
return int(value)
except (TypeError, ValueError):
return None


def _clean_text(value: Any) -> str | None:
if value is None:
return None
text = str(value).strip()
return text or None


def _format_date(value: date | datetime | str | None) -> str | None:
if value is None:
return None
if isinstance(value, datetime):
return value.date().isoformat()
if isinstance(value, date):
return value.isoformat()
return str(value)


def _build_stock_url(
ticker: str,
source: str,
base_url: str,
start_date: date | datetime | str | None,
end_date: date | datetime | str | None,
) -> str:
if source not in SOURCE_ENDPOINTS:
valid_sources = ", ".join(sorted(SOURCE_ENDPOINTS))
raise ValueError(
f"Unsupported Adanos source '{source}'. Expected one of: {valid_sources}"
)

ticker_path = quote(ticker.upper(), safe="")
path = SOURCE_ENDPOINTS[source].format(ticker=ticker_path)
params = {
key: value
for key, value in {
"from": _format_date(start_date),
"to": _format_date(end_date),
}.items()
if value
}
query = f"?{urlencode(params)}" if params else ""
return f"{base_url.rstrip('/')}{path}{query}"


def normalize_adanos_stock_payload(
ticker: str, payload: dict[str, Any], source: str = "reddit"
) -> dict[str, Any]:
"""Convert one Adanos stock sentiment response into AmpyFin feature columns."""

if any(payload.get(key) for key in ("error", "errors", "detail", "message")):
raise AdanosSentimentError(f"Adanos returned an error payload for {ticker}")

symbol = _clean_text(payload.get("ticker") or payload.get("symbol") or ticker)
if symbol is None:
raise AdanosSentimentError("Adanos payload must include a ticker or symbol")

mentions = _to_int(payload.get("mentions"))
if mentions is None:
mentions = _to_int(payload.get("trade_count"))

return {
"Ticker": symbol.upper(),
"adanos_source": source,
"adanos_sentiment_score": _to_float(payload.get("sentiment_score")),
"adanos_buzz_score": _to_float(payload.get("buzz_score")),
"adanos_bullish_pct": _to_float(payload.get("bullish_pct")),
"adanos_bearish_pct": _to_float(payload.get("bearish_pct")),
"adanos_mentions": mentions,
"adanos_trend": _clean_text(payload.get("trend")),
}


def adanos_features_from_payloads(
payloads: dict[str, dict[str, Any]],
source: str = "reddit",
) -> pd.DataFrame:
"""Build an AmpyFin-ready feature DataFrame from pre-fetched Adanos payloads."""

rows = [
normalize_adanos_stock_payload(ticker, payload, source=source)
for ticker, payload in payloads.items()
]
return pd.DataFrame(rows, columns=FEATURE_COLUMNS)


def fetch_adanos_features(
tickers: list[str],
api_key: str | None = None,
source: str = "reddit",
start_date: date | datetime | str | None = None,
end_date: date | datetime | str | None = None,
base_url: str = ADANOS_BASE_URL,
timeout: int = 10,
) -> pd.DataFrame:
"""Fetch Adanos stock sentiment features for a ticker list.

Args:
tickers: Stock ticker symbols to enrich.
api_key: Optional Adanos API key. Falls back to the ``ADANOS_API_KEY``
environment variable.
source: One of ``reddit``, ``x``, ``news``, or ``polymarket``.
start_date: Optional API ``from`` date.
end_date: Optional API ``to`` date.
base_url: Adanos API base URL, injectable for tests.
timeout: Request timeout in seconds.

Returns:
DataFrame keyed by ``Ticker`` with ``adanos_*`` feature columns.
"""

resolved_api_key = api_key or os.getenv(ADANOS_API_KEY_ENV)
if not resolved_api_key:
raise ValueError(
f"Set {ADANOS_API_KEY_ENV} or pass api_key to fetch Adanos sentiment features"
)

payloads: dict[str, dict[str, Any]] = {}
for ticker in tickers:
url = _build_stock_url(ticker, source, base_url, start_date, end_date)
request = Request(
url, headers={"X-API-Key": resolved_api_key, "Accept": "application/json"}
)
try:
with urlopen(request, timeout=timeout) as response:
payloads[ticker] = json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
raise AdanosSentimentError(
f"Adanos request failed for {ticker}: HTTP {exc.code}"
) from exc
except URLError as exc:
raise AdanosSentimentError(
f"Adanos request failed for {ticker}: {exc.reason}"
) from exc
except json.JSONDecodeError as exc:
raise AdanosSentimentError(
f"Adanos returned invalid JSON for {ticker}"
) from exc

return adanos_features_from_payloads(payloads, source=source)
Loading