"""Smoke tests for strategy_miner module.""" from __future__ import annotations import json import tempfile from pathlib import Path import pytest # --------------------------------------------------------------------------- # dtypes # --------------------------------------------------------------------------- def test_miner_config_from_dict(): from agent_market.strategy_miner.dtypes import MinerConfig cfg = MinerConfig.from_dict({"model": "gpt-4o", "max_iterations": 3, "unknown_key": 99}) assert cfg.model == "gpt-4o" assert cfg.max_iterations == 3 assert cfg.max_turns == 30 # default def test_miner_config_nested_sections(): from agent_market.strategy_miner.dtypes import MinerConfig cfg = MinerConfig.from_dict( { "budget": { "provider": "opencode", "max_iterations": 3, "max_turns": 9, "max_retries": 4, "repair_attempts": 2, }, "tools": { "tool_allowlist": ["file"], "bash_allow": False, "bash_timeout": 12, "bash_allowlist": ["echo ", "python3 "], }, "evaluation": { "min_trades": 25, "max_abs_drawdown": 12.5, "min_winrate": 0.55, "benchmark_suite": "benchmark_pack/default", }, "portfolio": { "portfolio_enabled": True, "portfolio_top_k": 4, "portfolio_min_candidates": 2, "portfolio_correlation_threshold": 0.8, "portfolio_max_weight": 0.5, }, } ) assert cfg.provider == "opencode" assert cfg.max_iterations == 3 assert cfg.max_turns == 9 assert cfg.max_retries == 4 assert cfg.repair_attempts == 2 assert cfg.tool_allowlist == ["file"] assert cfg.bash_allow is False assert cfg.bash_timeout == 12 assert cfg.bash_allowlist == ["echo ", "python3 "] assert cfg.min_trades == 25 assert cfg.max_abs_drawdown == 12.5 assert cfg.min_winrate == 0.55 assert cfg.benchmark_suite == "benchmark_pack/default" assert cfg.portfolio_enabled is True assert cfg.portfolio_top_k == 4 assert cfg.portfolio_correlation_threshold == 0.8 assert cfg.portfolio_max_weight == 0.5 def test_miner_config_defaults(): from agent_market.strategy_miner.dtypes import MinerConfig cfg = MinerConfig() assert cfg.max_retries == 2 assert cfg.max_parallel_roles == 1 assert cfg.max_drawdown_pct == 0.0 def test_miner_config_max_retries_override(): from agent_market.strategy_miner.dtypes import MinerConfig cfg = MinerConfig.from_dict({"max_retries": 5}) assert cfg.max_retries == 5 def test_miner_state_roundtrip(): from agent_market.strategy_miner.dtypes import MinerState, Phase, StrategyCandidate state = MinerState() state.phase = Phase.BACKTEST state.iteration = 2 candidate = StrategyCandidate( name="TestStrat", code="class X: pass", strategy_path=Path("/tmp/t.py") ) state.candidates.append(candidate) state.best_candidate = candidate state.best_score = 0.42 d = state.to_dict() j = json.dumps(d) state2 = MinerState.from_dict(json.loads(j)) assert state2.run_id == state.run_id assert state2.phase == Phase.BACKTEST assert state2.iteration == 2 assert state2.best_score == 0.42 assert state2.best_candidate.name == "TestStrat" assert len(state2.candidates) == 1 def test_miner_state_backward_compat_best_reward(): """Old checkpoints with 'best_reward' should load into best_score.""" from agent_market.strategy_miner.dtypes import MinerState, Phase old_data = { "run_id": "abc123", "phase": "strategy_gen", "iteration": 1, "best_reward": 0.75, "candidates": [], "history": [], } state = MinerState.from_dict(old_data) assert state.best_score == 0.75 # --------------------------------------------------------------------------- # grading # --------------------------------------------------------------------------- def test_compute_factor_score_none_inputs(): from agent_market.strategy_miner.grading import compute_factor_score assert compute_factor_score() is None assert compute_factor_score(features_parquet=None, expression="x") is None # --------------------------------------------------------------------------- # sandbox validation # --------------------------------------------------------------------------- def test_validate_strategy_code_pass(): from agent_market.strategy_miner.sandbox import validate_strategy_code code = """ from freqtrade.strategy import IStrategy import pandas_ta as ta class MyStrategy(IStrategy): timeframe = "5m" def populate_indicators(self, dataframe, metadata): return dataframe def populate_entry_trend(self, dataframe, metadata): return dataframe def populate_exit_trend(self, dataframe, metadata): return dataframe """ ok, msg = validate_strategy_code(code) assert ok, f"Expected pass: {msg}" def test_validate_strategy_code_forbidden_import(): from agent_market.strategy_miner.sandbox import validate_strategy_code code = """ import os from freqtrade.strategy import IStrategy class Bad(IStrategy): def populate_indicators(self, df, m): return df def populate_entry_trend(self, df, m): return df def populate_exit_trend(self, df, m): return df """ ok, msg = validate_strategy_code(code) assert not ok assert "os" in msg def test_validate_strategy_code_missing_method(): from agent_market.strategy_miner.sandbox import validate_strategy_code code = """ from freqtrade.strategy import IStrategy class NoMethods(IStrategy): pass """ ok, msg = validate_strategy_code(code) assert not ok assert "Missing" in msg def test_validate_strategy_code_no_istrategy(): from agent_market.strategy_miner.sandbox import validate_strategy_code code = """ class NotAStrategy: def populate_indicators(self, df, m): return df def populate_entry_trend(self, df, m): return df def populate_exit_trend(self, df, m): return df """ ok, msg = validate_strategy_code(code) assert not ok assert "IStrategy" in msg def test_validate_strategy_code_forbidden_call(): from agent_market.strategy_miner.sandbox import validate_strategy_code code = """ from freqtrade.strategy import IStrategy class Bad(IStrategy): def populate_indicators(self, df, m): eval("1+1") return df def populate_entry_trend(self, df, m): return df def populate_exit_trend(self, df, m): return df """ ok, msg = validate_strategy_code(code) assert not ok assert "eval" in msg def test_validate_strategy_code_blocks_negative_shift(): from agent_market.strategy_miner.sandbox import validate_strategy_code code = """ from freqtrade.strategy import IStrategy class Leaky(IStrategy): def populate_indicators(self, df, m): df['future'] = df['close'].shift(-1) return df def populate_entry_trend(self, df, m): return df def populate_exit_trend(self, df, m): return df """ ok, msg = validate_strategy_code(code) assert not ok assert "look-ahead" in msg def test_validate_strategy_code_blocks_centered_rolling(): from agent_market.strategy_miner.sandbox import validate_strategy_code code = """ from freqtrade.strategy import IStrategy class LeakyRolling(IStrategy): def populate_indicators(self, df, m): df['ma'] = df['close'].rolling(10, center=True).mean() return df def populate_entry_trend(self, df, m): return df def populate_exit_trend(self, df, m): return df """ ok, msg = validate_strategy_code(code) assert not ok assert "look-ahead" in msg def test_validate_strategy_code_rejects_date_set_index_before_informative_merge(): from agent_market.strategy_miner.sandbox import validate_strategy_code code = """ import pandas as pd from freqtrade.strategy import IStrategy, merge_informative_pair class BadMerge(IStrategy): def populate_indicators(self, dataframe, metadata): if not isinstance(dataframe.index, pd.DatetimeIndex): dataframe.set_index("date", inplace=True) inf = dataframe.copy() dataframe = merge_informative_pair(dataframe, inf, "5m", "15m", ffill=True) return dataframe def populate_entry_trend(self, dataframe, metadata): return dataframe def populate_exit_trend(self, dataframe, metadata): return dataframe """ ok, msg = validate_strategy_code(code) assert not ok assert "merge_informative_pair requires a preserved 'date' column" in msg def test_auto_fix_strategy_code_preserves_date_column_for_informative_merge(): from agent_market.strategy_miner.sandbox import auto_fix_strategy_code, validate_strategy_code code = """ import pandas as pd from freqtrade.strategy import IStrategy, merge_informative_pair class FixedMerge(IStrategy): def populate_indicators(self, dataframe, metadata): if not isinstance(dataframe.index, pd.DatetimeIndex): dataframe.set_index("date", inplace=True) inf = dataframe.copy() if not isinstance(inf.index, pd.DatetimeIndex): inf.set_index("date", inplace=True) dataframe = merge_informative_pair(dataframe, inf, "5m", "15m", ffill=True) return dataframe def populate_entry_trend(self, dataframe, metadata): return dataframe def populate_exit_trend(self, dataframe, metadata): return dataframe """ fixed, fixes = auto_fix_strategy_code(code) assert "preserve_date_column_for_informative_merge" in fixes assert '.set_index("date", inplace=True)' not in fixed assert 'dataframe.index = pd.DatetimeIndex(dataframe["date"])' in fixed assert 'inf.index = pd.DatetimeIndex(inf["date"])' in fixed ok, msg = validate_strategy_code(fixed) assert ok, msg def test_auto_fix_strategy_code_rewrites_self_merge_informative_pair_usage(): from agent_market.strategy_miner.sandbox import auto_fix_strategy_code, validate_strategy_code code = """ import pandas as pd from freqtrade.strategy import IStrategy class FixedSelfMerge(IStrategy): def populate_indicators(self, dataframe, metadata): inf = dataframe.copy() dataframe = self.merge_informative_pair(dataframe, inf, "5m", "15m", ffill=True) return dataframe def populate_entry_trend(self, dataframe, metadata): return dataframe def populate_exit_trend(self, dataframe, metadata): return dataframe """ fixed, fixes = auto_fix_strategy_code(code) assert "rewrite_self_merge_informative_pair" in fixes assert "ensure_merge_informative_pair_import" in fixes assert "self.merge_informative_pair" not in fixed assert "merge_informative_pair(dataframe, inf, '5m', '15m', ffill=True)" in fixed ok, msg = validate_strategy_code(fixed) assert ok, msg def test_auto_fix_strategy_code_forces_append_timeframe_false_when_suffix_is_used(): from agent_market.strategy_miner.sandbox import auto_fix_strategy_code, validate_strategy_code code = """ from freqtrade.strategy import IStrategy, merge_informative_pair class FixedSuffixMerge(IStrategy): def populate_indicators(self, dataframe, metadata): inf = dataframe.copy() dataframe = merge_informative_pair( dataframe, inf, "5m", "1h", suffix="1h", ffill=True, ) return dataframe def populate_entry_trend(self, dataframe, metadata): return dataframe def populate_exit_trend(self, dataframe, metadata): return dataframe """ fixed, fixes = auto_fix_strategy_code(code) assert "force_append_timeframe_false_for_suffix" in fixes assert "append_timeframe=False" in fixed ok, msg = validate_strategy_code(fixed) assert ok, msg def test_auto_fix_strategy_code_rewrites_parameter_default_to_value(): from agent_market.strategy_miner.sandbox import auto_fix_strategy_code, validate_strategy_code code = """ from freqtrade.strategy import IStrategy, DecimalParameter class BadParamDefault(IStrategy): p = DecimalParameter(0.5, 3.0, default=1.0, space="buy") def populate_indicators(self, dataframe, metadata): x = float(self.p.default) return dataframe def populate_entry_trend(self, dataframe, metadata): return dataframe def populate_exit_trend(self, dataframe, metadata): return dataframe """ fixed, fixes = auto_fix_strategy_code(code) assert "rewrite_parameter_default_to_value" in fixes assert "self.p.default" not in fixed assert "self.p.value" in fixed ok, msg = validate_strategy_code(fixed) assert ok, msg def test_validate_strategy_code_rejects_suffix_without_append_timeframe_false(): from agent_market.strategy_miner.sandbox import validate_strategy_code code = """ from freqtrade.strategy import IStrategy, merge_informative_pair class BadSuffixMerge(IStrategy): def populate_indicators(self, dataframe, metadata): inf = dataframe.copy() dataframe = merge_informative_pair(dataframe, inf, "5m", "1h", suffix="1h", ffill=True) return dataframe def populate_entry_trend(self, dataframe, metadata): return dataframe def populate_exit_trend(self, dataframe, metadata): return dataframe """ ok, msg = validate_strategy_code(code) assert not ok assert "append_timeframe=False" in msg def test_ensure_freqtrade_strategy_compliance_adds_ohlcv_suffix_guard_for_merge_asof(): from agent_market.strategy_miner.sandbox import ensure_freqtrade_strategy_compliance_code, validate_strategy_code code = """ import pandas as pd from freqtrade.strategy import IStrategy class MergeAsOfClose(IStrategy): timeframe = "5m" can_short = False order_types = {"entry": "market", "exit": "market", "stoploss": "market", "stoploss_on_exchange": False} order_time_in_force = {"entry": "GTC", "exit": "GTC"} def populate_indicators(self, df, metadata): informative = df[["date", "close"]].copy() df = pd.merge_asof( df.sort_values("date"), informative.sort_values("date"), on="date", direction="backward", ) return df def populate_entry_trend(self, df, metadata): return df def populate_exit_trend(self, df, metadata): return df """ fixed, fixes = ensure_freqtrade_strategy_compliance_code(code, timeframe="5m", enforce_can_short_false=True) assert "add_ohlcv_suffix_guard" in fixes assert "close_x" in fixed ok, msg = validate_strategy_code(fixed) assert ok, msg # --------------------------------------------------------------------------- # knowledge base # --------------------------------------------------------------------------- def test_knowledge_base_roundtrip(): from agent_market.strategy_miner.knowledge_base import KnowledgeBase with tempfile.TemporaryDirectory() as td: kb = KnowledgeBase(Path(td) / "kb.json") kb.add_elite("S1", "code1", 0.8, {"profit_total_pct": 10, "trades": 50}, 0) kb.add_elite("S2", "code2", 0.5, {"profit_total_pct": 5, "trades": 30}, 1) kb.add_failure("F1", 0, "validation", "Bad import") assert len(kb.elites) == 2 assert kb.elites[0]["reward"] == 0.8 # sorted desc assert len(kb.failures) == 1 # Reload kb2 = KnowledgeBase(Path(td) / "kb.json") assert len(kb2.elites) == 2 assert kb2.to_dict()["top_reward"] == 0.8 def test_knowledge_base_prefers_family_match_in_generation_query(): from agent_market.strategy_miner.knowledge_base import KnowledgeBase with tempfile.TemporaryDirectory() as td: kb = KnowledgeBase(Path(td) / "kb.json") kb.merge_payload( { "strategy_cards": [ { "run_id": "run_ml", "name": "HugeMlWinner", "iteration": 0, "candidate_type": "ml", "candidate_family": "ml/lightgbm", "timeframe": "5m", "universe": ["BTC/USDT", "ETH/USDT"], "metrics": {"sharpe": 999.0, "profit_pct": 20.0, "trades": 1000}, }, { "run_id": "run_rule", "name": "BreakoutRuleWinner", "iteration": 0, "candidate_type": "rule", "candidate_family": "rule/breakout", "timeframe": "5m", "universe": ["BTC/USDT", "ETH/USDT"], "metrics": {"sharpe": 1.2, "profit_pct": 4.0, "trades": 80}, }, ] } ) results = kb.query_strategy_cards_for_generation( top_n=2, family="rule/breakout", timeframe="5m", universe=["BTC/USDT", "ETH/USDT"], ) assert len(results) == 2 assert results[0]["name"] == "BreakoutRuleWinner" def test_knowledge_base_retrieve_for_generation_can_include_recent_cards(): from agent_market.strategy_miner.knowledge_base import KnowledgeBase with tempfile.TemporaryDirectory() as td: kb = KnowledgeBase(Path(td) / "kb.json") kb.merge_payload( { "strategy_cards": [ { "card_id": "run_old:OldBest:0", "created_at": "2026-01-01T00:00:00+00:00", "run_id": "run_old", "name": "OldBest", "iteration": 0, "candidate_type": "ml", "candidate_family": "ml/lightgbm", "timeframe": "5m", "universe": ["BTC/USDT"], "metrics": {"sharpe": 10.0, "profit_pct": 100.0, "trades": 1000}, }, { "card_id": "run_new:RecentLow:0", "created_at": "2026-01-02T00:00:00+00:00", "run_id": "run_new", "name": "RecentLow", "iteration": 0, "candidate_type": "ml", "candidate_family": "ml/lightgbm", "timeframe": "5m", "universe": ["BTC/USDT"], "metrics": {"sharpe": 0.0, "profit_pct": 0.0, "trades": 0}, }, ] } ) res = kb.retrieve_for_generation( family="ml/lightgbm", timeframe="5m", universe=["BTC/USDT"], top_n=1, recent_n=1, ) names = [c.get("name") for c in res.strategy_cards] assert "OldBest" in names assert "RecentLow" in names assert int((res.query or {}).get("recent_n") or 0) == 1 # --------------------------------------------------------------------------- # prompts # --------------------------------------------------------------------------- def test_build_strategy_gen_prompt(): from agent_market.strategy_miner.prompts import build_strategy_gen_prompt p = build_strategy_gen_prompt( iteration=0, sandbox_path="/tmp/sandbox", freqtrade_config="config.json", timerange="20250101-20260101", history=[], best_score=float("-inf"), ) assert "IStrategy" in p assert "Iteration 0" in p def test_build_analysis_prompt(): from agent_market.strategy_miner.prompts import build_analysis_prompt p = build_analysis_prompt( strategy_code="class X: pass", backtest_summary={"profit_total_pct": 5.0, "trades": 30}, metrics={"sharpe": 1.2, "sortino": 1.5}, ) assert "JSON" in p assert "strengths" in p assert "verdict" in p def test_strategy_gen_prompt_openai_compatible_no_tool_tags(): from agent_market.strategy_miner.prompts import build_strategy_gen_prompt p = build_strategy_gen_prompt( iteration=0, sandbox_path="/tmp/sandbox", freqtrade_config="config.json", timerange="20250101-20260101", history=[], best_score=float("-inf"), provider="openai_compatible", ) assert "You MAY use tool-call tags" not in p assert "single Python code block" in p def test_strategy_gen_prompt_opencode_has_tool_tags(): from agent_market.strategy_miner.prompts import build_strategy_gen_prompt p = build_strategy_gen_prompt( iteration=0, sandbox_path="/tmp/sandbox", freqtrade_config="config.json", timerange="20250101-20260101", history=[], best_score=float("-inf"), provider="opencode", ) assert "