Release v1.0.1

This commit is contained in:
Oleg Sheynin
2026-07-28 23:45:59 +00:00
parent 400bd41e56
commit 49c91e5d85
5 changed files with 2037 additions and 230 deletions
+39 -1
View File
@@ -4,7 +4,45 @@ All notable changes to this project are documented in this file.
## Unreleased ## Unreleased
- No unreleased changes yet. No unreleased changes yet.
## 2026-07-28 v1.0.1
- Added the `spbt_day` notebook for interactive single-day backtest result
analysis, including SQLite result file selection from the local data
directory.
- Added selector-pair loading and dense ranking by `mr_score.final`, preserving
rows with invalid score JSON for inspection.
- Added theoretical return calculation for ranked pairs from
`trading_instructions`, including reusable helper functions and tests.
- Added a Plotly histogram for visual analysis of total theoretical return by
pair.
- Moved notebook support code into reusable `scripts/spbt_day.py` helpers.
- Adjusted notebook table outputs to show all relevant rows and reduce
redundant intermediate displays.
- Added an alphabetically sorted pair selector for individual pair analysis.
- Added selected-pair theoretical execution tables and aligned TheoRet
calculations with target-delta trade generation.
- Added per-asset `strength` values to selected-pair theoretical execution
tables.
- Corrected theoretical execution size to use
`10000 * strength / reference_price`.
- Removed `:USD` quote suffixes from displayed pair names in notebook tables,
chart hovers, and the pair selector dropdown while preserving full internal
pair keys for calculations.
- Added `num_trades` to pair TheoRet summaries, counting asset-level theoretical
trades from effective `TARGET` and `CLOSE` instructions.
- Added sortable interactive grids for the pair TheoRet and selected-pair
theoretical execution tables.
- Styled interactive dataframe grids with black text on white backgrounds for
readability across notebook themes.
- Added a selected-pair Plotly chart that overlays theoretical BUY/SELL
executions on relative 1-minute market close data for both instruments.
- Anchored the selected-pair market chart at trading-day midnight and normalized
relative prices to each instrument's close at that timestamp.
- Added a `min_pctg_change` threshold for ranked pair TheoRet calculations to
skip small target-strength changes after a position is acquired.
- Added a notebook input field for the minimum TARGET strength-change threshold.
## 2026-07-25 v0.0.9 ## 2026-07-25 v0.0.9
+206 -189
View File
@@ -23,39 +23,43 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"from html import escape\n",
"from pathlib import Path\n", "from pathlib import Path\n",
"import sqlite3\n", "import importlib\n",
"import sys\n", "import sys\n",
"from urllib.parse import quote\n",
"\n", "\n",
"from IPython.display import display\n", "from IPython.display import display\n",
"import ipywidgets as widgets\n", "import ipywidgets as widgets\n",
"import pandas as pd\n",
"\n", "\n",
"START_DIR = Path.cwd().resolve()\n",
"for candidate in (START_DIR, *START_DIR.parents):\n",
" if (candidate / \"scripts\" / \"spbt_day.py\").exists():\n",
" if str(candidate) not in sys.path:\n",
" sys.path.insert(0, str(candidate))\n",
" break\n",
"\n", "\n",
"def find_repo_root(start: Path | None = None) -> Path:\n", "import scripts.spbt_day as spbt_day\n",
" \"\"\"Return the nearest parent containing repository-level files.\"\"\"\n",
" current = (start or Path.cwd()).resolve()\n",
" for candidate in (current, *current.parents):\n",
" if (candidate / \"requirements.txt\").exists() and (candidate / \"notebooks\").is_dir():\n",
" return candidate\n",
" return current\n",
"\n", "\n",
"spbt_day = importlib.reload(spbt_day)\n",
"\n",
"add_total_pnl = spbt_day.add_total_pnl\n",
"calculate_pair_theo_executions = spbt_day.calculate_pair_theo_executions\n",
"calculate_ranked_pairs_theo_ret = spbt_day.calculate_ranked_pairs_theo_ret\n",
"create_database_file_selector = spbt_day.create_database_file_selector\n",
"create_pair_name_dropdown = spbt_day.create_pair_name_dropdown\n",
"create_pair_trades_market_plot = spbt_day.create_pair_trades_market_plot\n",
"create_total_pnl_histogram = spbt_day.create_total_pnl_histogram\n",
"find_repo_root = spbt_day.find_repo_root\n",
"format_pair_name_for_display = spbt_day.format_pair_name_for_display\n",
"format_pair_names_for_display = spbt_day.format_pair_names_for_display\n",
"infer_trading_day_start_ns = spbt_day.infer_trading_day_start_ns\n",
"load_selector_pair_rankings = spbt_day.load_selector_pair_rankings\n",
"load_pair_market_data = spbt_day.load_pair_market_data\n",
"load_trading_instructions = spbt_day.load_trading_instructions\n",
"show_interactive_dataframe = spbt_day.show_interactive_dataframe\n",
"\n", "\n",
"REPO_ROOT = find_repo_root()\n", "REPO_ROOT = find_repo_root()\n",
"if str(REPO_ROOT) not in sys.path:\n",
" sys.path.insert(0, str(REPO_ROOT))\n",
"\n",
"from scripts.spbt_day import (\n",
" calculate_ranked_pairs_theo_ret,\n",
" load_selector_pair_rankings,\n",
" load_trading_instructions,\n",
")\n",
"\n",
"DEFAULT_DATA_DIR = REPO_ROOT / \"data\"\n", "DEFAULT_DATA_DIR = REPO_ROOT / \"data\"\n",
"SQLITE_EXTENSIONS = {\".db\", \".sqlite\", \".sqlite3\"}\n",
"\n",
"selected_db_path: Path | None = None\n",
"\n", "\n",
"REPO_ROOT, DEFAULT_DATA_DIR" "REPO_ROOT, DEFAULT_DATA_DIR"
] ]
@@ -67,136 +71,12 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"directory_input = widgets.Text(\n", "db_selector = create_database_file_selector(\n",
" value=str(DEFAULT_DATA_DIR),\n", " default_data_dir=DEFAULT_DATA_DIR,\n",
" description=\"Directory\",\n", " repo_root=REPO_ROOT,\n",
" continuous_update=False,\n",
" layout=widgets.Layout(width=\"100%\"),\n",
" style={\"description_width\": \"90px\"},\n",
")\n", ")\n",
"\n", "\n",
"show_all_files = widgets.Checkbox(\n", "display(db_selector.widget)"
" value=False,\n",
" description=\"Show all files\",\n",
" indent=False,\n",
")\n",
"\n",
"refresh_button = widgets.Button(\n",
" description=\"Refresh\",\n",
" icon=\"refresh\",\n",
" button_style=\"\",\n",
" tooltip=\"Rescan the selected directory\",\n",
")\n",
"\n",
"file_select = widgets.Select(\n",
" options=[],\n",
" rows=12,\n",
" description=\"Files\",\n",
" layout=widgets.Layout(width=\"100%\"),\n",
" style={\"description_width\": \"90px\"},\n",
")\n",
"\n",
"selected_path_display = widgets.HTML(value=\"<b>Selected database:</b> none\")\n",
"status_output = widgets.Output()\n",
"\n",
"\n",
"def normalize_directory(raw_path: str) -> Path:\n",
" path = Path(raw_path).expanduser()\n",
" if not path.is_absolute():\n",
" path = REPO_ROOT / path\n",
" return path.resolve()\n",
"\n",
"\n",
"def list_candidate_files(directory: Path, show_all: bool = False) -> list[Path]:\n",
" def result_file_sort_key(path: Path) -> tuple[int, str]:\n",
" name = path.name.lower()\n",
" if \".spbt_results.\" in name:\n",
" priority = 0\n",
" elif \"selector\" in name and \"results\" in name:\n",
" priority = 1\n",
" elif \"results\" in name:\n",
" priority = 2\n",
" else:\n",
" priority = 3\n",
" return priority, name\n",
"\n",
" if show_all:\n",
" return sorted(\n",
" (path for path in directory.iterdir() if path.is_file()),\n",
" key=result_file_sort_key,\n",
" )\n",
" return sorted(\n",
" (\n",
" path\n",
" for path in directory.iterdir()\n",
" if path.is_file() and path.suffix.lower() in SQLITE_EXTENSIONS\n",
" ),\n",
" key=result_file_sort_key,\n",
" )\n",
"\n",
"\n",
"def set_selected_database(path_value: str | None) -> None:\n",
" global selected_db_path\n",
" selected_db_path = Path(path_value).resolve() if path_value else None\n",
" label = str(selected_db_path) if selected_db_path else \"none\"\n",
" selected_path_display.value = f\"<b>Selected database:</b> {escape(label)}\"\n",
"\n",
"\n",
"def refresh_file_list(*_args) -> None:\n",
" directory = normalize_directory(directory_input.value)\n",
" with status_output:\n",
" status_output.clear_output()\n",
" if not directory.exists():\n",
" file_select.options = []\n",
" set_selected_database(None)\n",
" print(f\"Directory does not exist: {directory}\")\n",
" return\n",
" if not directory.is_dir():\n",
" file_select.options = []\n",
" set_selected_database(None)\n",
" print(f\"Path is not a directory: {directory}\")\n",
" return\n",
"\n",
" candidates = list_candidate_files(directory, show_all=show_all_files.value)\n",
" candidate_values = [str(path) for path in candidates]\n",
" previous_value = file_select.value\n",
" file_select.options = [(path.name, str(path)) for path in candidates]\n",
" if candidates:\n",
" file_select.value = previous_value if previous_value in candidate_values else candidate_values[0]\n",
" set_selected_database(file_select.value)\n",
" else:\n",
" set_selected_database(None)\n",
"\n",
" if candidates:\n",
" print(f\"Found {len(candidates)} file(s) in {directory}\")\n",
" else:\n",
" suffixes = \", \".join(sorted(SQLITE_EXTENSIONS))\n",
" print(f\"No SQLite files ({suffixes}) found in {directory}\")\n",
"\n",
"\n",
"def on_file_selected(change) -> None:\n",
" if change[\"name\"] == \"value\":\n",
" set_selected_database(change[\"new\"])\n",
"\n",
"\n",
"refresh_button.on_click(refresh_file_list)\n",
"show_all_files.observe(refresh_file_list, names=\"value\")\n",
"directory_input.observe(refresh_file_list, names=\"value\")\n",
"file_select.observe(on_file_selected, names=\"value\")\n",
"\n",
"display(\n",
" widgets.VBox(\n",
" [\n",
" widgets.HBox([directory_input, refresh_button]),\n",
" show_all_files,\n",
" file_select,\n",
" selected_path_display,\n",
" status_output,\n",
" ]\n",
" )\n",
")\n",
"\n",
"refresh_file_list()"
] ]
}, },
{ {
@@ -206,23 +86,8 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"def selected_database_path() -> Path:\n", "selected_database_path = db_selector.selected_database_path\n",
" \"\"\"Return the interactively selected SQLite result path.\"\"\"\n", "connect_selected_database = db_selector.connect_selected_database\n",
" if selected_db_path is None:\n",
" raise ValueError(\"Choose a SQLite result file before continuing.\")\n",
" if not selected_db_path.exists():\n",
" raise FileNotFoundError(f\"Selected database does not exist: {selected_db_path}\")\n",
" if not selected_db_path.is_file():\n",
" raise ValueError(f\"Selected database path is not a file: {selected_db_path}\")\n",
" return selected_db_path\n",
"\n",
"\n",
"def connect_selected_database() -> sqlite3.Connection:\n",
" \"\"\"Open a read-only SQLite connection to the selected result database.\"\"\"\n",
" db_path = selected_database_path()\n",
" uri = f\"file:{quote(db_path.as_posix(), safe='/:')}?mode=ro\"\n",
" return sqlite3.connect(uri, uri=True)\n",
"\n",
"\n", "\n",
"# Later notebook sections can call selected_database_path() or connect_selected_database()." "# Later notebook sections can call selected_database_path() or connect_selected_database()."
] ]
@@ -252,24 +117,11 @@
"finally:\n", "finally:\n",
" conn.close()\n", " conn.close()\n",
"\n", "\n",
"selector_pair_rankings" "selector_pair_rankings_display = format_pair_names_for_display(\n",
] " selector_pair_rankings[[\"pair_rank\", \"pair_name\", \"mr_score_final\"]]\n",
},
{
"cell_type": "code",
"execution_count": null,
"id": "selector-pair-ranking-summary",
"metadata": {},
"outputs": [],
"source": [
"selector_pair_ranking_summary = (\n",
" selector_pair_rankings[\"mr_score_parse_status\"]\n",
" .value_counts(dropna=False)\n",
" .rename_axis(\"mr_score_parse_status\")\n",
" .reset_index(name=\"row_count\")\n",
")\n", ")\n",
"\n", "with pd.option_context(\"display.max_rows\", None):\n",
"selector_pair_ranking_summary" " display(selector_pair_rankings_display)"
] ]
}, },
{ {
@@ -279,9 +131,26 @@
"source": [ "source": [
"## Theoretical Return by Pair\n", "## Theoretical Return by Pair\n",
"\n", "\n",
"Load `trading_instructions` and calculate theoretical return for each ranked pair. Each pair starts from a fixed `$10,000` theoretical USD base. `TARGET` opens or replaces the current theoretical position using each asset's `strength` and `reference_price`; `CLOSE` liquidates the open position at the close row's `reference_price`; `HOLD` is ignored.\n", "Load `trading_instructions` and calculate theoretical return for each ranked pair. Each pair starts from a fixed `$10,000` theoretical USD base. `TARGET` trades from the current theoretical position to the new target position, where target size is `10000 * strength / reference_price`; `CLOSE` liquidates the open position at the close row's `reference_price`; `HOLD` is ignored.\n",
"\n", "\n",
"`realized_pnl` and `unrealized_pnl` are percentage returns relative to `$10,000`, sorted by ascending MR rank." "`MIN_TARGET_STRENGTH_CHANGE_PCTG` can be raised above `0.0` to skip `TARGET` updates whose absolute percentage strength change is smaller than the threshold since the position was acquired. `num_trades` counts asset-level theoretical trades caused by effective `TARGET` and `CLOSE` rows. `realized_pnl` and `unrealized_pnl` are percentage returns relative to `$10,000`. The displayed dataframe is sorted by total return (`realized_pnl + unrealized_pnl`) ascending."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "target-change-threshold-input",
"metadata": {},
"outputs": [],
"source": [
"min_target_change_input = widgets.FloatText(\n",
" value=0.0,\n",
" description=\"Mininal TARGET change (%)\",\n",
" step=1.0,\n",
" layout=widgets.Layout(width=\"420px\"),\n",
" style={\"description_width\": \"190px\"},\n",
")\n",
"display(min_target_change_input)"
] ]
}, },
{ {
@@ -297,7 +166,7 @@
"finally:\n", "finally:\n",
" conn.close()\n", " conn.close()\n",
"\n", "\n",
"trading_instructions" "print(f\"Loaded {len(trading_instructions):,} trading instruction rows.\")"
] ]
}, },
{ {
@@ -307,12 +176,160 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"pair_theo_ret = calculate_ranked_pairs_theo_ret(\n", "MIN_TARGET_STRENGTH_CHANGE_PCTG = float(min_target_change_input.value)\n",
"\n",
"pair_theo_ret = add_total_pnl(\n",
" calculate_ranked_pairs_theo_ret(\n",
" selector_pair_rankings,\n", " selector_pair_rankings,\n",
" trading_instructions,\n", " trading_instructions,\n",
" min_pctg_change=MIN_TARGET_STRENGTH_CHANGE_PCTG,\n",
" )\n",
").sort_values(\n",
" [\"total_pnl\", \"pair_name\"],\n",
" ascending=[True, True],\n",
" kind=\"mergesort\",\n",
").drop(columns=\"total_pnl\").reset_index(drop=True)\n",
"\n",
"pair_theo_ret_display = format_pair_names_for_display(pair_theo_ret)\n",
"\n",
"show_interactive_dataframe(\n",
" pair_theo_ret_display,\n",
" table_id=\"pair-theo-ret-grid\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "theoretical-return-histogram-context",
"metadata": {},
"source": [
"## Total Theoretical Return Distribution\n",
"\n",
"Plot the distribution of total theoretical return, calculated as `realized_pnl + unrealized_pnl`. Plotly chooses histogram bins automatically."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "plot-total-theoretical-return-histogram",
"metadata": {},
"outputs": [],
"source": [
"total_pnl_histogram = create_total_pnl_histogram(pair_theo_ret)\n",
"\n",
"total_pnl_histogram"
]
},
{
"cell_type": "markdown",
"id": "individual-pair-analysis-context",
"metadata": {},
"source": [
"## Individual Pair Analysis\n",
"\n",
"Choose one pair for detailed follow-up analysis. Pair names are sorted alphabetically."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "choose-individual-pair",
"metadata": {},
"outputs": [],
"source": [
"pair_name_dropdown = create_pair_name_dropdown(selector_pair_rankings)\n",
"display(pair_name_dropdown)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "selected-individual-pair",
"metadata": {},
"outputs": [],
"source": [
"selected_pair_name = pair_name_dropdown.value\n",
"format_pair_name_for_display(selected_pair_name)"
]
},
{
"cell_type": "markdown",
"id": "selected-pair-theo-executions-context",
"metadata": {},
"source": [
"### Selected Pair Theoretical Executions\n",
"\n",
"Create the theoretical asset-level executions used by the PnL calculation for the selected pair. `TARGET` rows trade the position difference from the current theoretical position to the new target position, where target size is `10000 * strength / reference_price`; `CLOSE` rows flatten the current theoretical position. Positive size is `BUY`; negative size is `SELL`; USD value is signed as the opposite cash movement."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "selected-pair-theo-executions",
"metadata": {},
"outputs": [],
"source": [
"selected_pair_theo_executions = calculate_pair_theo_executions(\n",
" selected_pair_name,\n",
" trading_instructions,\n",
" min_pctg_change=MIN_TARGET_STRENGTH_CHANGE_PCTG,\n",
")\n", ")\n",
"\n", "\n",
"pair_theo_ret" "selected_pair_theo_execution_columns = [\n",
" \"time\",\n",
" \"asset\",\n",
" \"action\",\n",
" \"side\",\n",
" \"strength\",\n",
" \"size\",\n",
" \"price\",\n",
" \"usd_value\",\n",
"]\n",
"selected_pair_theo_executions_display = selected_pair_theo_executions.reindex(\n",
" columns=selected_pair_theo_execution_columns\n",
")\n",
"show_interactive_dataframe(\n",
" selected_pair_theo_executions_display,\n",
" table_id=\"selected-pair-theo-executions-grid\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "selected-pair-market-trades-context",
"metadata": {},
"source": [
"### Selected Pair Trades on Market Data\n",
"\n",
"Load full available 1-minute market data for the selected pair's instruments from `ohlcv_1min`, starting at midnight UTC of the trading day inferred from `trading_instructions`. Close prices are shown as relative prices from each instrument's close at that midnight. Theoretical executions are overlaid at their execution `reference_price`, normalized by the same midnight close. Execution markers use execution timestamps directly and do not require a matching OHLCV row."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "selected-pair-market-trades-plot",
"metadata": {},
"outputs": [],
"source": [
"trading_day_start_ns = infer_trading_day_start_ns(trading_instructions)\n",
"\n",
"conn = connect_selected_database()\n",
"try:\n",
" selected_pair_market_data = load_pair_market_data(\n",
" conn,\n",
" selected_pair_name,\n",
" trading_day_start_ns=trading_day_start_ns,\n",
" )\n",
"finally:\n",
" conn.close()\n",
"\n",
"selected_pair_market_trades_plot = create_pair_trades_market_plot(\n",
" selected_pair_name,\n",
" selected_pair_market_data,\n",
" selected_pair_theo_executions,\n",
")\n",
"\n",
"selected_pair_market_trades_plot"
] ]
} }
], ],
+2
View File
@@ -1,9 +1,11 @@
# Interactive analysis # Interactive analysis
ipykernel>=6.29,<7 ipykernel>=6.29,<7
ipywidgets>=8.1,<9 ipywidgets>=8.1,<9
itables>=2.2,<3
jupyter>=1.1,<2 jupyter>=1.1,<2
nbformat>=5.10,<6 nbformat>=5.10,<6
pandas>=2.2,<3 pandas>=2.2,<3
plotly>=5.24,<7
# Verification # Verification
nbmake>=1.5,<2 nbmake>=1.5,<2
+888 -36
View File
File diff suppressed because it is too large Load Diff
+900 -2
View File
@@ -1,16 +1,34 @@
import sqlite3 import sqlite3
from pathlib import Path
import pandas as pd import pandas as pd
import pytest import pytest
from scripts.spbt_day import ( from scripts.spbt_day import (
add_total_pnl,
calculate_pair_theo_executions,
calculate_pair_theo_ret, calculate_pair_theo_ret,
calculate_ranked_pairs_theo_ret, calculate_ranked_pairs_theo_ret,
create_pair_name_dropdown,
create_pair_trades_market_plot,
connect_sqlite_read_only,
create_total_pnl_histogram,
find_repo_root,
format_pair_name_for_display,
format_pair_names_for_display,
infer_trading_day_start_ns,
list_candidate_files,
load_pair_market_data,
load_selector_pair_rankings, load_selector_pair_rankings,
load_trading_instructions, load_trading_instructions,
normalize_directory,
pair_assets_and_quote, pair_assets_and_quote,
parse_selector_instrument,
parse_mr_score_final, parse_mr_score_final,
rank_selector_pairs, rank_selector_pairs,
read_only_sqlite_uri,
show_interactive_dataframe,
sorted_pair_names,
) )
@@ -89,6 +107,318 @@ def test_pair_assets_and_quote_parses_two_leg_pair():
assert pair_assets_and_quote("ADA:USD-BTC:USD") == (("ADA", "BTC"), "USD") assert pair_assets_and_quote("ADA:USD-BTC:USD") == (("ADA", "BTC"), "USD")
def test_infer_trading_day_start_ns_uses_utc_midnight():
trd_inst_df = pd.DataFrame(
{
"time_ns": [
pd.Timestamp("2026-06-17T02:25:00Z").value,
pd.Timestamp("2026-06-17T00:01:00Z").value,
]
}
)
assert infer_trading_day_start_ns(trd_inst_df) == pd.Timestamp(
"2026-06-17T00:00:00Z"
).value
def test_parse_selector_instrument_splits_exchange_account_and_instrument_id():
assert parse_selector_instrument("COINBASE_AT:PAIR-ADA-USD") == (
"COINBASE_AT",
"PAIR-ADA-USD",
)
def test_load_pair_market_data_maps_selector_instruments_and_relative_close():
trading_day_start_ns = 10
conn = sqlite3.connect(":memory:")
conn.execute(
"""
CREATE TABLE selector_pairs (
pair_name TEXT,
instrument_a TEXT,
instrument_b TEXT
)
"""
)
conn.execute(
"""
CREATE TABLE ohlcv_1min (
tstamp TEXT,
tstamp_ns INTEGER,
exch_acct TEXT,
instrument_id TEXT,
close REAL
)
"""
)
conn.execute(
"INSERT INTO selector_pairs VALUES (?, ?, ?)",
(
"AAA:USD-BBB:USD",
"EXCH_A:PAIR-AAA-USD",
"EXCH_B:PAIR-BBB-USD",
),
)
conn.executemany(
"INSERT INTO ohlcv_1min VALUES (?, ?, ?, ?, ?)",
[
("pre", 9, "EXCH_A", "PAIR-AAA-USD", 90.0),
("t0", 10, "EXCH_A", "PAIR-AAA-USD", 100.0),
("t1", 11, "EXCH_A", "PAIR-AAA-USD", 110.0),
("pre", 9, "EXCH_B", "PAIR-BBB-USD", 55.0),
("t0", 10, "EXCH_B", "PAIR-BBB-USD", 50.0),
("t1", 11, "EXCH_B", "PAIR-BBB-USD", 45.0),
("t0", 10, "OTHER", "PAIR-AAA-USD", 999.0),
],
)
market_data = load_pair_market_data(
conn,
"AAA:USD-BBB:USD",
trading_day_start_ns=trading_day_start_ns,
)
assert market_data[
["asset", "exch_acct", "instrument_id", "close", "initial_close"]
].to_dict("records") == [
{
"asset": "AAA",
"exch_acct": "EXCH_A",
"instrument_id": "PAIR-AAA-USD",
"close": 100.0,
"initial_close": 100.0,
},
{
"asset": "AAA",
"exch_acct": "EXCH_A",
"instrument_id": "PAIR-AAA-USD",
"close": 110.0,
"initial_close": 100.0,
},
{
"asset": "BBB",
"exch_acct": "EXCH_B",
"instrument_id": "PAIR-BBB-USD",
"close": 50.0,
"initial_close": 50.0,
},
{
"asset": "BBB",
"exch_acct": "EXCH_B",
"instrument_id": "PAIR-BBB-USD",
"close": 45.0,
"initial_close": 50.0,
},
]
assert market_data["time_ns"].tolist() == [10, 11, 10, 11]
assert market_data["relative_close"].tolist() == [
0.0,
pytest.approx(0.1),
0.0,
pytest.approx(-0.1),
]
def test_load_pair_market_data_requires_market_rows_for_both_assets():
trading_day_start_ns = 1
conn = sqlite3.connect(":memory:")
conn.execute(
"""
CREATE TABLE selector_pairs (
pair_name TEXT,
instrument_a TEXT,
instrument_b TEXT
)
"""
)
conn.execute(
"""
CREATE TABLE ohlcv_1min (
tstamp TEXT,
tstamp_ns INTEGER,
exch_acct TEXT,
instrument_id TEXT,
close REAL
)
"""
)
conn.execute(
"INSERT INTO selector_pairs VALUES (?, ?, ?)",
(
"AAA:USD-BBB:USD",
"EXCH_A:PAIR-AAA-USD",
"EXCH_B:PAIR-BBB-USD",
),
)
conn.execute(
"INSERT INTO ohlcv_1min VALUES (?, ?, ?, ?, ?)",
("t1", 1, "EXCH_A", "PAIR-AAA-USD", 100.0),
)
with pytest.raises(
ValueError,
match=r"ohlcv_1min does not contain market data for asset\(s\): BBB",
):
load_pair_market_data(
conn,
"AAA:USD-BBB:USD",
trading_day_start_ns=trading_day_start_ns,
)
def test_load_pair_market_data_requires_time_zero_close_for_each_asset():
trading_day_start_ns = 1
conn = sqlite3.connect(":memory:")
conn.execute(
"""
CREATE TABLE selector_pairs (
pair_name TEXT,
instrument_a TEXT,
instrument_b TEXT
)
"""
)
conn.execute(
"""
CREATE TABLE ohlcv_1min (
tstamp TEXT,
tstamp_ns INTEGER,
exch_acct TEXT,
instrument_id TEXT,
close REAL
)
"""
)
conn.execute(
"INSERT INTO selector_pairs VALUES (?, ?, ?)",
(
"AAA:USD-BBB:USD",
"EXCH_A:PAIR-AAA-USD",
"EXCH_B:PAIR-BBB-USD",
),
)
conn.executemany(
"INSERT INTO ohlcv_1min VALUES (?, ?, ?, ?, ?)",
[
("t1", 1, "EXCH_A", "PAIR-AAA-USD", None),
("t2", 2, "EXCH_A", "PAIR-AAA-USD", 110.0),
("t1", 1, "EXCH_B", "PAIR-BBB-USD", 50.0),
],
)
with pytest.raises(
ValueError,
match=r"ohlcv_1min initial close must be positive for asset\(s\): AAA",
):
load_pair_market_data(
conn,
"AAA:USD-BBB:USD",
trading_day_start_ns=trading_day_start_ns,
)
def test_load_pair_market_data_requires_exact_trading_day_start_row():
trading_day_start_ns = 10
conn = sqlite3.connect(":memory:")
conn.execute(
"""
CREATE TABLE selector_pairs (
pair_name TEXT,
instrument_a TEXT,
instrument_b TEXT
)
"""
)
conn.execute(
"""
CREATE TABLE ohlcv_1min (
tstamp TEXT,
tstamp_ns INTEGER,
exch_acct TEXT,
instrument_id TEXT,
close REAL
)
"""
)
conn.execute(
"INSERT INTO selector_pairs VALUES (?, ?, ?)",
(
"AAA:USD-BBB:USD",
"EXCH_A:PAIR-AAA-USD",
"EXCH_B:PAIR-BBB-USD",
),
)
conn.executemany(
"INSERT INTO ohlcv_1min VALUES (?, ?, ?, ?, ?)",
[
("t1", 11, "EXCH_A", "PAIR-AAA-USD", 110.0),
("t0", 10, "EXCH_B", "PAIR-BBB-USD", 50.0),
],
)
with pytest.raises(
ValueError,
match=(
"ohlcv_1min does not contain trading-day start close for "
r"asset\(s\): AAA"
),
):
load_pair_market_data(
conn,
"AAA:USD-BBB:USD",
trading_day_start_ns=trading_day_start_ns,
)
def test_create_pair_trades_market_plot_adds_relative_lines_and_trade_markers():
market_data = pd.DataFrame(
{
"pair_name": ["AAA:USD-BBB:USD"] * 4,
"asset": ["AAA", "AAA", "BBB", "BBB"],
"time": ["t1", "t2", "t1", "t2"],
"time_ns": [1, 2, 1, 2],
"close": [100.0, 110.0, 50.0, 45.0],
"initial_close": [100.0, 100.0, 50.0, 50.0],
"relative_close": [0.0, 0.1, 0.0, -0.1],
}
)
theo_executions = pd.DataFrame(
{
"time": ["t1.5", "t2.5"],
"time_ns": [15, 25],
"asset": ["AAA", "BBB"],
"action": ["TARGET", "CLOSE"],
"side": ["BUY", "SELL"],
"size": [2.0, -3.0],
"price": [105.0, 40.0],
}
)
figure = create_pair_trades_market_plot(
"AAA:USD-BBB:USD",
market_data,
theo_executions,
)
assert [trace.name for trace in figure.data] == [
"AAA close",
"AAA BUY",
"BBB close",
"BBB SELL",
]
assert figure.data[0].y.tolist() == [0.0, 0.1]
assert figure.data[1].marker.symbol == "triangle-up"
assert figure.data[1].marker.color == "darkgreen"
assert figure.data[1].x.tolist() == ["t1.5"]
assert figure.data[1].y.tolist() == [pytest.approx(0.05)]
assert figure.data[3].marker.symbol == "triangle-down"
assert figure.data[3].marker.color == "darkred"
assert figure.data[3].x.tolist() == ["t2.5"]
assert figure.data[3].y.tolist() == [pytest.approx(-0.2)]
assert figure.layout.xaxis.range == ("t1", "t2")
def test_calculate_pair_theo_ret_replaces_targets_and_closes_open_position(): def test_calculate_pair_theo_ret_replaces_targets_and_closes_open_position():
trd_inst_df = pd.DataFrame( trd_inst_df = pd.DataFrame(
{ {
@@ -118,7 +448,297 @@ def test_calculate_pair_theo_ret_replaces_targets_and_closes_open_position():
assert theo_ret == { assert theo_ret == {
"pair_name": "AAA:USD-BBB:USD", "pair_name": "AAA:USD-BBB:USD",
"realized_pnl": pytest.approx(24.0), "num_trades": 6,
"realized_pnl": pytest.approx(0.1),
"unrealized_pnl": 0.0,
}
def test_calculate_pair_theo_executions_uses_target_deltas_and_signed_cash():
trd_inst_df = pd.DataFrame(
{
"time_ns": [1, 2, 3, 4],
"tstamp": ["t1", "t2", "t3", "t4"],
"data": [
(
'{"action":"CLOSE","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"95"},'
'"BBB":{"reference_price":"55"}}}'
),
(
'{"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":"110","strength":"0.015"},'
'"BBB":{"reference_price":"45","strength":"-0.01"}}}'
),
(
'{"action":"CLOSE","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"120"},'
'"BBB":{"reference_price":"40"}}}'
),
],
}
)
executions = calculate_pair_theo_executions("AAA:USD-BBB:USD", trd_inst_df)
display_columns = [
"time",
"asset",
"action",
"side",
"strength",
"size",
"price",
"usd_value",
]
execution_rows = executions[display_columns].to_dict("records")
assert execution_rows[:4] == [
{
"time": "t2",
"asset": "AAA",
"action": "TARGET",
"side": "BUY",
"strength": 0.01,
"size": 1.0,
"price": 100.0,
"usd_value": -100.0,
},
{
"time": "t2",
"asset": "BBB",
"action": "TARGET",
"side": "SELL",
"strength": -0.02,
"size": -4.0,
"price": 50.0,
"usd_value": 200.0,
},
{
"time": "t3",
"asset": "AAA",
"action": "TARGET",
"side": "BUY",
"strength": 0.015,
"size": pytest.approx(0.36363636363636365),
"price": 110.0,
"usd_value": pytest.approx(-40.0),
},
{
"time": "t3",
"asset": "BBB",
"action": "TARGET",
"side": "BUY",
"strength": -0.01,
"size": pytest.approx(1.7777777777777777),
"price": 45.0,
"usd_value": pytest.approx(-80.0),
},
]
assert execution_rows[4] | {"strength": None} == {
"time": "t4",
"asset": "AAA",
"action": "CLOSE",
"side": "SELL",
"strength": None,
"size": pytest.approx(-1.3636363636363638),
"price": 120.0,
"usd_value": pytest.approx(163.63636363636365),
}
assert execution_rows[5] | {"strength": None} == {
"time": "t4",
"asset": "BBB",
"action": "CLOSE",
"side": "BUY",
"strength": None,
"size": pytest.approx(2.2222222222222223),
"price": 40.0,
"usd_value": pytest.approx(-88.88888888888889),
}
assert executions["strength"].iloc[:4].tolist() == [0.01, -0.02, 0.015, -0.01]
assert executions["strength"].iloc[4:].isna().all()
def test_calculate_pair_theo_executions_skips_small_target_strength_changes():
trd_inst_df = pd.DataFrame(
{
"time_ns": [1, 2, 3, 4],
"tstamp": ["t1", "t2", "t3", "t4"],
"data": [
(
'{"action":"TARGET","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100","strength":"0.5"},'
'"BBB":{"reference_price":"50","strength":"-0.5"}}}'
),
(
'{"action":"TARGET","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100","strength":"0.53"},'
'"BBB":{"reference_price":"50","strength":"-0.47"}}}'
),
(
'{"action":"TARGET","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100","strength":"0.7"},'
'"BBB":{"reference_price":"50","strength":"-0.7"}}}'
),
(
'{"action":"CLOSE","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100"},'
'"BBB":{"reference_price":"50"}}}'
),
],
}
)
executions = calculate_pair_theo_executions(
"AAA:USD-BBB:USD",
trd_inst_df,
min_pctg_change=25,
)
execution_rows = executions[["time", "asset", "action", "strength", "size"]]
assert execution_rows.iloc[:4].to_dict("records") == [
{
"time": "t1",
"asset": "AAA",
"action": "TARGET",
"strength": 0.5,
"size": 50.0,
},
{
"time": "t1",
"asset": "BBB",
"action": "TARGET",
"strength": -0.5,
"size": -100.0,
},
{
"time": "t3",
"asset": "AAA",
"action": "TARGET",
"strength": 0.7,
"size": 20.0,
},
{
"time": "t3",
"asset": "BBB",
"action": "TARGET",
"strength": -0.7,
"size": -40.0,
},
]
assert execution_rows.iloc[4].to_dict() | {"strength": None} == {
"time": "t4",
"asset": "AAA",
"action": "CLOSE",
"strength": None,
"size": -70.0,
}
assert execution_rows.iloc[5].to_dict() | {"strength": None} == {
"time": "t4",
"asset": "BBB",
"action": "CLOSE",
"strength": None,
"size": 140.0,
}
def test_calculate_pair_theo_executions_trades_threshold_boundary_and_zero_crossing():
trd_inst_df = pd.DataFrame(
{
"time_ns": [1, 2, 3, 4],
"tstamp": ["t1", "t2", "t3", "t4"],
"data": [
(
'{"action":"TARGET","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100","strength":"0.4"},'
'"BBB":{"reference_price":"50","strength":"0"}}}'
),
(
'{"action":"TARGET","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100","strength":"0.5"},'
'"BBB":{"reference_price":"50","strength":"0"}}}'
),
(
'{"action":"TARGET","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100","strength":"0.51"},'
'"BBB":{"reference_price":"50","strength":"0.1"}}}'
),
(
'{"action":"CLOSE","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100"},'
'"BBB":{"reference_price":"50"}}}'
),
],
}
)
executions = calculate_pair_theo_executions(
"AAA:USD-BBB:USD",
trd_inst_df,
min_pctg_change=25,
)
assert executions[["time", "asset", "action", "strength", "size"]].iloc[
:3
].to_dict("records") == [
{
"time": "t1",
"asset": "AAA",
"action": "TARGET",
"strength": 0.4,
"size": 40.0,
},
{
"time": "t2",
"asset": "AAA",
"action": "TARGET",
"strength": 0.5,
"size": 10.0,
},
{
"time": "t3",
"asset": "BBB",
"action": "TARGET",
"strength": 0.1,
"size": 20.0,
},
]
def test_calculate_pair_theo_ret_uses_execution_cash_flows():
trd_inst_df = pd.DataFrame(
{
"time_ns": [1, 2, 3],
"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":"110","strength":"0.015"},'
'"BBB":{"reference_price":"45","strength":"-0.01"}}}'
),
(
'{"action":"CLOSE","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"120"},'
'"BBB":{"reference_price":"40"}}}'
),
],
}
)
assert calculate_pair_theo_ret("AAA:USD-BBB:USD", trd_inst_df) == {
"pair_name": "AAA:USD-BBB:USD",
"num_trades": 6,
"realized_pnl": pytest.approx(0.5474747474747474),
"unrealized_pnl": 0.0, "unrealized_pnl": 0.0,
} }
@@ -144,6 +764,7 @@ def test_calculate_pair_theo_ret_ignores_unmatched_quote_and_close_without_targe
assert calculate_pair_theo_ret("AAA:USD-BBB:USD", trd_inst_df) == { assert calculate_pair_theo_ret("AAA:USD-BBB:USD", trd_inst_df) == {
"pair_name": "AAA:USD-BBB:USD", "pair_name": "AAA:USD-BBB:USD",
"num_trades": 0,
"realized_pnl": 0.0, "realized_pnl": 0.0,
"unrealized_pnl": 0.0, "unrealized_pnl": 0.0,
} }
@@ -180,20 +801,297 @@ def test_calculate_ranked_pairs_theo_ret_preserves_pairs_without_instructions():
{ {
"pair_name": "AAA:USD-BBB:USD", "pair_name": "AAA:USD-BBB:USD",
"mr_ranking": 1, "mr_ranking": 1,
"realized_pnl": 20.0, "num_trades": 4,
"realized_pnl": 0.3,
"unrealized_pnl": 0.0, "unrealized_pnl": 0.0,
}, },
{ {
"pair_name": "CCC:USD-DDD:USD", "pair_name": "CCC:USD-DDD:USD",
"mr_ranking": 2, "mr_ranking": 2,
"num_trades": 0,
"realized_pnl": 0.0, "realized_pnl": 0.0,
"unrealized_pnl": 0.0, "unrealized_pnl": 0.0,
}, },
] ]
def test_calculate_ranked_pairs_theo_ret_applies_min_pctg_change():
rankings = pd.DataFrame(
{
"pair_name": ["AAA:USD-BBB:USD"],
"pair_rank": pd.Series([1], dtype="Int64"),
}
)
trd_inst_df = pd.DataFrame(
{
"time_ns": [1, 2, 3],
"data": [
(
'{"action":"TARGET","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100","strength":"0.5"},'
'"BBB":{"reference_price":"50","strength":"-0.5"}}}'
),
(
'{"action":"TARGET","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100","strength":"0.53"},'
'"BBB":{"reference_price":"50","strength":"-0.47"}}}'
),
(
'{"action":"CLOSE","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100"},'
'"BBB":{"reference_price":"50"}}}'
),
],
}
)
result = calculate_ranked_pairs_theo_ret(
rankings,
trd_inst_df,
min_pctg_change=25,
)
assert result.to_dict("records") == [
{
"pair_name": "AAA:USD-BBB:USD",
"mr_ranking": 1,
"num_trades": 4,
"realized_pnl": 0.0,
"unrealized_pnl": 0.0,
}
]
def test_calculate_ranked_pairs_theo_ret_validates_min_pctg_change():
rankings = pd.DataFrame(
{
"pair_name": ["AAA:USD-BBB:USD"],
"pair_rank": pd.Series([1], dtype="Int64"),
}
)
trd_inst_df = pd.DataFrame({"time_ns": [], "data": []})
with pytest.raises(ValueError, match="min_pctg_change must be non-negative"):
calculate_ranked_pairs_theo_ret(
rankings,
trd_inst_df,
min_pctg_change=-1,
)
def test_load_trading_instructions_validates_required_table(): def test_load_trading_instructions_validates_required_table():
conn = sqlite3.connect(":memory:") conn = sqlite3.connect(":memory:")
with pytest.raises(ValueError, match="missing required table: trading_instructions"): with pytest.raises(ValueError, match="missing required table: trading_instructions"):
load_trading_instructions(conn) load_trading_instructions(conn)
def test_find_repo_root_and_normalize_directory():
repo_root = find_repo_root(Path("notebooks").resolve())
assert repo_root.name == "stat_pairs_backtest"
assert normalize_directory("data", repo_root) == (repo_root / "data").resolve()
def test_list_candidate_files_prefers_result_databases(tmp_path):
names = [
"20260617.spbt_md.db",
"20260617.spbt_results.db",
"20260617.spbt_selector_results.db",
"notes.txt",
]
for name in names:
(tmp_path / name).write_text("", encoding="utf-8")
assert [path.name for path in list_candidate_files(tmp_path)] == [
"20260617.spbt_results.db",
"20260617.spbt_selector_results.db",
"20260617.spbt_md.db",
]
assert [path.name for path in list_candidate_files(tmp_path, show_all=True)] == [
"20260617.spbt_results.db",
"20260617.spbt_selector_results.db",
"20260617.spbt_md.db",
"notes.txt",
]
def test_connect_sqlite_read_only_uses_read_only_uri(tmp_path):
db_path = tmp_path / "example name.sqlite"
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE sample (value INTEGER)")
conn.execute("INSERT INTO sample (value) VALUES (1)")
conn.commit()
conn.close()
assert read_only_sqlite_uri(db_path).endswith("?mode=ro")
read_only_conn = connect_sqlite_read_only(db_path)
try:
assert read_only_conn.execute("SELECT value FROM sample").fetchone() == (1,)
with pytest.raises(sqlite3.OperationalError, match="readonly"):
read_only_conn.execute("INSERT INTO sample (value) VALUES (2)")
finally:
read_only_conn.close()
def test_add_total_pnl_and_histogram_builder():
pair_theo_ret = pd.DataFrame(
{
"pair_name": ["AAA:USD-BBB:USD", "PAIR_B"],
"mr_ranking": [1, 2],
"realized_pnl": [1.5, -0.5],
"unrealized_pnl": [0.25, 0.0],
}
)
with_total = add_total_pnl(pair_theo_ret)
histogram = create_total_pnl_histogram(pair_theo_ret)
assert with_total["total_pnl"].tolist() == [1.75, -0.5]
assert histogram.data[0].type == "histogram"
assert histogram.data[0].x.tolist() == [1.75, -0.5]
assert histogram.data[0].xbins.start is None
assert histogram.data[0].xbins.size is None
def test_format_pair_names_for_display_removes_usd_suffix_without_mutating_source():
pair_theo_ret = pd.DataFrame(
{
"pair_name": [
"AAA:USD-BBB:USD",
"CCC:EUR-DDD:EUR",
"AAA:USDT-BBB:USDT",
None,
],
"realized_pnl": [1.0, 2.0, 3.0, 4.0],
}
)
formatted = format_pair_names_for_display(pair_theo_ret)
assert format_pair_name_for_display("AAA:USD-BBB:USD") == "AAA-BBB"
assert formatted["pair_name"].tolist() == [
"AAA-BBB",
"CCC:EUR-DDD:EUR",
"AAA:USDT-BBB:USDT",
None,
]
assert pair_theo_ret["pair_name"].tolist() == [
"AAA:USD-BBB:USD",
"CCC:EUR-DDD:EUR",
"AAA:USDT-BBB:USDT",
None,
]
def test_show_interactive_dataframe_uses_sortable_grid_defaults(monkeypatch):
calls = []
def fake_show(dataframe, **kwargs):
calls.append((dataframe, kwargs))
import itables
monkeypatch.setattr(itables, "show", fake_show)
dataframe = pd.DataFrame({"pair_name": ["AAA-BBB"], "num_trades": [2]})
show_interactive_dataframe(
dataframe,
table_id="pair-theo-ret-grid",
pageLength=50,
)
assert len(calls) == 1
assert calls[0][0] is dataframe
assert calls[0][1] == {
"paging": True,
"pageLength": 50,
"scrollX": True,
"ordering": True,
"showIndex": False,
"maxBytes": "8MB",
"classes": "display compact stripe hover",
"css": calls[0][1]["css"],
"table_id": "pair-theo-ret-grid",
}
assert "background-color: #ffffff" in calls[0][1]["css"]
assert "color: #000000" in calls[0][1]["css"]
def test_sorted_pair_names_and_dropdown_use_alphabetical_unique_pairs():
selector_pair_rankings = pd.DataFrame(
{
"pair_name": [
"BTC:USD-ETH:USD",
"ADA:USD-BTC:USD",
"BTC:USD-ETH:USD",
]
}
)
assert sorted_pair_names(selector_pair_rankings) == [
"ADA:USD-BTC:USD",
"BTC:USD-ETH:USD",
]
dropdown = create_pair_name_dropdown(selector_pair_rankings)
assert dropdown.options == (
("ADA-BTC", "ADA:USD-BTC:USD"),
("BTC-ETH", "BTC:USD-ETH:USD"),
)
assert dropdown.value == "ADA:USD-BTC:USD"
def test_example_database_pair_theo_executions_include_strength():
db_path = Path("data/20260617.spbt_results.db")
if not db_path.exists():
pytest.skip(f"example database not available: {db_path}")
conn = connect_sqlite_read_only(db_path)
try:
rankings = load_selector_pair_rankings(conn)
trading_instructions = load_trading_instructions(conn)
finally:
conn.close()
empty_pair_executions = calculate_pair_theo_executions(
"ADA:USD-BNB:USD",
trading_instructions,
)
non_empty_pair_executions = calculate_pair_theo_executions(
"ADA:USD-BTC:USD",
trading_instructions,
)
assert "strength" in empty_pair_executions.columns
assert "strength" in non_empty_pair_executions.columns
assert len(rankings) == 78
assert len(non_empty_pair_executions) > 0
assert (
non_empty_pair_executions.loc[
non_empty_pair_executions["action"] == "TARGET",
"strength",
]
.notna()
.all()
)
assert (
non_empty_pair_executions.loc[
non_empty_pair_executions["action"] == "CLOSE",
"strength",
]
.isna()
.all()
)
first_target_execution = non_empty_pair_executions[
non_empty_pair_executions["action"] == "TARGET"
].iloc[0]
assert first_target_execution["size"] == pytest.approx(
10_000 * first_target_execution["strength"] / first_target_execution["price"]
)
assert first_target_execution["usd_value"] == pytest.approx(
-first_target_execution["size"] * first_target_execution["price"]
)