From d31e479940cd02f9f0370be5924f11cf72b57c28 Mon Sep 17 00:00:00 2001 From: Alex Schneider Date: Wed, 20 May 2026 20:49:03 +0200 Subject: [PATCH] feat: add optional Adanos sentiment prompt context --- .../prompt_orchestration/get_prompt_data.py | 76 +++++++++++++++++++ .../multi_model_ipo/prompts/daily_prompt.py | 14 +++- .../prompts/deep_research_prompt.py | 10 ++- README.md | 14 ++++ tests/test_adanos_sentiment_context.py | 47 ++++++++++++ 5 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 tests/test_adanos_sentiment_context.py diff --git a/Experiments/multi_model_ipo/prompt_orchestration/get_prompt_data.py b/Experiments/multi_model_ipo/prompt_orchestration/get_prompt_data.py index c01276e32..b1a479f0f 100644 --- a/Experiments/multi_model_ipo/prompt_orchestration/get_prompt_data.py +++ b/Experiments/multi_model_ipo/prompt_orchestration/get_prompt_data.py @@ -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() @@ -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 # ========================================================= diff --git a/Experiments/multi_model_ipo/prompts/daily_prompt.py b/Experiments/multi_model_ipo/prompts/daily_prompt.py index f246aa168..5d00fd008 100644 --- a/Experiments/multi_model_ipo/prompts/daily_prompt.py +++ b/Experiments/multi_model_ipo/prompts/daily_prompt.py @@ -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 # ------------------------------------------------------------------- @@ -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) @@ -137,6 +142,9 @@ MACRO_NEWS: {MACRO_NEWS} +ADANOS_SENTIMENT_CONTEXT: +{ADANOS_SENTIMENT_CONTEXT} + PORTFOLIO_STATE: {PORTFOLIO_STATE} @@ -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() @@ -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, @@ -319,4 +329,4 @@ def create_daily_prompt(libb: LIBBmodel): + OUTPUT_TEMPLATE ) - return prompt \ No newline at end of file + return prompt diff --git a/Experiments/multi_model_ipo/prompts/deep_research_prompt.py b/Experiments/multi_model_ipo/prompts/deep_research_prompt.py index 0a177e484..c219f6e53 100644 --- a/Experiments/multi_model_ipo/prompts/deep_research_prompt.py +++ b/Experiments/multi_model_ipo/prompts/deep_research_prompt.py @@ -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) @@ -121,6 +122,9 @@ MACRO_NEWS: {MACRO_NEWS} +ADANOS_SENTIMENT_CONTEXT: +{ADANOS_SENTIMENT_CONTEXT} + IPO_UNIVERSE: {IPO_UNIVERSE} @@ -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() @@ -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, @@ -323,4 +331,4 @@ def create_deep_research_prompt(libb: LIBBmodel): + OUTPUT_TEMPLATE ) - return prompt \ No newline at end of file + return prompt diff --git a/README.md b/README.md index 02412a014..ed940932c 100644 --- a/README.md +++ b/README.md @@ -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" +``` --- diff --git a/tests/test_adanos_sentiment_context.py b/tests/test_adanos_sentiment_context.py new file mode 100644 index 000000000..e53127b2c --- /dev/null +++ b/tests/test_adanos_sentiment_context.py @@ -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