perf(portfolio): G5-P2收尾—all_weather三调用点向量化:①filter_roic逐只循环→一次批量+fields=['roic']短路(9×)②_trend_mean get_price长表+pivot→get_closes_panel宽表(340×,count=N→start-N*2自然日+.tail(N)同momentum口径,fq均raw)③prepare_stock_list get_price(close+high_limit)→get_limit_status_batch精确涨跌停口径;测试fixture补panel/limit_batch mock(原MagicMock碰巧truthy蒙混paused剔除语义,默认改全正常);+6回归测试,portfolio全套302绿 [vps]
This commit is contained in:
@@ -68,6 +68,30 @@ def make_strategy(
|
||||
|
||||
provider.get_price.side_effect = _get_price
|
||||
|
||||
# get_closes_panel:从 price_df_map 同源构造 close 宽表(忽略 count 差异)
|
||||
def _get_closes_panel(symbols, start=None, end=None, interval="d", fq="raw"):
|
||||
frames = [
|
||||
df[["time", "code", "close"]]
|
||||
for (sec, fields, _cnt), df in price_df_map.items()
|
||||
if sec == str(symbols) and tuple(fields) == ("close",)
|
||||
and df is not None and len(df)
|
||||
]
|
||||
if not frames:
|
||||
return pd.DataFrame()
|
||||
long_df = pd.concat(frames)
|
||||
return long_df.pivot(index="time", columns="code", values="close")
|
||||
|
||||
provider.get_closes_panel.side_effect = _get_closes_panel
|
||||
|
||||
# 涨跌停/停牌批量:默认全正常(filter_paused 无bar=剔除语义,MagicMock/{} 会误杀)
|
||||
def _get_limit_status_batch(codes, date=None):
|
||||
return {
|
||||
c: {"is_limit_up": False, "is_limit_down": False, "is_paused": False}
|
||||
for c in codes
|
||||
}
|
||||
|
||||
provider.get_limit_status_batch.side_effect = _get_limit_status_batch
|
||||
|
||||
broker = BrokerFacade()
|
||||
broker.order_target_value = MagicMock(return_value=MagicMock(filled=100))
|
||||
broker.order_value = MagicMock(return_value=MagicMock(filled=100))
|
||||
@@ -136,23 +160,69 @@ class TestPrepareStockList:
|
||||
|
||||
def test_records_yesterday_limit_up(self):
|
||||
s = make_strategy()
|
||||
# 清掉 make_strategy 设置的 side_effect,直接用 return_value
|
||||
s.provider.get_price.side_effect = None
|
||||
pos = MagicMock(); pos.security = "600519.XSHG"
|
||||
ctx = MagicMock()
|
||||
ctx.portfolio.positions = {"600519.XSHG": pos}
|
||||
ctx.previous_date = "2024-09-30"
|
||||
# close == high_limit 视为涨停
|
||||
s.provider.get_price.return_value = pd.DataFrame({
|
||||
"code": ["600519.XSHG"],
|
||||
"close": [10.0],
|
||||
"high_limit": [10.0],
|
||||
})
|
||||
# get_limit_status_batch 返回昨日涨停(G5-P2:原 get_price close==high_limit 改批量口径)
|
||||
s.provider.get_limit_status_batch.side_effect = \
|
||||
lambda codes, date=None: {
|
||||
c: {"is_limit_up": True, "is_limit_down": False, "is_paused": False}
|
||||
for c in codes
|
||||
}
|
||||
s.prepare_stock_list(ctx)
|
||||
assert "600519.XSHG" in s.yesterday_hl_list
|
||||
|
||||
def test_uses_batch_endpoint_once(self):
|
||||
"""G5-P2:原 get_price(close+high_limit) → get_limit_status_batch 一次批量。"""
|
||||
s = make_strategy()
|
||||
pos = MagicMock(); pos.security = "600519.XSHG"
|
||||
ctx = MagicMock()
|
||||
ctx.portfolio.positions = {"600519.XSHG": pos, "000001.XSHE": MagicMock(security="000001.XSHE")}
|
||||
ctx.previous_date = "2024-09-30"
|
||||
s.prepare_stock_list(ctx)
|
||||
assert s.provider.get_limit_status_batch.call_count == 1
|
||||
args, _kwargs = s.provider.get_limit_status_batch.call_args
|
||||
assert sorted(args[0]) == ["000001.XSHE", "600519.XSHG"]
|
||||
s.provider.get_price.assert_not_called()
|
||||
|
||||
# =================== stop_loss ===================
|
||||
|
||||
# =================== trend_mean ===================
|
||||
class TestTrendMean:
|
||||
def _panel_seed(self):
|
||||
return pd.DataFrame({
|
||||
"time": pd.to_datetime(["2024-09-20", "2024-09-30"]),
|
||||
"code": ["600519.XSHG"] * 2,
|
||||
"close": [10.0, 15.0], # 涨 50%
|
||||
})
|
||||
|
||||
def test_uses_closes_panel_not_get_price(self):
|
||||
"""G5-P2:原 get_price 长表+pivot → get_closes_panel 宽表。"""
|
||||
s = make_strategy(price_df_map={
|
||||
("['600519.XSHG']", ("close",), 10): self._panel_seed(),
|
||||
})
|
||||
out = s._trend_mean(["600519.XSHG"], "2024-09-30", 10)
|
||||
assert out == 50.0
|
||||
s.provider.get_closes_panel.assert_called_once()
|
||||
s.provider.get_price.assert_not_called()
|
||||
|
||||
def test_empty_or_short_panel_returns_zero(self):
|
||||
"""panel 缺失/不足 2 行 → 0.0 不崩(降级同原 get_price 空df语义)。"""
|
||||
s = make_strategy()
|
||||
assert s._trend_mean([], "2024-09-30", 10) == 0.0
|
||||
assert s._trend_mean(["600519.XSHG"], "2024-09-30", 10) == 0.0 # 无种子数据
|
||||
|
||||
def test_nan_only_change_not_propagated(self):
|
||||
"""某列 NaN(close 缺失)→ nan_to_num 归 0,不产出 NaN。"""
|
||||
wide = pd.DataFrame(
|
||||
{"600519.XSHG": [np.nan, 15.0], "000001.XSHE": [10.0, 11.0]},
|
||||
index=pd.to_datetime(["2024-09-20", "2024-09-30"]),
|
||||
)
|
||||
s = make_strategy()
|
||||
s.provider.get_closes_panel.side_effect = None
|
||||
s.provider.get_closes_panel.return_value = wide
|
||||
# 600519 NaN→0%, 000001 +10% → 均值 5(浮点容差)
|
||||
assert s._trend_mean(["600519.XSHG", "000001.XSHE"], "2024-09-30", 10) == pytest.approx(5.0)
|
||||
class TestStopLoss:
|
||||
def test_stop_loss_triggers_when_price_drops_8pct(self):
|
||||
"""avg_cost=100, price=91 (< 100*0.92=92) → 止损。"""
|
||||
@@ -467,3 +537,15 @@ class TestFilterRoic:
|
||||
s = make_strategy()
|
||||
out = s.filter_roic([], previous_date="2024-09-30")
|
||||
assert out == []
|
||||
|
||||
def test_batch_single_call_with_fields_shortcut(self):
|
||||
"""G5-P2:原逐只循环 → 一次批量 + fields=['roic'] 短路。"""
|
||||
df = make_fund_df([{"code": "A.XSHG", "roic": 0.15}])
|
||||
s = make_strategy()
|
||||
s.provider.get_fundamentals_df.return_value = df
|
||||
s.filter_roic(["A.XSHG", "B.XSHG"], previous_date="2024-09-30")
|
||||
# 一次批量调用(非逐只 N 次),且带 fields 短路
|
||||
assert s.provider.get_fundamentals_df.call_count == 1
|
||||
args, kwargs = s.provider.get_fundamentals_df.call_args
|
||||
assert list(args[0]) == ["A.XSHG", "B.XSHG"]
|
||||
assert kwargs.get("fields") == ["roic"]
|
||||
|
||||
Reference in New Issue
Block a user