Compare commits

...

4 Commits

Author SHA1 Message Date
Oleg Sheynin 9d553dcf1a Release v1.0.3 2026-07-29 01:01:08 +00:00
Oleg Sheynin a3e5acd765 Release v1.0.2 2026-07-29 00:39:10 +00:00
Oleg Sheynin 49c91e5d85 Release v1.0.1 2026-07-28 23:45:59 +00:00
Oleg Sheynin 400bd41e56 notbebooks initial 2026-07-28 00:50:32 +00:00
10 changed files with 3369 additions and 2 deletions
+2
View File
@@ -18,5 +18,7 @@ data/*
results/*
!results/.gitkeep
data
cvttpy
tmp/
+60 -1
View File
@@ -4,7 +4,66 @@ All notable changes to this project are documented in this file.
## Unreleased
- No unreleased changes yet.
No unreleased changes yet.
## 2026-07-29 v1.0.3
- Removed invalid fixed sizing mode from Panel Tabulator grids to avoid Bokeh
layout warnings while preserving compact table layout.
- Changed the Panel Calculate action to refresh the result-file list before
loading data and removed the standalone Panel Refresh button.
## 2026-07-29 v1.0.2
- Added a Panel application for single-day SPBT result analysis with result-file
selection, minimum TARGET-change input, pair TheoRet table, pair selector,
selected-pair execution table, and market/trade chart.
- Added a launcher script for the Panel application.
- Changed notebook and Panel pair analysis to use per-row Analyze actions from
the Pair TheoRet grid, deferring selected-pair calculations until clicked.
- Adjusted Panel sizing so key controls use compact widths and Pair TheoRet uses
content width with vertical scrolling instead of full-width paginated layout.
- Added a FastListTemplate shell to the Panel application for sidebar controls
and configurable app color accents.
- Made Plotly chart panes use all available horizontal space.
## 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
-1
View File
@@ -1 +0,0 @@
+354
View File
@@ -0,0 +1,354 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "single-day-title",
"metadata": {},
"source": [
"# Single-Day Backtest Result Analysis\n",
"\n",
"This notebook analyzes the result of one single-day backtest stored in a SQLite database. Development is staged; Step 1 only selects the database file that later sections will read.\n",
"\n",
"Input assumptions for Step 1:\n",
"\n",
"- The default data directory is `data/` at the repository root.\n",
"- SQLite result files usually use `.db`, `.sqlite`, or `.sqlite3` extensions.\n",
"- The directory can be changed interactively if the result file lives elsewhere."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "imports-and-paths",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import importlib\n",
"import sys\n",
"\n",
"from IPython.display import display\n",
"import ipywidgets as widgets\n",
"import pandas as pd\n",
"import panel as pn\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",
"import scripts.spbt_day as spbt_day\n",
"\n",
"spbt_day = importlib.reload(spbt_day)\n",
"pn.extension(\"tabulator\", \"plotly\")\n",
"\n",
"ANALYZE_BUTTON_COLUMN = spbt_day.ANALYZE_BUTTON_COLUMN\n",
"SELECTED_PAIR_EXECUTION_DISPLAY_COLUMNS = spbt_day.SELECTED_PAIR_EXECUTION_DISPLAY_COLUMNS\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_theo_ret_analyze_grid = spbt_day.create_pair_theo_ret_analyze_grid\n",
"create_pair_trades_market_plot = spbt_day.create_pair_trades_market_plot\n",
"create_selected_pair_executions_grid = spbt_day.create_selected_pair_executions_grid\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",
"format_pair_theo_ret_for_analyze_grid = spbt_day.format_pair_theo_ret_for_analyze_grid\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",
"pair_name_from_analyze_event = spbt_day.pair_name_from_analyze_event\n",
"show_interactive_dataframe = spbt_day.show_interactive_dataframe\n",
"\n",
"REPO_ROOT = find_repo_root()\n",
"DEFAULT_DATA_DIR = REPO_ROOT / \"data\"\n",
"\n",
"REPO_ROOT, DEFAULT_DATA_DIR"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "database-file-selector",
"metadata": {},
"outputs": [],
"source": [
"db_selector = create_database_file_selector(\n",
" default_data_dir=DEFAULT_DATA_DIR,\n",
" repo_root=REPO_ROOT,\n",
")\n",
"\n",
"display(db_selector.widget)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "selected-database-helpers",
"metadata": {},
"outputs": [],
"source": [
"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()."
]
},
{
"cell_type": "markdown",
"id": "selector-pair-rankings-context",
"metadata": {},
"source": [
"## Selector Pair Rankings\n",
"\n",
"Load `selector_pairs.pair_name` and `selector_pairs.mr_score` from the selected SQLite database. The JSON field `mr_score.final` is parsed as a numeric score and ranked descending with dense ranks, so tied scores share the same rank and the next distinct score gets the next rank.\n",
"\n",
"Rows with missing, malformed, non-numeric, or non-finite `mr_score.final` values are preserved, sorted after ranked rows, and marked in `mr_score_parse_status`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "load-selector-pair-rankings",
"metadata": {},
"outputs": [],
"source": [
"conn = connect_selected_database()\n",
"try:\n",
" selector_pair_rankings = load_selector_pair_rankings(conn)\n",
"finally:\n",
" conn.close()\n",
"\n",
"selector_pair_rankings_display = format_pair_names_for_display(\n",
" selector_pair_rankings[[\"pair_rank\", \"pair_name\", \"mr_score_final\"]]\n",
")\n",
"with pd.option_context(\"display.max_rows\", None):\n",
" display(selector_pair_rankings_display)"
]
},
{
"cell_type": "markdown",
"id": "theoretical-return-context",
"metadata": {},
"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` 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",
"`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)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "load-trading-instructions",
"metadata": {},
"outputs": [],
"source": [
"conn = connect_selected_database()\n",
"try:\n",
" trading_instructions = load_trading_instructions(conn)\n",
"finally:\n",
" conn.close()\n",
"\n",
"print(f\"Loaded {len(trading_instructions):,} trading instruction rows.\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "calculate-pair-theoretical-returns",
"metadata": {},
"outputs": [],
"source": [
"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_theo_ret_for_analyze_grid(pair_theo_ret)\n",
"pair_theo_ret_grid = create_pair_theo_ret_analyze_grid(\n",
" pair_theo_ret_display,\n",
" height=520,\n",
")\n",
"display(pair_theo_ret_grid)"
]
},
{
"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",
"total_pnl_histogram_pane = pn.pane.Plotly(\n",
" total_pnl_histogram,\n",
" height=360,\n",
" sizing_mode=\"stretch_width\",\n",
")\n",
"\n",
"display(total_pnl_histogram_pane)"
]
},
{
"cell_type": "markdown",
"id": "individual-pair-analysis-context",
"metadata": {},
"source": [
"## Individual Pair Analysis\n",
"\n",
"Click the Analyze button in the Pair TheoRet grid to load detailed follow-up analysis for that row. The selected-pair execution table and market/trade chart are not calculated until an Analyze button is clicked."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "individual-pair-analysis",
"metadata": {},
"outputs": [],
"source": [
"selected_pair_name = None\n",
"selected_pair_theo_executions = pd.DataFrame()\n",
"selected_pair_theo_executions_display = pd.DataFrame(\n",
" columns=SELECTED_PAIR_EXECUTION_DISPLAY_COLUMNS\n",
")\n",
"selected_pair_market_data = pd.DataFrame()\n",
"selected_pair_market_trades_plot = None\n",
"\n",
"selected_pair_message = pn.pane.Markdown(\n",
" \"Click Analyze in the Pair TheoRet grid to load individual-pair details.\"\n",
")\n",
"selected_pair_theo_executions_grid = create_selected_pair_executions_grid(\n",
" selected_pair_theo_executions_display,\n",
" height=360,\n",
")\n",
"selected_pair_market_trades_plot_pane = pn.pane.Plotly(\n",
" None,\n",
" height=520,\n",
" sizing_mode=\"stretch_width\",\n",
")\n",
"\n",
"\n",
"def analyze_pair_click(event):\n",
" global selected_pair_name\n",
" global selected_pair_theo_executions\n",
" global selected_pair_theo_executions_display\n",
" global selected_pair_market_data\n",
" global selected_pair_market_trades_plot\n",
"\n",
" try:\n",
" selected_pair_name = pair_name_from_analyze_event(pair_theo_ret_grid, event)\n",
" selected_pair_message.object = (\n",
" f\"Selected pair: **{format_pair_name_for_display(selected_pair_name)}**\"\n",
" )\n",
"\n",
" 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",
" selected_pair_theo_executions_display = selected_pair_theo_executions.reindex(\n",
" columns=SELECTED_PAIR_EXECUTION_DISPLAY_COLUMNS\n",
" )\n",
" selected_pair_theo_executions_grid.value = selected_pair_theo_executions_display\n",
"\n",
" trading_day_start_ns = infer_trading_day_start_ns(trading_instructions)\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",
" selected_pair_market_trades_plot_pane.object = selected_pair_market_trades_plot\n",
" except Exception as exc:\n",
" selected_pair_message.object = f\"**Error:** {exc}\"\n",
" selected_pair_market_trades_plot_pane.object = None\n",
"\n",
"\n",
"pair_theo_ret_grid.on_click(analyze_pair_click, column=ANALYZE_BUTTON_COLUMN)\n",
"\n",
"display(\n",
" pn.Column(\n",
" selected_pair_message,\n",
" \"### Theoretical Executions\",\n",
" selected_pair_theo_executions_grid,\n",
" \"### Trades on Market Data\",\n",
" selected_pair_market_trades_plot_pane,\n",
" )\n",
")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "python3.12-venv (3.12.13.final.0)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+296
View File
@@ -0,0 +1,296 @@
"""Panel application for single-day SPBT result analysis."""
from __future__ import annotations
from pathlib import Path
import sys
from typing import Any
import pandas as pd
import panel as pn
APP_DIR = Path(__file__).resolve().parent
REPO_ROOT = APP_DIR.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from scripts import spbt_day
pn.extension("tabulator", "plotly")
PAIR_THEO_RET_SORT_COLUMNS = ["total_pnl", "pair_name"]
PAIR_THEO_RET_DISPLAY_DROP_COLUMNS = ["total_pnl"]
APP_TITLE = "SPBT Day Analysis"
APP_ACCENT_COLOR = "#226c67"
APP_HEADER_COLOR = "#184c47"
class SpbtDayPanelApp:
"""Stateful Panel UI for single-day SPBT analysis."""
def __init__(self, repo_root: Path | None = None) -> None:
self.repo_root = (repo_root or spbt_day.find_repo_root(REPO_ROOT)).resolve()
self.selector_pair_rankings = pd.DataFrame()
self.trading_instructions = pd.DataFrame()
self.pair_theo_ret = pd.DataFrame()
self.selected_pair_theo_executions = pd.DataFrame()
self.selected_pair_name: str | None = None
self.min_pctg_change = 0.0
self.directory_input = pn.widgets.TextInput(
label="Directory",
value=str(self.repo_root / "data"),
)
self.show_all_files = pn.widgets.Checkbox(label="Show all files", value=False)
self.file_select = pn.widgets.Select(
label="SQLite result file",
options={},
width=360,
)
self.min_pctg_change_input = pn.widgets.FloatInput(
label="Mininal TARGET change (%)",
value=0.0,
step=1.0,
width=220,
)
self.calculate_button = pn.widgets.Button(
label="Calculate",
color="primary",
width=110,
)
self.status = pn.pane.Markdown("")
self.pair_theo_ret_table = spbt_day.create_pair_theo_ret_analyze_grid(
pd.DataFrame(),
height=420,
)
self.total_pnl_histogram = pn.pane.Plotly(
None,
height=360,
sizing_mode="stretch_width",
)
self.selected_pair_message = pn.pane.Markdown(
"Click Analyze in the Pair TheoRet grid to load individual-pair details."
)
self.selected_pair_executions_table = spbt_day.create_selected_pair_executions_grid(
height=320,
)
self.selected_pair_market_plot = pn.pane.Plotly(
None,
height=520,
sizing_mode="stretch_width",
)
self.calculate_button.on_click(self.calculate)
self.directory_input.param.watch(self.refresh_files, "value")
self.show_all_files.param.watch(self.refresh_files, "value")
self.pair_theo_ret_table.on_click(
self.analyze_pair_click,
column=spbt_day.ANALYZE_BUTTON_COLUMN,
)
self.refresh_files()
def set_status(self, message: str, *, error: bool = False) -> None:
"""Update visible status text."""
prefix = "**Error:** " if error else ""
self.status.object = f"{prefix}{message}" if message else ""
def selected_database_path(self) -> Path:
"""Return the selected result database path."""
if not self.file_select.value:
raise ValueError("Select a SQLite result file before calculating.")
db_path = Path(str(self.file_select.value)).resolve()
if not db_path.exists():
raise FileNotFoundError(f"Selected database does not exist: {db_path}")
if not db_path.is_file():
raise ValueError(f"Selected database path is not a file: {db_path}")
return db_path
def refresh_files(self, *_events: Any) -> bool:
"""Refresh selectable SQLite files from the configured directory."""
try:
directory = spbt_day.normalize_directory(
self.directory_input.value,
self.repo_root,
)
candidates = spbt_day.list_candidate_files(
directory,
show_all=self.show_all_files.value,
)
except Exception as exc:
self.file_select.options = {}
self.file_select.value = None
self.set_status(str(exc), error=True)
return False
options = {path.name: str(path) for path in candidates}
previous_value = self.file_select.value
self.file_select.options = options
if previous_value in options.values():
self.file_select.value = previous_value
elif options:
self.file_select.value = next(iter(options.values()))
else:
self.file_select.value = None
if options:
self.set_status(f"Found {len(options):,} file(s) in {directory}.")
else:
self.set_status(f"No selectable files found in {directory}.")
return True
def calculate(self, *_events: Any) -> None:
"""Load selected data and calculate all-pair TheoRet."""
self.calculate_button.loading = True
try:
if not self.refresh_files():
return
db_path = self.selected_database_path()
self.min_pctg_change = float(self.min_pctg_change_input.value)
conn = spbt_day.connect_sqlite_read_only(db_path)
try:
self.selector_pair_rankings = spbt_day.load_selector_pair_rankings(conn)
self.trading_instructions = spbt_day.load_trading_instructions(conn)
finally:
conn.close()
self.pair_theo_ret = (
spbt_day.add_total_pnl(
spbt_day.calculate_ranked_pairs_theo_ret(
self.selector_pair_rankings,
self.trading_instructions,
min_pctg_change=self.min_pctg_change,
)
)
.sort_values(
PAIR_THEO_RET_SORT_COLUMNS,
ascending=[True, True],
kind="mergesort",
)
.drop(columns=PAIR_THEO_RET_DISPLAY_DROP_COLUMNS)
.reset_index(drop=True)
)
self.pair_theo_ret_table.value = spbt_day.format_pair_theo_ret_for_analyze_grid(
self.pair_theo_ret
)
self.total_pnl_histogram.object = spbt_day.create_total_pnl_histogram(
self.pair_theo_ret
)
self.clear_selected_pair_analysis()
self.set_status(
f"Calculated {len(self.pair_theo_ret):,} pair row(s) from {db_path.name}."
)
except Exception as exc:
self.set_status(str(exc), error=True)
finally:
self.calculate_button.loading = False
def clear_selected_pair_analysis(self) -> None:
"""Clear individual-pair outputs until a row Analyze button is clicked."""
self.selected_pair_name = None
self.selected_pair_theo_executions = pd.DataFrame()
self.selected_pair_message.object = (
"Click Analyze in the Pair TheoRet grid to load individual-pair details."
)
self.selected_pair_executions_table.value = pd.DataFrame(
columns=spbt_day.SELECTED_PAIR_EXECUTION_DISPLAY_COLUMNS
)
self.selected_pair_market_plot.object = None
def analyze_pair_click(self, event: Any) -> None:
"""Run selected-pair analysis from a Pair TheoRet Analyze button click."""
self.update_selected_pair(
spbt_day.pair_name_from_analyze_event(self.pair_theo_ret_table, event)
)
def analyze_pair_row(self, row: int) -> None:
"""Run selected-pair analysis for a Pair TheoRet table row."""
event = type("AnalyzeEvent", (), {"row": row})()
self.analyze_pair_click(event)
def update_selected_pair(self, pair_name: str) -> None:
"""Calculate selected-pair executions and market plot."""
if self.trading_instructions.empty:
self.clear_selected_pair_analysis()
return
self.selected_pair_name = pair_name
self.selected_pair_message.object = (
f"Selected pair: **{spbt_day.format_pair_name_for_display(pair_name)}**"
)
self.selected_pair_theo_executions = spbt_day.calculate_pair_theo_executions(
pair_name,
self.trading_instructions,
min_pctg_change=self.min_pctg_change,
)
self.selected_pair_executions_table.value = (
self.selected_pair_theo_executions.reindex(
columns=spbt_day.SELECTED_PAIR_EXECUTION_DISPLAY_COLUMNS
)
)
try:
trading_day_start_ns = spbt_day.infer_trading_day_start_ns(
self.trading_instructions
)
conn = spbt_day.connect_sqlite_read_only(self.selected_database_path())
try:
selected_pair_market_data = spbt_day.load_pair_market_data(
conn,
pair_name,
trading_day_start_ns=trading_day_start_ns,
)
finally:
conn.close()
self.selected_pair_market_plot.object = spbt_day.create_pair_trades_market_plot(
pair_name,
selected_pair_market_data,
self.selected_pair_theo_executions,
)
except Exception as exc:
self.selected_pair_market_plot.object = None
self.set_status(str(exc), error=True)
@property
def view(self) -> pn.template.FastListTemplate:
"""Return the app layout."""
controls = pn.Column(
"## Inputs",
self.directory_input,
self.show_all_files,
self.file_select,
self.min_pctg_change_input,
self.calculate_button,
self.status,
width=400,
)
main = pn.Column(
"## Pair TheoRet",
self.pair_theo_ret_table,
self.total_pnl_histogram,
"## Individual Pair",
self.selected_pair_message,
"### Theoretical Executions",
self.selected_pair_executions_table,
"### Trades on Market Data",
self.selected_pair_market_plot,
)
return pn.template.FastListTemplate(
title=APP_TITLE,
sidebar=[controls],
main=[main],
sidebar_width=430,
accent_base_color=APP_ACCENT_COLOR,
header_background=APP_HEADER_COLOR,
main_layout=None,
)
app_controller = SpbtDayPanelApp()
app = app_controller.view
app.servable(title=APP_TITLE)
+4
View File
@@ -1,8 +1,12 @@
# Interactive analysis
ipykernel>=6.29,<7
ipywidgets>=8.1,<9
itables>=2.2,<3
jupyter>=1.1,<2
nbformat>=5.10,<6
pandas>=2.2,<3
panel>=1.5,<2
plotly>=5.24,<7
# Verification
nbmake>=1.5,<2
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"
panel serve panel/spbt_day_panel.py --show "$@"
+1260
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+238
View File
@@ -0,0 +1,238 @@
import importlib.util
import sqlite3
from pathlib import Path
import pandas as pd
def load_panel_app_module():
module_path = Path("panel/spbt_day_panel.py").resolve()
spec = importlib.util.spec_from_file_location("spbt_day_panel_app", module_path)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
def create_panel_fixture_db(db_path: Path) -> None:
trading_day_start_ns = pd.Timestamp("2026-06-17T00:00:00Z").value
conn = sqlite3.connect(db_path)
try:
conn.execute(
"""
CREATE TABLE selector_pairs (
time_ns INTEGER,
tstamp TEXT,
pair_name TEXT,
instrument_a TEXT,
instrument_b TEXT,
mr_score TEXT
)
"""
)
conn.execute(
"""
CREATE TABLE trading_instructions (
time_ns INTEGER,
tstamp TEXT,
book_id TEXT,
strategy_id TEXT,
type TEXT,
data TEXT
)
"""
)
conn.execute(
"""
CREATE TABLE ohlcv_1min (
tstamp TEXT,
tstamp_ns INTEGER,
exch_acct TEXT,
exchange_id TEXT,
instrument_id TEXT,
interval_sec INTEGER,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
vwap REAL,
num_trades INTEGER
)
"""
)
conn.execute(
"INSERT INTO selector_pairs VALUES (?, ?, ?, ?, ?, ?)",
(
10,
"2026-06-17T00:00:00Z",
"AAA:USD-BBB:USD",
"EXCH:PAIR-AAA-USD",
"EXCH:PAIR-BBB-USD",
'{"final":"0.5"}',
),
)
conn.executemany(
"INSERT INTO trading_instructions VALUES (?, ?, ?, ?, ?, ?)",
[
(
trading_day_start_ns,
"2026-06-17T00:00:00Z",
"book",
"strategy-AAA:USD-BBB:USD",
"TARGET_POSITION",
(
'{"action":"TARGET","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"100","strength":"0.5"},'
'"BBB":{"reference_price":"50","strength":"-0.5"}}}'
),
),
(
trading_day_start_ns + 60_000_000_000,
"2026-06-17T00:01:00Z",
"book",
"strategy-AAA:USD-BBB:USD",
"CLOSE_POSITION",
(
'{"action":"CLOSE","quote_asset":"USD","assets":'
'{"AAA":{"reference_price":"110"},'
'"BBB":{"reference_price":"45"}}}'
),
),
],
)
conn.executemany(
"INSERT INTO ohlcv_1min VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
(
"2026-06-17T00:00:00Z",
trading_day_start_ns,
"EXCH",
"EXCH",
"PAIR-AAA-USD",
60,
100.0,
100.0,
100.0,
100.0,
1.0,
100.0,
1,
),
(
"2026-06-17T00:00:00Z",
trading_day_start_ns,
"EXCH",
"EXCH",
"PAIR-BBB-USD",
60,
50.0,
50.0,
50.0,
50.0,
1.0,
50.0,
1,
),
],
)
conn.commit()
finally:
conn.close()
def test_pair_analyze_grid_keeps_clean_labels_and_full_pair_values():
module = load_panel_app_module()
pair_theo_ret = pd.DataFrame(
{
"pair_name": ["BTC:USD-ETH:USD", "ADA:USD-BTC:USD"],
"mr_ranking": [2, 1],
"realized_pnl": [0.0, 0.0],
"unrealized_pnl": [0.0, 0.0],
}
)
formatted = module.spbt_day.format_pair_theo_ret_for_analyze_grid(pair_theo_ret)
assert formatted["pair_name"].tolist() == ["BTC-ETH", "ADA-BTC"]
assert formatted[module.spbt_day.PAIR_NAME_VALUE_COLUMN].tolist() == [
"BTC:USD-ETH:USD",
"ADA:USD-BTC:USD",
]
def test_panel_app_uses_fast_list_template(tmp_path):
module = load_panel_app_module()
app = module.SpbtDayPanelApp(repo_root=tmp_path)
view = app.view
assert not hasattr(app, "refresh_button")
assert isinstance(view, module.pn.template.FastListTemplate)
assert view.title == module.APP_TITLE
assert view.sidebar_width == 430
assert view.accent_base_color == module.APP_ACCENT_COLOR
assert view.header_background == module.APP_HEADER_COLOR
assert len(view.sidebar) == 1
assert len(view.main) == 1
def test_panel_app_calculates_pairs_and_selected_pair_outputs(tmp_path):
module = load_panel_app_module()
data_dir = tmp_path / "data"
data_dir.mkdir()
db_path = data_dir / "20260617.spbt_results.db"
create_panel_fixture_db(db_path)
app = module.SpbtDayPanelApp(repo_root=tmp_path)
app.directory_input.value = str(data_dir)
app.refresh_files()
app.min_pctg_change_input.value = 0.0
app.calculate()
assert app.file_select.value == str(db_path)
assert app.file_select.width == 360
assert app.min_pctg_change_input.width == 220
assert app.calculate_button.width == 110
assert app.total_pnl_histogram.sizing_mode == "stretch_width"
assert app.selected_pair_market_plot.sizing_mode == "stretch_width"
assert app.pair_theo_ret_table.pagination is None
assert app.pair_theo_ret_table.layout == "fit_data_table"
assert app.pair_theo_ret_table.value["pair_name"].tolist() == ["AAA-BBB"]
assert (
app.pair_theo_ret_table.value[module.spbt_day.PAIR_NAME_VALUE_COLUMN].tolist()
== ["AAA:USD-BBB:USD"]
)
assert app.selected_pair_name is None
assert app.selected_pair_executions_table.value.empty
assert app.selected_pair_market_plot.object is None
app.analyze_pair_row(0)
assert app.selected_pair_name == "AAA:USD-BBB:USD"
assert app.selected_pair_executions_table.value["action"].tolist() == [
"TARGET",
"TARGET",
"CLOSE",
"CLOSE",
]
assert app.selected_pair_market_plot.object is not None
def test_calculate_refreshes_file_list_before_loading(tmp_path):
module = load_panel_app_module()
data_dir = tmp_path / "data"
data_dir.mkdir()
app = module.SpbtDayPanelApp(repo_root=tmp_path)
app.directory_input.value = str(data_dir)
app.refresh_files()
assert app.file_select.value is None
db_path = data_dir / "20260617.spbt_results.db"
create_panel_fixture_db(db_path)
app.calculate()
assert app.file_select.value == str(db_path)
assert app.pair_theo_ret_table.value["pair_name"].tolist() == ["AAA-BBB"]