summaryrefslogtreecommitdiff
path: root/tests/worker/test_normalize.py
blob: a4c9a38d3bc4f0b3975a8129d4083349ea3c8966 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
"""Normalization tests — synthetic fixtures only (_synthetic)."""
import math
import sys
import types
from pathlib import Path

import pandas as pd
import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
sys.path.insert(0, str(Path(__file__).resolve().parent))

from fixtures_synth import synthetic_daily, SYNTH  # noqa: E402
from worker.normalize import normalize_frame, build_manifest_entry, \
    compute_object_hash, coverage_summary, NormalizeError  # noqa: E402


def test_synthetic_marker_present():
    assert SYNTH["_synthetic"] is True
    df = synthetic_daily()
    assert df.attrs["synthetic"] is True


def test_normalize_keeps_raw_extra_fields_and_precision():
    df = synthetic_daily()
    out = normalize_frame(df, symbol="SH#600000")
    assert "_synthetic" not in out.columns
    # canonical columns present
    for col in ("date", "open", "high", "low", "close", "volume"):
        assert col in out.columns
    # raw extra fields preserved
    assert "amount" in out.columns and "turnover" in out.columns
    # precision kept, no rounding drift
    assert out["close"].iloc[0] == df["close"].iloc[0]
    assert out["volume"].iloc[3] == df["volume"].iloc[3]
    # symbol never substituted across instruments
    assert out["symbol"].iloc[5] == "SH#600000"
    assert out["date"].notna().all()


def test_normalize_rejects_missing_ohlcv():
    df = synthetic_daily()
    df.loc[4, "close"] = math.nan
    with pytest.raises(NormalizeError):
        normalize_frame(df, symbol="SH#600000")


def test_normalize_rejects_bad_dates():
    df = synthetic_daily()
    df["date"] = df["date"].astype(object)
    df.loc[2, "date"] = "not-a-date"
    with pytest.raises(NormalizeError):
        normalize_frame(df, symbol="SH#600000")


def test_normalize_sorts_and_dedups_dates():
    df = synthetic_daily()
    df = pd.concat([df.iloc[5:], df.iloc[:5]]).reset_index(drop=True)
    out = normalize_frame(df, symbol="SH#600000")
    assert list(out["date"]) == sorted(out["date"])
    assert len(out) == 20


def test_object_hash_stable_and_content_sensitive():
    df = synthetic_daily()
    h1 = compute_object_hash(df)
    h2 = compute_object_hash(normalize_frame(df, symbol="SH#600000"))
    assert h1 == h2 and len(h1) == 64
    df2 = synthetic_daily(base=11.0)
    assert compute_object_hash(df2) != h1


def test_coverage_summary_and_manifest_entry():
    df = synthetic_daily(days=10, extra_fields=False)
    cov = coverage_summary(df)
    assert cov["actual_start"] == df["date"].iloc[0]
    entry = build_manifest_entry(
        df, instrument={"symbol": "SH#600000", "market": "cn", "asset_type": "stock"},
        provider="akshare", endpoint="stock_zh_a_hist", params={"period": "daily"},
        adjustment="none", requested_start="2024-01-01", requested_end="2024-12-31",
        raw_object_hash="rawhash", warnings=[],
        schema_version="1", normalization_version="1",
        path="objects/ab/cd.json", fetched_at="2026-09-16T00:00:00Z",
        akshare_version="1.18.95",
    )
    assert entry["immutable"] is True
    assert entry["row_count"] == 10
    assert entry["adjustment"] == "none"
    assert entry["actual_start"] <= entry["actual_end"]
    assert "potential symbol substitution" not in entry["warnings"]
    for internal in ("path",):
        assert internal in entry  # server strips internal-only fields downstream