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
+207 -190
View File
@@ -23,39 +23,43 @@
"metadata": {},
"outputs": [],
"source": [
"from html import escape\n",
"from pathlib import Path\n",
"import sqlite3\n",
"import importlib\n",
"import sys\n",
"from urllib.parse import quote\n",
"\n",
"from IPython.display import display\n",
"import ipywidgets as widgets\n",
"import pandas as pd\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",
"def find_repo_root(start: Path | None = None) -> Path:\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",
"import scripts.spbt_day as spbt_day\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",
"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",
"SQLITE_EXTENSIONS = {\".db\", \".sqlite\", \".sqlite3\"}\n",
"\n",
"selected_db_path: Path | None = None\n",
"\n",
"REPO_ROOT, DEFAULT_DATA_DIR"
]
@@ -67,136 +71,12 @@
"metadata": {},
"outputs": [],
"source": [
"directory_input = widgets.Text(\n",
" value=str(DEFAULT_DATA_DIR),\n",
" description=\"Directory\",\n",
" continuous_update=False,\n",
" layout=widgets.Layout(width=\"100%\"),\n",
" style={\"description_width\": \"90px\"},\n",
"db_selector = create_database_file_selector(\n",
" default_data_dir=DEFAULT_DATA_DIR,\n",
" repo_root=REPO_ROOT,\n",
")\n",
"\n",
"show_all_files = widgets.Checkbox(\n",
" 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()"
"display(db_selector.widget)"
]
},
{
@@ -206,23 +86,8 @@
"metadata": {},
"outputs": [],
"source": [
"def selected_database_path() -> Path:\n",
" \"\"\"Return the interactively selected SQLite result path.\"\"\"\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",
"selected_database_path = db_selector.selected_database_path\n",
"connect_selected_database = db_selector.connect_selected_database\n",
"\n",
"# Later notebook sections can call selected_database_path() or connect_selected_database()."
]
@@ -252,24 +117,11 @@
"finally:\n",
" conn.close()\n",
"\n",
"selector_pair_rankings"
]
},
{
"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",
"selector_pair_rankings_display = format_pair_names_for_display(\n",
" selector_pair_rankings[[\"pair_rank\", \"pair_name\", \"mr_score_final\"]]\n",
")\n",
"\n",
"selector_pair_ranking_summary"
"with pd.option_context(\"display.max_rows\", None):\n",
" display(selector_pair_rankings_display)"
]
},
{
@@ -279,9 +131,26 @@
"source": [
"## Theoretical Return by Pair\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",
"`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",
" conn.close()\n",
"\n",
"trading_instructions"
"print(f\"Loaded {len(trading_instructions):,} trading instruction rows.\")"
]
},
{
@@ -307,12 +176,160 @@
"metadata": {},
"outputs": [],
"source": [
"pair_theo_ret = calculate_ranked_pairs_theo_ret(\n",
" selector_pair_rankings,\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",
" 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",
"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"
]
}
],