C13: report clear CSV errors in the offline examples
backtest.py and multi_timeframe.py read OHLCV CSVs with csv.DictReader and crashed opaquely on malformed input: a missing column raised a bare KeyError, a non-numeric cell surfaced NumPy's column-less ValueError, and an empty or all-NaN series hit IndexError deep in summarize. Validate the header against the required columns up front, catch non-numeric cells and report the offending row/column, reject a header-only file distinctly from a headerless one, and guard resample and summarize against empty input. Every failure mode now raises a ValueError naming the file, row and column.
This commit is contained in:
+37
-13
@@ -24,6 +24,10 @@ import numpy as np
|
|||||||
import wickra as ta
|
import wickra as ta
|
||||||
|
|
||||||
|
|
||||||
|
# Columns the OHLCV layout requires; the CSV header must name every one.
|
||||||
|
REQUIRED_COLUMNS = ("timestamp", "open", "high", "low", "close", "volume")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class History:
|
class History:
|
||||||
timestamp: np.ndarray
|
timestamp: np.ndarray
|
||||||
@@ -35,24 +39,44 @@ class History:
|
|||||||
|
|
||||||
|
|
||||||
def read_history(path: str) -> History:
|
def read_history(path: str) -> History:
|
||||||
"""Load an OHLCV CSV into typed NumPy columns."""
|
"""Load an OHLCV CSV into typed NumPy columns.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if the file has no header, is missing a required column,
|
||||||
|
holds no data rows, or contains a non-numeric value (the message
|
||||||
|
pinpoints the offending row and column instead of surfacing an
|
||||||
|
opaque ``KeyError`` or NumPy ``ValueError``).
|
||||||
|
"""
|
||||||
rows: List[List[str]] = []
|
rows: List[List[str]] = []
|
||||||
with open(path, newline="") as f:
|
with open(path, newline="") as f:
|
||||||
reader = csv.DictReader(f)
|
reader = csv.DictReader(f)
|
||||||
for row in reader:
|
if reader.fieldnames is None:
|
||||||
rows.append(
|
raise ValueError(f"{path}: CSV has no header row")
|
||||||
[
|
missing = [c for c in REQUIRED_COLUMNS if c not in reader.fieldnames]
|
||||||
row["timestamp"],
|
if missing:
|
||||||
row["open"],
|
raise ValueError(
|
||||||
row["high"],
|
f"{path}: CSV is missing required column(s): "
|
||||||
row["low"],
|
f"{', '.join(missing)}; found: {', '.join(reader.fieldnames)}"
|
||||||
row["close"],
|
|
||||||
row["volume"],
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
for row in reader:
|
||||||
|
rows.append([row[c] for c in REQUIRED_COLUMNS])
|
||||||
if not rows:
|
if not rows:
|
||||||
raise ValueError("CSV is empty")
|
raise ValueError(f"{path}: CSV has a header but no data rows")
|
||||||
arr = np.array(rows, dtype=np.float64)
|
try:
|
||||||
|
arr = np.array(rows, dtype=np.float64)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
# NumPy's own message names neither the row nor the column — locate
|
||||||
|
# the first non-numeric cell and report it precisely.
|
||||||
|
for line_no, row in enumerate(rows, start=2):
|
||||||
|
for col, value in zip(REQUIRED_COLUMNS, row):
|
||||||
|
try:
|
||||||
|
float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValueError(
|
||||||
|
f"{path}: row {line_no} column '{col}' is not "
|
||||||
|
f"numeric: {value!r}"
|
||||||
|
) from exc
|
||||||
|
raise # could not localise — surface the original error
|
||||||
return History(
|
return History(
|
||||||
timestamp=arr[:, 0].astype(np.int64),
|
timestamp=arr[:, 0].astype(np.int64),
|
||||||
open=arr[:, 1],
|
open=arr[:, 1],
|
||||||
|
|||||||
@@ -21,17 +21,43 @@ import numpy as np
|
|||||||
import wickra as ta
|
import wickra as ta
|
||||||
|
|
||||||
|
|
||||||
|
# Columns the OHLCV layout requires; the CSV header must name every one.
|
||||||
|
REQUIRED_COLUMNS = ("timestamp", "open", "high", "low", "close", "volume")
|
||||||
|
|
||||||
|
|
||||||
def read_csv(path: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
def read_csv(path: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||||||
"""Read an OHLCV CSV into typed columns."""
|
"""Read an OHLCV CSV into typed columns.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if the file has no header, is missing a required column,
|
||||||
|
holds no data rows, or contains a non-numeric value (the message
|
||||||
|
names the offending row).
|
||||||
|
"""
|
||||||
ts, o, h, l, c, v = [], [], [], [], [], []
|
ts, o, h, l, c, v = [], [], [], [], [], []
|
||||||
with open(path, newline="") as f:
|
with open(path, newline="") as f:
|
||||||
for row in csv.DictReader(f):
|
reader = csv.DictReader(f)
|
||||||
ts.append(int(row["timestamp"]))
|
if reader.fieldnames is None:
|
||||||
o.append(float(row["open"]))
|
raise ValueError(f"{path}: CSV has no header row")
|
||||||
h.append(float(row["high"]))
|
missing = [col for col in REQUIRED_COLUMNS if col not in reader.fieldnames]
|
||||||
l.append(float(row["low"]))
|
if missing:
|
||||||
c.append(float(row["close"]))
|
raise ValueError(
|
||||||
v.append(float(row["volume"]))
|
f"{path}: CSV is missing required column(s): "
|
||||||
|
f"{', '.join(missing)}; found: {', '.join(reader.fieldnames)}"
|
||||||
|
)
|
||||||
|
for line_no, row in enumerate(reader, start=2):
|
||||||
|
try:
|
||||||
|
ts.append(int(row["timestamp"]))
|
||||||
|
o.append(float(row["open"]))
|
||||||
|
h.append(float(row["high"]))
|
||||||
|
l.append(float(row["low"]))
|
||||||
|
c.append(float(row["close"]))
|
||||||
|
v.append(float(row["volume"]))
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"{path}: row {line_no} has a non-numeric value: {exc}"
|
||||||
|
) from exc
|
||||||
|
if not ts:
|
||||||
|
raise ValueError(f"{path}: CSV has a header but no data rows")
|
||||||
return (
|
return (
|
||||||
np.asarray(ts, dtype=np.int64),
|
np.asarray(ts, dtype=np.int64),
|
||||||
np.asarray(o, dtype=np.float64),
|
np.asarray(o, dtype=np.float64),
|
||||||
@@ -51,7 +77,13 @@ def resample(
|
|||||||
v: np.ndarray,
|
v: np.ndarray,
|
||||||
bucket: int,
|
bucket: int,
|
||||||
) -> Tuple[np.ndarray, ...]:
|
) -> Tuple[np.ndarray, ...]:
|
||||||
"""Aggregate bar-level columns into coarser buckets of size `bucket`."""
|
"""Aggregate bar-level columns into coarser buckets of size `bucket`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if the input series is empty.
|
||||||
|
"""
|
||||||
|
if ts.size == 0:
|
||||||
|
raise ValueError("resample received an empty series")
|
||||||
bucket_index = (ts // bucket).astype(np.int64)
|
bucket_index = (ts // bucket).astype(np.int64)
|
||||||
boundaries = np.diff(bucket_index, prepend=bucket_index[0] - 1) != 0
|
boundaries = np.diff(bucket_index, prepend=bucket_index[0] - 1) != 0
|
||||||
group_ids = np.cumsum(boundaries) - 1
|
group_ids = np.cumsum(boundaries) - 1
|
||||||
@@ -76,14 +108,19 @@ def resample(
|
|||||||
|
|
||||||
|
|
||||||
def summarize(label: str, close: np.ndarray, high: np.ndarray, low: np.ndarray) -> None:
|
def summarize(label: str, close: np.ndarray, high: np.ndarray, low: np.ndarray) -> None:
|
||||||
|
if close.size == 0:
|
||||||
|
print(f" {label:<10} (empty series — nothing to summarize)")
|
||||||
|
return
|
||||||
rsi = ta.RSI(14).batch(close)
|
rsi = ta.RSI(14).batch(close)
|
||||||
macd = ta.MACD().batch(close)
|
macd = ta.MACD().batch(close)
|
||||||
adx = ta.ADX(14).batch(high, low, close)
|
adx = ta.ADX(14).batch(high, low, close)
|
||||||
last_macd_hist = macd[~np.isnan(macd[:, 2])][-1, 2] if np.any(~np.isnan(macd[:, 2])) else float("nan")
|
last_macd_hist = macd[~np.isnan(macd[:, 2])][-1, 2] if np.any(~np.isnan(macd[:, 2])) else float("nan")
|
||||||
last_adx = adx[~np.isnan(adx[:, 2])][-1, 2] if np.any(~np.isnan(adx[:, 2])) else float("nan")
|
last_adx = adx[~np.isnan(adx[:, 2])][-1, 2] if np.any(~np.isnan(adx[:, 2])) else float("nan")
|
||||||
|
valid_rsi = rsi[~np.isnan(rsi)]
|
||||||
|
last_rsi = valid_rsi[-1] if valid_rsi.size else float("nan")
|
||||||
print(
|
print(
|
||||||
f" {label:<10} bars={close.size:>5} "
|
f" {label:<10} bars={close.size:>5} "
|
||||||
f"last_close={close[-1]:>10.4f} rsi={rsi[~np.isnan(rsi)][-1]:>6.2f} "
|
f"last_close={close[-1]:>10.4f} rsi={last_rsi:>6.2f} "
|
||||||
f"macd_hist={last_macd_hist:+.4f} adx={last_adx:>6.2f}"
|
f"macd_hist={last_macd_hist:+.4f} adx={last_adx:>6.2f}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user