Files
stat_pairs_backtest/tests/test_spbt_day.py
T
2026-07-28 00:50:32 +00:00

200 lines
6.3 KiB
Python

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)