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
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,12 @@ def get_macro_news(n: int = 5, summary_limit: int = 200):
# =========================================================

MASSIVE_API_KEY = os.getenv("MASSIVE_API_KEY")
ADANOS_API_KEY = os.getenv("ADANOS_API_KEY")
BASE_URL = "https://api.polygon.io"
ADANOS_API_BASE_URL = (os.getenv("ADANOS_API_BASE_URL") or "https://api.adanos.org").rstrip("/")
REQUEST_TIMEOUT = 20
ADANOS_REQUEST_TIMEOUT = 8
ADANOS_PLATFORMS = ("reddit", "x", "news", "polymarket")

session = requests.Session()

Expand Down Expand Up @@ -113,10 +117,82 @@ def _request_json(url: str, params: dict) -> dict | list | None:
resp.raise_for_status()
return resp.json()

except (requests.RequestException, ValueError):
return None


def _request_adanos_json(platform: str, ticker: str) -> dict | None:
if not ADANOS_API_KEY:
return None

try:
resp = session.get(
f"{ADANOS_API_BASE_URL}/{platform}/stocks/v1/stock/{ticker}",
headers={"X-API-Key": ADANOS_API_KEY},
timeout=ADANOS_REQUEST_TIMEOUT,
)
if resp.status_code == 404:
return None
resp.raise_for_status()
payload = resp.json()
return payload if isinstance(payload, dict) else None
except requests.RequestException:
return None


def _format_adanos_sentiment_row(ticker: str, platform: str, payload: dict) -> str | None:
if payload.get("found") is False:
return None

fields = [
f"TICKER={ticker}",
f"SOURCE={platform}",
f"SENTIMENT={payload.get('sentiment_score', 'UNKNOWN')}",
f"BUZZ={payload.get('buzz_score', 'UNKNOWN')}",
f"MENTIONS={payload.get('mentions', 'UNKNOWN')}",
f"BULLISH_PCT={payload.get('bullish_pct', 'UNKNOWN')}",
f"BEARISH_PCT={payload.get('bearish_pct', 'UNKNOWN')}",
f"TREND={payload.get('trend', 'UNKNOWN')}",
]
return " | ".join(fields)


def get_adanos_sentiment_context(tickers, max_tickers: int = 12) -> str:
"""
Return optional Adanos Market Sentiment context for already-allowed tickers.

This helper never expands the model's trading universe. Callers should pass
only portfolio tickers or the explicit IPO whitelist already included in the
prompt.
"""
normalized: list[str] = []
for ticker in tickers:
if not isinstance(ticker, str):
continue
value = ticker.strip().upper()
if value and value not in normalized:
normalized.append(value)
if len(normalized) >= max_tickers:
break

lines = ["ADANOS_SENTIMENT_START"]
if not normalized:
lines.append("No eligible tickers supplied.")
elif not ADANOS_API_KEY:
lines.append("Optional Adanos Market Sentiment API disabled: ADANOS_API_KEY is not configured.")
else:
for ticker in normalized:
for platform in ADANOS_PLATFORMS:
payload = _request_adanos_json(platform, ticker)
if not payload:
continue
row = _format_adanos_sentiment_row(ticker, platform, payload)
if row:
lines.append(row)
lines.append("ADANOS_SENTIMENT_END")
return "\n".join(lines)


# =========================================================
# FEATURE HELPERS
# =========================================================
Expand Down
14 changes: 12 additions & 2 deletions Experiments/multi_model_ipo/prompts/daily_prompt.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import pandas as pd

from ..prompt_orchestration.get_prompt_data import get_macro_news, build_eligibility_series
from ..prompt_orchestration.get_prompt_data import (
build_eligibility_series,
get_adanos_sentiment_context,
get_macro_news,
)
from libb.model import LIBBmodel

# -------------------------------------------------------------------
Expand Down Expand Up @@ -115,6 +119,7 @@
You receive ONLY:

- MACRO_NEWS (rates, inflation, liquidity, sector rotation)
- ADANOS_SENTIMENT_CONTEXT (optional sentiment for PORTFOLIO_STATE tickers only)
- PORTFOLIO_STATE (authoritative holdings, cost basis, stops, conviction)
- TRADE_EXECUTION_LOG (CSV: timestamp | ticker | action | status | shares | price)

Expand All @@ -137,6 +142,9 @@
MACRO_NEWS:
{MACRO_NEWS}

ADANOS_SENTIMENT_CONTEXT:
{ADANOS_SENTIMENT_CONTEXT}

PORTFOLIO_STATE:
{PORTFOLIO_STATE}

Expand Down Expand Up @@ -295,6 +303,7 @@ def create_daily_prompt(libb: LIBBmodel):

macro_news = get_macro_news()
portfolio_state = libb.portfolio
adanos_sentiment_context = get_adanos_sentiment_context(portfolio_state["ticker"])
portfolio_eligibility = build_eligibility_series(portfolio_state["ticker"])
execution_log = libb.recent_execution_logs()

Expand All @@ -308,6 +317,7 @@ def create_daily_prompt(libb: LIBBmodel):
+ INPUT_BLOCK
+ GIVEN_DATA.format(
MACRO_NEWS=macro_news,
ADANOS_SENTIMENT_CONTEXT=adanos_sentiment_context,
PORTFOLIO_STATE=portfolio_state,
TRADE_EXECUTION_LOG=execution_log,
PORTFOLIO_TICKER_ELIGIBILITY=portfolio_eligibility,
Expand All @@ -319,4 +329,4 @@ def create_daily_prompt(libb: LIBBmodel):
+ OUTPUT_TEMPLATE
)

return prompt
return prompt
10 changes: 9 additions & 1 deletion Experiments/multi_model_ipo/prompts/deep_research_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
You receive ONLY:

- MACRO_NEWS (rates, inflation, liquidity, sector rotation)
- ADANOS_SENTIMENT_CONTEXT (optional sentiment for IPO_UNIVERSE and current portfolio tickers only)
- IPO_UNIVERSE (hard BUY whitelist)
- PORTFOLIO_STATE (authoritative holdings, cost basis, stops, conviction)
- TRADE_EXECUTION_LOG (CSV: timestamp | ticker | action | status | shares | price)
Expand All @@ -121,6 +122,9 @@
MACRO_NEWS:
{MACRO_NEWS}

ADANOS_SENTIMENT_CONTEXT:
{ADANOS_SENTIMENT_CONTEXT}

IPO_UNIVERSE:
{IPO_UNIVERSE}

Expand Down Expand Up @@ -285,10 +289,13 @@ def create_deep_research_prompt(libb: LIBBmodel):

ipo_universe = get_ipo_universe()
formatted_ipo_universe = format_universe_for_prompt(ipo_universe)
sentiment_tickers = [item.get("ticker") for item in ipo_universe if isinstance(item, dict)]

ipo_universe_eligibility = build_eligibility_series_from_universe(ipo_universe)

portfolio_state = libb.portfolio
sentiment_tickers.extend(portfolio_state["ticker"])
adanos_sentiment_context = get_adanos_sentiment_context(sentiment_tickers)
portfolio_tickers_eligibility = build_eligibility_series(portfolio_state["ticker"])
execution_log = libb.recent_execution_logs()

Expand All @@ -308,6 +315,7 @@ def create_deep_research_prompt(libb: LIBBmodel):
+ INPUT_BLOCK
+ GIVEN_DATA.format(
MACRO_NEWS=macro_news,
ADANOS_SENTIMENT_CONTEXT=adanos_sentiment_context,
IPO_UNIVERSE=formatted_ipo_universe,
IPO_TICKER_ELIGIBILITY=ipo_universe_eligibility,
PORTFOLIO_STATE=portfolio_state,
Expand All @@ -323,4 +331,4 @@ def create_deep_research_prompt(libb: LIBBmodel):
+ OUTPUT_TEMPLATE
)

return prompt
return prompt
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ Here are the artifacts links for the Micro-Cap Experiment:
- yfinance (primary data source)
- Stooq (fallback data source)
- Matplotlib
- Optional Adanos Market Sentiment API context for IPO experiment prompts (`ADANOS_API_KEY`)

### Optional Sentiment Context

The multi-model IPO experiment can enrich prompts with Adanos Market Sentiment
context for tickers that are already present in the allowed portfolio or IPO
universe. This does not expand the model's trading universe; it only adds
Reddit, X / FinTwit, News, and Polymarket sentiment context for eligible U.S.
equities.

```bash
export ADANOS_API_KEY="sk_live_..."
export ADANOS_API_BASE_URL="https://api.adanos.org"
```

---

Expand Down
47 changes: 47 additions & 0 deletions tests/test_adanos_sentiment_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import sys
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

from Experiments.multi_model_ipo.prompt_orchestration import get_prompt_data


def test_adanos_sentiment_context_is_disabled_without_key(monkeypatch):
monkeypatch.setattr(get_prompt_data, "ADANOS_API_KEY", None)

output = get_prompt_data.get_adanos_sentiment_context(["AAPL"])

assert "ADANOS_SENTIMENT_START" in output
assert "ADANOS_API_KEY is not configured" in output
assert "ADANOS_SENTIMENT_END" in output


def test_adanos_sentiment_context_formats_available_sources(monkeypatch):
calls = []

def fake_request(platform, ticker):
calls.append((platform, ticker))
if platform == "reddit":
return {
"found": True,
"sentiment_score": 0.31,
"buzz_score": 64.2,
"mentions": 42,
"bullish_pct": 52,
"bearish_pct": 17,
"trend": "rising",
}
return {"found": False}

monkeypatch.setattr(get_prompt_data, "ADANOS_API_KEY", "sk_live_test")
monkeypatch.setattr(get_prompt_data, "_request_adanos_json", fake_request)

output = get_prompt_data.get_adanos_sentiment_context(["aapl", "AAPL", "msft"])

assert "TICKER=AAPL | SOURCE=reddit | SENTIMENT=0.31" in output
assert "TICKER=MSFT | SOURCE=reddit | SENTIMENT=0.31" in output
assert ("reddit", "AAPL") in calls
assert calls.count(("reddit", "AAPL")) == 1