Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions backend_api_python/app/services/factors/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -1077,8 +1077,21 @@ def _garman_klass_volatility(frame: pd.DataFrame, params: Mapping[str, Any]) ->

def _amihud_illiquidity(frame: pd.DataFrame, params: Mapping[str, Any]) -> float:
period = _period(params)
close = _require(frame["close"], period + 1)
volume = pd.to_numeric(frame["volume"], errors="coerce").iloc[-period:]
returns = close.pct_change().abs().iloc[-period:]
traded_value = (close.iloc[-period:] * volume).replace(0, np.nan)
return float((returns.to_numpy() / traded_value.to_numpy()).mean())
raw_close = pd.to_numeric(frame["close"], errors="coerce").replace([np.inf, -np.inf], np.nan)
raw_volume = pd.to_numeric(frame["volume"], errors="coerce").replace([np.inf, -np.inf], np.nan)
valid_close = raw_close.notna().to_numpy()
close_values = raw_close.to_numpy(dtype=float)[valid_close]
if len(close_values) < period + 1:
raise FactorError("factor.insufficientHistory")
tail_close = close_values[-(period + 1):]
prev_close = tail_close[:-1]
curr_close = tail_close[1:]
curr_volume = raw_volume.to_numpy(dtype=float)[valid_close][-period:]
with np.errstate(divide="ignore", invalid="ignore"):
abs_returns = np.abs(curr_close / prev_close - 1.0)
traded_value = curr_close * curr_volume
ratio = abs_returns / np.where(traded_value > 0.0, traded_value, np.nan)
finite = ratio[np.isfinite(ratio)]
if finite.size == 0:
return float("nan")
return float(finite.mean())
38 changes: 38 additions & 0 deletions backend_api_python/tests/test_factor_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,41 @@ def test_extended_factor_families_have_expected_invariants():
assert compute_factor("parkinson_volatility", frame) > 0
assert np.isfinite(compute_factor("skewness", frame))
assert np.isfinite(compute_factor("kurtosis", frame))


def test_amihud_illiquidity_aligns_close_and_volume_with_missing_or_zero_bars():
trailing_nan = pd.DataFrame({
"close": [100.0, 102.0, 101.0, 104.0, 103.0, 106.0, np.nan],
"volume": [1_000.0, 1_100.0, 1_200.0, 1_300.0, 1_400.0, 1_500.0, 1_600.0],
})
clean = trailing_nan.iloc[:-1]
assert compute_factor("amihud_illiquidity", trailing_nan, {"period": 5}) == pytest.approx(
compute_factor("amihud_illiquidity", clean, {"period": 5})
)

internal_nan_mismatch = pd.DataFrame({
"close": [100.0, 101.0, np.nan, 103.0, 104.0, np.nan, 106.0, 108.0],
"volume": [100.0, 200.0, 9_999.0, 300.0, 400.0, 9_999.0, 500.0, 600.0],
})
expected = compute_factor(
"amihud_illiquidity",
internal_nan_mismatch.dropna(subset=["close"]).reset_index(drop=True),
{"period": 4},
)
assert compute_factor("amihud_illiquidity", internal_nan_mismatch, {"period": 4}) == pytest.approx(
expected
)

zero_volume = pd.DataFrame({
"close": [100.0, 101.0, 102.0, 103.0, 104.0, 105.0],
"volume": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
})
assert np.isnan(compute_factor("amihud_illiquidity", zero_volume, {"period": 5}))

short_close = pd.DataFrame({
"close": [100.0, np.nan, 102.0, 103.0],
"volume": [1_000.0, 1_000.0, 1_000.0, 1_000.0],
})
with pytest.raises(FactorError) as caught:
compute_factor("amihud_illiquidity", short_close, {"period": 5})
assert caught.value.code == "factor.insufficientHistory"
27 changes: 27 additions & 0 deletions backend_api_python/tests/test_factor_research.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,30 @@ def test_factor_research_warmup_bars_handles_shorthand_and_custom_multistage_fac
[timestamp],
)
assert panel.loc[timestamp].notna().all(), factor_id


def test_factor_research_correlation_survives_nan_close_in_amihud_illiquidity():
index = pd.date_range("2025-01-01", periods=60, freq="B")
frames = {}
for offset, symbol in enumerate(["A", "B", "C", "D", "E", "F"], start=1):
close = pd.Series([100.0 + day * (0.2 + offset * 0.05) for day in range(len(index))], index=index)
if symbol == "F":
close.iloc[40] = float("nan")
frames[symbol] = pd.DataFrame({
"open": close - 0.1,
"high": close + 1.0,
"low": close - 1.0,
"close": close,
"volume": [1_000.0 + day * 10.0 for day in range(len(index))],
}, index=index)

result = FactorResearchEngine().run(
frames=frames,
factor_id="momentum_20",
start_date=index[30],
end_date=index[-1],
groups=3,
holding_period=5,
)

assert "amihud_illiquidity" in result["factorCorrelation"]["factors"]
Loading