update
This commit is contained in:
parent
70dce8c58c
commit
6a7c20db91
@ -23,6 +23,27 @@ LOGGER = get_logger(__name__)
|
|||||||
LOG_EXTRA = {"stage": "backtest"}
|
LOG_EXTRA = {"stage": "backtest"}
|
||||||
|
|
||||||
|
|
||||||
|
def _valuation_score(value: object, scale: float) -> float:
|
||||||
|
try:
|
||||||
|
numeric = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
if numeric <= 0:
|
||||||
|
return 0.0
|
||||||
|
score = scale / (scale + numeric)
|
||||||
|
return max(0.0, min(1.0, score))
|
||||||
|
|
||||||
|
|
||||||
|
def _volume_ratio_score(value: object) -> float:
|
||||||
|
try:
|
||||||
|
numeric = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
if numeric < 0:
|
||||||
|
numeric = 0.0
|
||||||
|
return max(0.0, min(1.0, numeric / 10.0))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BtConfig:
|
class BtConfig:
|
||||||
@ -129,6 +150,9 @@ class BacktestEngine:
|
|||||||
)
|
)
|
||||||
close_values = [value for _date, value in closes if value is not None]
|
close_values = [value for _date, value in closes if value is not None]
|
||||||
|
|
||||||
|
mom5 = scope_values.get("factors.mom_5")
|
||||||
|
if mom5 is None and len(close_values) >= 5:
|
||||||
|
mom5 = momentum(close_values, 5)
|
||||||
mom20 = scope_values.get("factors.mom_20")
|
mom20 = scope_values.get("factors.mom_20")
|
||||||
if mom20 is None and len(close_values) >= 20:
|
if mom20 is None and len(close_values) >= 20:
|
||||||
mom20 = momentum(close_values, 20)
|
mom20 = momentum(close_values, 20)
|
||||||
@ -153,6 +177,9 @@ class BacktestEngine:
|
|||||||
turn20 = scope_values.get("factors.turn_20")
|
turn20 = scope_values.get("factors.turn_20")
|
||||||
if turn20 is None and turnover_values:
|
if turn20 is None and turnover_values:
|
||||||
turn20 = rolling_mean(turnover_values, 20)
|
turn20 = rolling_mean(turnover_values, 20)
|
||||||
|
turn5 = scope_values.get("factors.turn_5")
|
||||||
|
if turn5 is None and len(turnover_values) >= 5:
|
||||||
|
turn5 = rolling_mean(turnover_values, 5)
|
||||||
|
|
||||||
if mom20 is None:
|
if mom20 is None:
|
||||||
mom20 = 0.0
|
mom20 = 0.0
|
||||||
@ -162,6 +189,10 @@ class BacktestEngine:
|
|||||||
volat20 = 0.0
|
volat20 = 0.0
|
||||||
if turn20 is None:
|
if turn20 is None:
|
||||||
turn20 = 0.0
|
turn20 = 0.0
|
||||||
|
if mom5 is None:
|
||||||
|
mom5 = 0.0
|
||||||
|
if turn5 is None:
|
||||||
|
turn5 = 0.0
|
||||||
|
|
||||||
liquidity_score = normalize(turn20, factor=20.0)
|
liquidity_score = normalize(turn20, factor=20.0)
|
||||||
cost_penalty = normalize(
|
cost_penalty = normalize(
|
||||||
@ -169,15 +200,32 @@ class BacktestEngine:
|
|||||||
factor=50.0,
|
factor=50.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val_pe = scope_values.get("factors.val_pe_score")
|
||||||
|
if val_pe is None:
|
||||||
|
val_pe = _valuation_score(scope_values.get("daily_basic.pe"), scale=12.0)
|
||||||
|
|
||||||
|
val_pb = scope_values.get("factors.val_pb_score")
|
||||||
|
if val_pb is None:
|
||||||
|
val_pb = _valuation_score(scope_values.get("daily_basic.pb"), scale=2.5)
|
||||||
|
|
||||||
|
volume_ratio_score = scope_values.get("factors.volume_ratio_score")
|
||||||
|
if volume_ratio_score is None:
|
||||||
|
volume_ratio_score = _volume_ratio_score(scope_values.get("daily_basic.volume_ratio"))
|
||||||
|
|
||||||
sentiment_index = scope_values.get("news.sentiment_index", 0.0)
|
sentiment_index = scope_values.get("news.sentiment_index", 0.0)
|
||||||
heat_score = scope_values.get("news.heat_score", 0.0)
|
heat_score = scope_values.get("news.heat_score", 0.0)
|
||||||
scope_values.setdefault("news.sentiment_index", sentiment_index)
|
scope_values.setdefault("news.sentiment_index", sentiment_index)
|
||||||
scope_values.setdefault("news.heat_score", heat_score)
|
scope_values.setdefault("news.heat_score", heat_score)
|
||||||
|
|
||||||
|
scope_values.setdefault("factors.mom_5", mom5)
|
||||||
scope_values.setdefault("factors.mom_20", mom20)
|
scope_values.setdefault("factors.mom_20", mom20)
|
||||||
scope_values.setdefault("factors.mom_60", mom60)
|
scope_values.setdefault("factors.mom_60", mom60)
|
||||||
scope_values.setdefault("factors.volat_20", volat20)
|
scope_values.setdefault("factors.volat_20", volat20)
|
||||||
scope_values.setdefault("factors.turn_20", turn20)
|
scope_values.setdefault("factors.turn_20", turn20)
|
||||||
|
scope_values.setdefault("factors.turn_5", turn5)
|
||||||
|
scope_values.setdefault("factors.val_pe_score", val_pe)
|
||||||
|
scope_values.setdefault("factors.val_pb_score", val_pb)
|
||||||
|
scope_values.setdefault("factors.volume_ratio_score", volume_ratio_score)
|
||||||
if scope_values.get("macro.industry_heat") is None:
|
if scope_values.get("macro.industry_heat") is None:
|
||||||
scope_values["macro.industry_heat"] = 0.5
|
scope_values["macro.industry_heat"] = 0.5
|
||||||
if scope_values.get("macro.relative_strength") is None:
|
if scope_values.get("macro.relative_strength") is None:
|
||||||
@ -213,10 +261,12 @@ class BacktestEngine:
|
|||||||
)
|
)
|
||||||
|
|
||||||
features = {
|
features = {
|
||||||
|
"mom_5": mom5,
|
||||||
"mom_20": mom20,
|
"mom_20": mom20,
|
||||||
"mom_60": mom60,
|
"mom_60": mom60,
|
||||||
"volat_20": volat20,
|
"volat_20": volat20,
|
||||||
"turn_20": turn20,
|
"turn_20": turn20,
|
||||||
|
"turn_5": turn5,
|
||||||
"liquidity_score": liquidity_score,
|
"liquidity_score": liquidity_score,
|
||||||
"cost_penalty": cost_penalty,
|
"cost_penalty": cost_penalty,
|
||||||
"news_heat": heat_score,
|
"news_heat": heat_score,
|
||||||
@ -227,6 +277,9 @@ class BacktestEngine:
|
|||||||
scope_values.get("index.performance_peers", 0.0),
|
scope_values.get("index.performance_peers", 0.0),
|
||||||
),
|
),
|
||||||
"risk_penalty": min(1.0, volat20 * 5.0),
|
"risk_penalty": min(1.0, volat20 * 5.0),
|
||||||
|
"valuation_pe_score": val_pe,
|
||||||
|
"valuation_pb_score": val_pb,
|
||||||
|
"volume_ratio_score": volume_ratio_score,
|
||||||
"is_suspended": is_suspended,
|
"is_suspended": is_suspended,
|
||||||
"limit_up": limit_up,
|
"limit_up": limit_up,
|
||||||
"limit_down": limit_down,
|
"limit_down": limit_down,
|
||||||
|
|||||||
@ -58,7 +58,7 @@ def compute_factors(
|
|||||||
entry for ``trade_date`` will be ignored.
|
entry for ``trade_date`` will be ignored.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
specs = [spec for spec in factors if spec.window > 0]
|
specs = [spec for spec in factors if spec.window >= 0]
|
||||||
if not specs:
|
if not specs:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,8 @@ from app.features.factors import (
|
|||||||
FactorSpec,
|
FactorSpec,
|
||||||
compute_factor_range,
|
compute_factor_range,
|
||||||
compute_factors,
|
compute_factors,
|
||||||
|
_valuation_score,
|
||||||
|
_volume_ratio_score,
|
||||||
)
|
)
|
||||||
from app.utils.config import DataPaths, get_config
|
from app.utils.config import DataPaths, get_config
|
||||||
from app.utils.data_access import DataBroker
|
from app.utils.data_access import DataBroker
|
||||||
@ -58,18 +60,25 @@ def _populate_sample_data(ts_code: str, as_of: date) -> None:
|
|||||||
1_000_000.0,
|
1_000_000.0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
pe = 10.0 + (offset % 5)
|
||||||
|
pb = 1.5 + (offset % 3) * 0.1
|
||||||
|
ps = 2.0 + (offset % 4) * 0.1
|
||||||
|
volume_ratio = 0.5 + (offset % 4) * 0.5
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT OR REPLACE INTO daily_basic
|
INSERT OR REPLACE INTO daily_basic
|
||||||
(ts_code, trade_date, turnover_rate, turnover_rate_f, volume_ratio)
|
(ts_code, trade_date, turnover_rate, turnover_rate_f, volume_ratio, pe, pb, ps)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
ts_code,
|
ts_code,
|
||||||
trade_date,
|
trade_date,
|
||||||
turnover,
|
turnover,
|
||||||
turnover,
|
turnover,
|
||||||
1.0,
|
volume_ratio,
|
||||||
|
pe,
|
||||||
|
pb,
|
||||||
|
ps,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -79,7 +88,14 @@ def test_compute_factors_persists_and_updates(isolated_db):
|
|||||||
trade_day = date(2025, 1, 30)
|
trade_day = date(2025, 1, 30)
|
||||||
_populate_sample_data(ts_code, trade_day)
|
_populate_sample_data(ts_code, trade_day)
|
||||||
|
|
||||||
specs = [*DEFAULT_FACTORS, FactorSpec("mom_5", 5)]
|
specs = [
|
||||||
|
*DEFAULT_FACTORS,
|
||||||
|
FactorSpec("mom_5", 5),
|
||||||
|
FactorSpec("turn_5", 5),
|
||||||
|
FactorSpec("val_pe_score", 0),
|
||||||
|
FactorSpec("val_pb_score", 0),
|
||||||
|
FactorSpec("volume_ratio_score", 0),
|
||||||
|
]
|
||||||
results = compute_factors(trade_day, specs)
|
results = compute_factors(trade_day, specs)
|
||||||
|
|
||||||
assert results
|
assert results
|
||||||
@ -95,18 +111,29 @@ def test_compute_factors_persists_and_updates(isolated_db):
|
|||||||
expected_mom5 = momentum(close_series, 5)
|
expected_mom5 = momentum(close_series, 5)
|
||||||
expected_volat20 = volatility(close_series, 20)
|
expected_volat20 = volatility(close_series, 20)
|
||||||
expected_turn20 = rolling_mean(turnover_series, 20)
|
expected_turn20 = rolling_mean(turnover_series, 20)
|
||||||
|
expected_turn5 = rolling_mean(turnover_series, 5)
|
||||||
|
latest_pe = 10.0 + (0 % 5)
|
||||||
|
latest_pb = 1.5 + (0 % 3) * 0.1
|
||||||
|
latest_volume_ratio = 0.5 + (0 % 4) * 0.5
|
||||||
|
expected_val_pe = _valuation_score(latest_pe, scale=12.0)
|
||||||
|
expected_val_pb = _valuation_score(latest_pb, scale=2.5)
|
||||||
|
expected_vol_ratio_score = _volume_ratio_score(latest_volume_ratio)
|
||||||
|
|
||||||
assert result.values["mom_20"] == pytest.approx(expected_mom20)
|
assert result.values["mom_20"] == pytest.approx(expected_mom20)
|
||||||
assert result.values["mom_60"] == pytest.approx(expected_mom60)
|
assert result.values["mom_60"] == pytest.approx(expected_mom60)
|
||||||
assert result.values["mom_5"] == pytest.approx(expected_mom5)
|
assert result.values["mom_5"] == pytest.approx(expected_mom5)
|
||||||
assert result.values["volat_20"] == pytest.approx(expected_volat20)
|
assert result.values["volat_20"] == pytest.approx(expected_volat20)
|
||||||
assert result.values["turn_20"] == pytest.approx(expected_turn20)
|
assert result.values["turn_20"] == pytest.approx(expected_turn20)
|
||||||
|
assert result.values["turn_5"] == pytest.approx(expected_turn5)
|
||||||
|
assert result.values["val_pe_score"] == pytest.approx(expected_val_pe)
|
||||||
|
assert result.values["val_pb_score"] == pytest.approx(expected_val_pb)
|
||||||
|
assert result.values["volume_ratio_score"] == pytest.approx(expected_vol_ratio_score)
|
||||||
|
|
||||||
trade_date_str = trade_day.strftime("%Y%m%d")
|
trade_date_str = trade_day.strftime("%Y%m%d")
|
||||||
with db_session(read_only=True) as conn:
|
with db_session(read_only=True) as conn:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT mom_20, mom_60, mom_5, volat_20, turn_20
|
SELECT mom_20, mom_60, mom_5, volat_20, turn_20, turn_5, val_pe_score, val_pb_score, volume_ratio_score
|
||||||
FROM factors WHERE ts_code = ? AND trade_date = ?
|
FROM factors WHERE ts_code = ? AND trade_date = ?
|
||||||
""",
|
""",
|
||||||
(ts_code, trade_date_str),
|
(ts_code, trade_date_str),
|
||||||
@ -117,9 +144,24 @@ def test_compute_factors_persists_and_updates(isolated_db):
|
|||||||
assert row["mom_5"] == pytest.approx(expected_mom5)
|
assert row["mom_5"] == pytest.approx(expected_mom5)
|
||||||
assert row["volat_20"] == pytest.approx(expected_volat20)
|
assert row["volat_20"] == pytest.approx(expected_volat20)
|
||||||
assert row["turn_20"] == pytest.approx(expected_turn20)
|
assert row["turn_20"] == pytest.approx(expected_turn20)
|
||||||
|
assert row["turn_5"] == pytest.approx(expected_turn5)
|
||||||
|
assert row["val_pe_score"] == pytest.approx(expected_val_pe)
|
||||||
|
assert row["val_pb_score"] == pytest.approx(expected_val_pb)
|
||||||
|
assert row["volume_ratio_score"] == pytest.approx(expected_vol_ratio_score)
|
||||||
|
|
||||||
broker = DataBroker()
|
broker = DataBroker()
|
||||||
latest = broker.fetch_latest(ts_code, trade_date_str, ["factors.mom_5", "factors.turn_20"])
|
latest = broker.fetch_latest(
|
||||||
|
ts_code,
|
||||||
|
trade_date_str,
|
||||||
|
[
|
||||||
|
"factors.mom_5",
|
||||||
|
"factors.turn_20",
|
||||||
|
"factors.turn_5",
|
||||||
|
"factors.val_pe_score",
|
||||||
|
"factors.val_pb_score",
|
||||||
|
"factors.volume_ratio_score",
|
||||||
|
],
|
||||||
|
)
|
||||||
assert latest["factors.mom_5"] == pytest.approx(expected_mom5)
|
assert latest["factors.mom_5"] == pytest.approx(expected_mom5)
|
||||||
assert latest["factors.turn_20"] == pytest.approx(expected_turn20)
|
assert latest["factors.turn_20"] == pytest.approx(expected_turn20)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user