ihsg-forecasting-dashboard / tests /test_web_api.py
ElvssYo's picture
Manual retrain artifact update
2928379 verified
Raw
History Blame Contribute Delete
9.51 kB
from http import HTTPStatus
import kag.web.api as web_api
from kag.web.api import api_response, artifact_status
def test_api_health_route():
status, payload = api_response("/api/health", {})
assert status == HTTPStatus.OK
assert payload == {"status": "ok"}
def test_api_symbols_route_includes_jkse():
status, payload = api_response("/api/market/symbols", {})
assert status == HTTPStatus.OK
assert any(row["ticker"] == "IHSG" and row["symbol"] == "^JKSE" for row in payload["symbols"])
assert len(payload["symbols"]) == 101
assert any(row["ticker"] == "BBCA" and row["name"] for row in payload["symbols"])
bbri = next(row for row in payload["symbols"] if row["ticker"] == "BBRI")
assert bbri["domain"] == "bri.co.id"
assert bbri["logo_url"].startswith("https://")
def test_api_stocks_metadata_route():
status, payload = api_response("/api/stocks/metadata", {"limit": ["30"]})
assert status == HTTPStatus.OK
assert payload["status"] == "ok"
assert payload["source"] == "data/company_metadata.json"
assert len(payload["stocks"]) == 30
assert any(row["ticker"] == "BBCA" and row["domain"] == "bca.co.id" for row in payload["stocks"])
def test_api_market_movers_route(monkeypatch):
def fake_load_market_movers(*, limit):
assert limit == 10
return {
"source": "prices.parquet",
"available_tickers": 2,
"gainers": [{"ticker": "BBCA", "change_pct": 0.01}],
"losers": [{"ticker": "BMRI", "change_pct": -0.02}],
}
monkeypatch.setattr(web_api, "load_market_movers", fake_load_market_movers)
status, payload = api_response("/api/market/movers", {"limit": ["10"]})
assert status == HTTPStatus.OK
assert payload["status"] == "ok"
assert payload["available_tickers"] == 2
assert payload["gainers"][0]["ticker"] == "BBCA"
def test_api_market_profile_route(monkeypatch):
def fake_load_company_profile(ticker):
assert ticker == "BBCA"
return {
"ticker": "BBCA",
"symbol": "BBCA.JK",
"name": "PT Bank Central Asia Tbk",
"logo_url": "https://example.test/bbca.png",
}
monkeypatch.setattr(web_api, "load_company_profile", fake_load_company_profile)
status, payload = api_response("/api/market/profile", {"ticker": ["BBCA"]})
assert status == HTTPStatus.OK
assert payload["status"] == "ok"
assert payload["profile"]["ticker"] == "BBCA"
assert payload["profile"]["logo_url"].startswith("https://")
def test_api_unknown_route_returns_not_found():
status, payload = api_response("/api/missing", {})
assert status == HTTPStatus.NOT_FOUND
assert payload["status"] == "error"
def test_api_candles_route_supports_intraday_period(monkeypatch):
calls = {}
def fake_load_local_price_ohlcv(symbol, *, period, interval):
raise AssertionError("intraday candles should not read local daily artifacts")
def fake_load_yfinance_ohlcv(symbol, *, period, interval):
calls["symbol"] = symbol
calls["period"] = period
calls["interval"] = interval
return []
monkeypatch.setattr(web_api, "load_local_price_ohlcv", fake_load_local_price_ohlcv)
monkeypatch.setattr(web_api, "load_yfinance_ohlcv", fake_load_yfinance_ohlcv)
status, payload = api_response(
"/api/market/candles",
{"symbol": ["^JKSE"], "period": ["1d"], "interval": ["5m"]},
)
assert status == HTTPStatus.OK
assert payload["period"] == "1d"
assert payload["interval"] == "5m"
assert calls == {"symbol": "^JKSE", "period": "1d", "interval": "5m"}
def test_api_candles_route_falls_back_when_local_daily_fails(monkeypatch):
calls = {}
def fake_load_local_price_ohlcv(symbol, *, period, interval):
assert symbol == "^JKSE"
assert period == "1d"
assert interval == "1d"
raise RuntimeError("bad local parquet")
def fake_load_yfinance_ohlcv(symbol, *, period, interval):
calls["symbol"] = symbol
calls["period"] = period
calls["interval"] = interval
return [{"date": "2026-05-25", "close": 7300.0, "source": "Yahoo Finance / yfinance"}]
monkeypatch.setattr(web_api, "load_local_price_ohlcv", fake_load_local_price_ohlcv)
monkeypatch.setattr(web_api, "load_yfinance_ohlcv", fake_load_yfinance_ohlcv)
status, payload = api_response(
"/api/market/candles",
{"symbol": ["^JKSE"], "period": ["1d"], "interval": ["1d"]},
)
assert status == HTTPStatus.OK
assert payload["period"] == "1d"
assert payload["interval"] == "1d"
assert payload["candles"]
assert calls == {"symbol": "^JKSE", "period": "1d", "interval": "1d"}
def test_api_candles_route_rejects_bad_interval():
status, payload = api_response(
"/api/market/candles",
{"symbol": ["^JKSE"], "period": ["1d"], "interval": ["bad"]},
)
assert status == HTTPStatus.BAD_REQUEST
assert payload["status"] == "error"
def test_api_prediction_route_allows_technical_snapshot(monkeypatch):
monkeypatch.setattr(
web_api,
"generate_latest_global_prediction",
lambda ticker: {
"status": "technical_snapshot",
"ticker": ticker,
"technical_snapshot": {"trend": "Mixed trend"},
},
)
status, payload = api_response("/api/prediction", {"ticker": ["GOTO"]})
assert status == HTTPStatus.OK
assert payload["status"] == "technical_snapshot"
def test_api_top_predictions_route(monkeypatch):
def fake_load_top_prediction_rankings(*, limit):
assert limit == 10
return {
"status": "ok",
"message": "Showing latest available model predictions.",
"up": [{"ticker": "BBCA", "predicted_return": 0.02}],
"down": [{"ticker": "GOTO", "predicted_return": -0.01}],
}
monkeypatch.setattr(web_api, "load_top_prediction_rankings", fake_load_top_prediction_rankings)
status, payload = api_response("/api/predictions/top", {"limit": ["10"]})
assert status == HTTPStatus.OK
assert payload["status"] == "ok"
assert payload["up"][0]["ticker"] == "BBCA"
assert payload["down"][0]["ticker"] == "GOTO"
def test_api_projection_route_builds_projection(monkeypatch):
def fake_load_local_price_ohlcv(symbol, *, period, interval):
assert symbol == "BBCA.JK"
assert period == "max"
assert interval == "1d"
return [
{"date": "2026-01-01", "close": 100.0},
{"date": "2026-01-02", "close": 101.0},
{"date": "2026-01-05", "close": 102.0},
]
monkeypatch.setattr(web_api, "load_local_price_ohlcv", fake_load_local_price_ohlcv)
monkeypatch.setattr(
web_api,
"generate_latest_global_prediction",
lambda ticker: {"status": "ok", "ticker": ticker, "predicted_direction": 1, "confidence": 0.4},
)
status, payload = api_response(
"/api/projection",
{
"ticker": ["BBCA"],
"amount": ["1000000"],
"entry_date": ["2026-01-01"],
"exit_date": ["2026-01-05"],
},
)
assert status == HTTPStatus.OK
assert payload["status"] == "ok"
assert payload["ticker"] == "BBCA"
assert payload["projection"]["initial_amount"] == 1_000_000
assert payload["projection"]["exit_value"] == 1_020_000
def test_api_projection_route_accepts_small_positive_amount(monkeypatch):
def fake_load_local_price_ohlcv(symbol, *, period, interval):
return [
{"date": "2026-01-01", "close": 100.0},
{"date": "2026-01-05", "close": 105.0},
]
monkeypatch.setattr(web_api, "load_local_price_ohlcv", fake_load_local_price_ohlcv)
monkeypatch.setattr(
web_api,
"generate_latest_global_prediction",
lambda ticker: {"status": "technical_snapshot", "ticker": ticker},
)
status, payload = api_response(
"/api/projection",
{
"ticker": ["BBCA"],
"amount": ["1000"],
"entry_date": ["2026-01-01"],
"exit_date": ["2026-01-05"],
},
)
assert status == HTTPStatus.OK
assert payload["projection"]["initial_amount"] == 1000
def test_api_projection_route_validates_amount():
status, payload = api_response(
"/api/projection",
{
"ticker": ["BBCA"],
"amount": ["0"],
"entry_date": ["2026-01-01"],
"exit_date": ["2026-01-05"],
},
)
assert status == HTTPStatus.BAD_REQUEST
assert payload["status"] == "error"
def test_api_news_route_describes_market_fallback(monkeypatch):
def fake_load_recent_news_rows(ticker, *, limit):
assert ticker == "GOTO"
assert limit == 6
return [{"title": "Market update", "scope": "market"}]
monkeypatch.setattr(web_api, "load_recent_news_rows", fake_load_recent_news_rows)
status, payload = api_response("/api/news", {"ticker": ["GOTO"], "limit": ["6"]})
assert status == HTTPStatus.OK
assert payload["context_message"] == "Showing recent market news when ticker-specific news is limited"
assert payload["news"][0]["scope"] == "market"
def test_artifact_status_reports_real_paths():
rows = artifact_status()
assert any(row["path"] == "data/final_dataset.parquet" for row in rows)
assert all("exists" in row for row in rows)