update
This commit is contained in:
@@ -2,13 +2,15 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import yaml
|
||||
|
||||
from cbpred.pipeline import (
|
||||
CSV_COLUMNS,
|
||||
@@ -25,6 +27,9 @@ from cbpred.pipeline import (
|
||||
run_training,
|
||||
Symbol,
|
||||
run_banbot_download,
|
||||
detect_db_type,
|
||||
effective_banbot_config,
|
||||
prepare_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -44,6 +49,99 @@ def make_hour_rows(start_ms: int, hours: int, phase: float = 0.0):
|
||||
|
||||
|
||||
class PipelineTests(unittest.TestCase):
|
||||
def test_download_reads_utf8_stderr_on_windows(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
downloader = root / "downloader.py"
|
||||
downloader.write_text(
|
||||
"import sys\n"
|
||||
"sys.stderr.buffer.write('\\u4e00'.encode('utf-8'))\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
symbol = Symbol(1, "binance", "linear", "BTC/USDT:USDT", 1, 0)
|
||||
downloaded, unavailable = run_banbot_download(
|
||||
[symbol], root, root / "config.yml", 1, 2,
|
||||
banbot_cmd=f'"{sys.executable}" "{downloader}"',
|
||||
)
|
||||
self.assertEqual(downloaded, [symbol])
|
||||
self.assertEqual(unavailable, [])
|
||||
|
||||
def test_auto_database_prefers_timescale_when_both_tables_exist(self):
|
||||
conn = MagicMock()
|
||||
cur = conn.cursor.return_value.__enter__.return_value
|
||||
cur.fetchone.return_value = ("exsymbol", "exsymbol_q")
|
||||
self.assertEqual(detect_db_type(conn), "timescale")
|
||||
cur.execute.assert_called_once_with("SELECT to_regclass('exsymbol'), to_regclass('exsymbol_q')")
|
||||
|
||||
def test_auto_database_detects_questdb_postgres_tables(self):
|
||||
conn = MagicMock()
|
||||
conn.cursor.return_value.__enter__.return_value.fetchone.return_value = (None, "exsymbol_q")
|
||||
self.assertEqual(detect_db_type(conn), "questdb")
|
||||
|
||||
def test_auto_database_falls_back_for_questdb_without_to_regclass(self):
|
||||
conn = MagicMock()
|
||||
cur = conn.cursor.return_value.__enter__.return_value
|
||||
cur.execute.side_effect = [
|
||||
RuntimeError("unknown function: to_regclass"),
|
||||
RuntimeError("table does not exist [table=exsymbol]"),
|
||||
None,
|
||||
]
|
||||
self.assertEqual(detect_db_type(conn), "questdb")
|
||||
self.assertEqual(conn.rollback.call_count, 2)
|
||||
|
||||
def test_auto_database_does_not_hide_permission_errors(self):
|
||||
conn = MagicMock()
|
||||
conn.cursor.return_value.__enter__.return_value.execute.side_effect = RuntimeError("permission denied")
|
||||
with self.assertRaisesRegex(RuntimeError, "permission denied"):
|
||||
detect_db_type(conn)
|
||||
|
||||
def test_effective_config_is_removed_after_failure_and_original_unchanged(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
config_path = Path(directory) / "config.yml"
|
||||
original = "database:\n url: original\n db_type: questdb\nassets: ./assets\n"
|
||||
config_path.write_text(original)
|
||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||
with effective_banbot_config(config_path, "selected", "timescale") as effective:
|
||||
self.assertEqual(effective.parent, config_path.parent)
|
||||
self.assertTrue(effective.exists())
|
||||
raise RuntimeError("failed")
|
||||
self.assertFalse(effective.exists())
|
||||
self.assertEqual(config_path.read_text(), original)
|
||||
|
||||
def test_prepare_propagates_selected_database_to_download_and_export(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
config_path = root / "config.yml"
|
||||
original = "database:\n url: original\n db_type: questdb\n max_conn: 4\nmarket_type: spot\nassets: ./assets\n"
|
||||
config_path.write_text(original)
|
||||
symbol = Symbol(1, "binance", "linear", "BTC/USDT:USDT", 1, 0)
|
||||
commands = []
|
||||
config_paths = []
|
||||
|
||||
def run(command, **kwargs):
|
||||
commands.append(command)
|
||||
path = Path(command[command.index("--config") + 1])
|
||||
config_paths.append(path)
|
||||
effective = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(effective["database"], {"url": "selected-secret", "db_type": "timescale", "max_conn": 4})
|
||||
self.assertEqual(effective["assets"], "./assets")
|
||||
self.assertEqual(effective["market_type"], "linear")
|
||||
self.assertEqual(path.parent, config_path.parent)
|
||||
self.assertNotIn("selected-secret", command)
|
||||
|
||||
conn = MagicMock()
|
||||
with patch("cbpred.pipeline.connect_database", return_value=conn) as connect, \
|
||||
patch("cbpred.pipeline.detect_db_type", return_value="timescale"), \
|
||||
patch("cbpred.pipeline.fetch_symbols", return_value=([symbol], [])), \
|
||||
patch("cbpred.pipeline.subprocess.run", side_effect=run):
|
||||
manifest = prepare_data(root / "data", root, config_path, dsn="selected-secret", end_ms=EIGHT_H_MS * 1000)
|
||||
connect.assert_called_once_with("selected-secret")
|
||||
self.assertEqual(len(commands), 2)
|
||||
self.assertEqual(config_paths[0], config_paths[1])
|
||||
self.assertFalse(config_paths[0].exists())
|
||||
self.assertEqual(config_path.read_text(), original)
|
||||
self.assertEqual(manifest["db_type"], "timescale")
|
||||
|
||||
def test_download_skips_invalid_current_symbol_and_retries_chunk(self):
|
||||
valid = Symbol(1, "binance", "linear", "BTC/USDT:USDT", 1, 0)
|
||||
stale = Symbol(2, "binance", "linear", "SXP/USDT:USDT", 1, 0)
|
||||
|
||||
Reference in New Issue
Block a user