import sqlite3 import pandas as pd import pytest from scripts.spbt_day import ( calculate_pair_theo_ret, calculate_ranked_pairs_theo_ret, load_selector_pair_rankings, load_trading_instructions, pair_assets_and_quote, parse_mr_score_final, rank_selector_pairs, ) @pytest.mark.parametrize( ("raw_score", "expected_score", "expected_status"), [ ('{"final":"0.75"}', 0.75, "ok"), ('{"final":0.5}', 0.5, "ok"), (None, None, "missing_mr_score"), ("not-json", None, "malformed_json"), ("[]", None, "unexpected_json_type"), ('{"other": "0.1"}', None, "missing_final"), ('{"final": ""}', None, "missing_final"), ('{"final": true}', None, "non_numeric_final"), ('{"final": "abc"}', None, "non_numeric_final"), ('{"final": "NaN"}', None, "non_finite_final"), ], ) def test_parse_mr_score_final(raw_score, expected_score, expected_status): assert parse_mr_score_final(raw_score) == (expected_score, expected_status) def test_rank_selector_pairs_uses_dense_descending_rank_and_preserves_bad_rows(): selector_pairs = pd.DataFrame( { "pair_name": ["PAIR_C", "PAIR_A", "PAIR_B", "PAIR_BAD"], "mr_score": [ '{"final":"0.7"}', '{"final":"0.9"}', '{"final":"0.7"}', '{"final":"bad"}', ], } ) ranked = rank_selector_pairs(selector_pairs) assert ranked["pair_name"].tolist() == ["PAIR_A", "PAIR_B", "PAIR_C", "PAIR_BAD"] assert ranked["pair_rank"].iloc[:3].tolist() == [1, 2, 2] assert pd.isna(ranked["pair_rank"].iloc[3]) assert ranked["mr_score_parse_status"].tolist() == [ "ok", "ok", "ok", "non_numeric_final", ] def test_load_selector_pair_rankings_validates_required_table(): conn = sqlite3.connect(":memory:") with pytest.raises(ValueError, match="missing required table: selector_pairs"): load_selector_pair_rankings(conn) def test_load_selector_pair_rankings_reads_sqlite_table(): conn = sqlite3.connect(":memory:") conn.execute("CREATE TABLE selector_pairs (pair_name TEXT, mr_score TEXT)") conn.executemany( "INSERT INTO selector_pairs (pair_name, mr_score) VALUES (?, ?)", [ ("PAIR_A", '{"final":"0.1"}'), ("PAIR_B", '{"final":"0.2"}'), ], ) ranked = load_selector_pair_rankings(conn) assert ranked[["pair_rank", "pair_name", "mr_score_final"]].to_dict("records") == [ {"pair_rank": 1, "pair_name": "PAIR_B", "mr_score_final": 0.2}, {"pair_rank": 2, "pair_name": "PAIR_A", "mr_score_final": 0.1}, ] def test_pair_assets_and_quote_parses_two_leg_pair(): assert pair_assets_and_quote("ADA:USD-BTC:USD") == (("ADA", "BTC"), "USD") def test_calculate_pair_theo_ret_replaces_targets_and_closes_open_position(): trd_inst_df = pd.DataFrame( { "time_ns": [1, 2, 3], "tstamp": ["t1", "t2", "t3"], "data": [ ( '{"action":"TARGET","quote_asset":"USD","assets":' '{"AAA":{"reference_price":"100","strength":"0.01"},' '"BBB":{"reference_price":"50","strength":"-0.02"}}}' ), ( '{"action":"TARGET","quote_asset":"USD","assets":' '{"AAA":{"reference_price":"120","strength":"0.01"},' '"BBB":{"reference_price":"60","strength":"-0.02"}}}' ), ( '{"action":"CLOSE","quote_asset":"USD","assets":' '{"AAA":{"reference_price":"132"},' '"BBB":{"reference_price":"54"}}}' ), ], } ) theo_ret = calculate_pair_theo_ret("AAA:USD-BBB:USD", trd_inst_df) assert theo_ret == { "pair_name": "AAA:USD-BBB:USD", "realized_pnl": pytest.approx(24.0), "unrealized_pnl": 0.0, } def test_calculate_pair_theo_ret_ignores_unmatched_quote_and_close_without_target(): trd_inst_df = pd.DataFrame( { "time_ns": [1, 2], "data": [ ( '{"action":"TARGET","quote_asset":"EUR","assets":' '{"AAA":{"reference_price":"100","strength":"0.01"},' '"BBB":{"reference_price":"50","strength":"-0.02"}}}' ), ( '{"action":"CLOSE","quote_asset":"USD","assets":' '{"AAA":{"reference_price":"110"},' '"BBB":{"reference_price":"45"}}}' ), ], } ) assert calculate_pair_theo_ret("AAA:USD-BBB:USD", trd_inst_df) == { "pair_name": "AAA:USD-BBB:USD", "realized_pnl": 0.0, "unrealized_pnl": 0.0, } def test_calculate_ranked_pairs_theo_ret_preserves_pairs_without_instructions(): rankings = pd.DataFrame( { "pair_name": ["AAA:USD-BBB:USD", "CCC:USD-DDD:USD"], "pair_rank": pd.Series([1, 2], dtype="Int64"), } ) trd_inst_df = pd.DataFrame( { "time_ns": [1, 2], "data": [ ( '{"action":"TARGET","quote_asset":"USD","assets":' '{"AAA":{"reference_price":"100","strength":"0.01"},' '"BBB":{"reference_price":"50","strength":"-0.02"}}}' ), ( '{"action":"CLOSE","quote_asset":"USD","assets":' '{"AAA":{"reference_price":"110"},' '"BBB":{"reference_price":"45"}}}' ), ], } ) result = calculate_ranked_pairs_theo_ret(rankings, trd_inst_df) assert result.to_dict("records") == [ { "pair_name": "AAA:USD-BBB:USD", "mr_ranking": 1, "realized_pnl": 20.0, "unrealized_pnl": 0.0, }, { "pair_name": "CCC:USD-DDD:USD", "mr_ranking": 2, "realized_pnl": 0.0, "unrealized_pnl": 0.0, }, ] def test_load_trading_instructions_validates_required_table(): conn = sqlite3.connect(":memory:") with pytest.raises(ValueError, match="missing required table: trading_instructions"): load_trading_instructions(conn)