diff --git a/application/runtime_strategy_adapters.py b/application/runtime_strategy_adapters.py index 277cf6b..17534c9 100644 --- a/application/runtime_strategy_adapters.py +++ b/application/runtime_strategy_adapters.py @@ -48,14 +48,26 @@ def _bar_size_to_yfinance_interval(bar_size: str) -> str: return "1d" -def _frame_column(frame: pd.DataFrame, name: str): +def _coerce_frame_selection_to_series(selection: Any) -> pd.Series | None: + if isinstance(selection, pd.Series): + return selection + if isinstance(selection, pd.DataFrame) and len(selection.columns) > 0: + return selection.iloc[:, 0] + return None + + +def _frame_column(frame: pd.DataFrame, name: str) -> pd.Series | None: if name in frame.columns: - return frame[name] + series = _coerce_frame_selection_to_series(frame[name]) + if series is not None: + return series if isinstance(frame.columns, pd.MultiIndex): expected = name.lower() for column in frame.columns: if any(str(part).lower() == expected for part in column): - return frame[column] + series = _coerce_frame_selection_to_series(frame[column]) + if series is not None: + return series return None diff --git a/tests/test_runtime_strategy_adapters.py b/tests/test_runtime_strategy_adapters.py index cfe1190..c831d1b 100644 --- a/tests/test_runtime_strategy_adapters.py +++ b/tests/test_runtime_strategy_adapters.py @@ -2,7 +2,7 @@ import pandas as pd -from application.runtime_strategy_adapters import build_runtime_strategy_adapters +from application.runtime_strategy_adapters import _coerce_yfinance_candles, build_runtime_strategy_adapters def test_strategy_plugin_signals_are_loaded_reported_and_rendered(): @@ -98,6 +98,31 @@ def test_historical_candles_fall_back_when_ibkr_history_is_empty(): assert adapters.get_historical_candles("fake-ib", "QQQ") == fallback +def test_yfinance_candle_coercion_handles_single_symbol_multiindex_columns(): + frame = pd.DataFrame( + [[100.0, 101.0, 102.0, 99.0, 1_000_000.0]], + index=pd.to_datetime(["2026-05-22"]), + columns=pd.MultiIndex.from_tuples( + [ + ("Open", "QQQ"), + ("Close", "QQQ"), + ("High", "QQQ"), + ("Low", "QQQ"), + ("Volume", "QQQ"), + ] + ), + ) + + candles = _coerce_yfinance_candles(frame) + + assert len(candles) == 1 + assert candles[0]["open"] == 100.0 + assert candles[0]["close"] == 101.0 + assert candles[0]["high"] == 102.0 + assert candles[0]["low"] == 99.0 + assert candles[0]["volume"] == 1_000_000.0 + + def test_strategy_plugin_true_crisis_builds_escalated_alert_message(): translations = { "strategy_plugin_line": "plugin={plugin}|mode={mode}|route={route}|action={action}",