diff --git a/.github/workflows/polars-ci.yml b/.github/workflows/polars-ci.yml index fb9e376..00e4573 100644 --- a/.github/workflows/polars-ci.yml +++ b/.github/workflows/polars-ci.yml @@ -2,11 +2,12 @@ name: Polars CI on: pull_request: - types: [review_requested, ready_for_review, synchronize] + types: [review_requested, ready_for_review] paths: - ".github/workflows/polars-*.yml" - "Cargo.toml" - "Makefile" + - "core/**" - "polars/**" concurrency: @@ -17,82 +18,42 @@ permissions: contents: read jobs: - preflight: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - outputs: - validate: ${{ steps.filter.outputs.validate }} - steps: - - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 - id: filter - with: - filters: | - validate: - - "Cargo.toml" - - "Makefile" - - "polars/**" - validate: - needs: preflight - if: ${{ always() }} runs-on: ubuntu-latest defaults: run: shell: bash steps: - - name: Fail if preflight did not succeed - if: ${{ needs.preflight.result != 'success' }} - run: | - echo "preflight job did not succeed: ${{ needs.preflight.result }}" - exit 1 - - - name: Skip validation for non-polars changes - if: ${{ needs.preflight.result == 'success' && needs.preflight.outputs.validate != 'true' }} - run: echo "Skipping Polars CI validate job because this PR does not touch Cargo.toml, Makefile, or polars/**." - - - if: ${{ needs.preflight.outputs.validate == 'true' }} - uses: actions/checkout@v6 - - if: ${{ needs.preflight.outputs.validate == 'true' }} - uses: actions/setup-python@v6 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: "3.10" - - if: ${{ needs.preflight.outputs.validate == 'true' }} - uses: astral-sh/setup-uv@v8.0.0 - - if: ${{ needs.preflight.outputs.validate == 'true' }} - uses: dtolnay/rust-toolchain@stable + - uses: astral-sh/setup-uv@v8.0.0 + - uses: dtolnay/rust-toolchain@stable - name: Sync polars dependencies - if: ${{ needs.preflight.outputs.validate == 'true' }} working-directory: polars run: uv sync --group dev --no-install-project - name: Test techr-core - if: ${{ needs.preflight.outputs.validate == 'true' }} run: cargo test -p techr-core - name: Build local extension for tests - if: ${{ needs.preflight.outputs.validate == 'true' }} working-directory: polars run: uv run maturin develop --uv - name: Test polars package - if: ${{ needs.preflight.outputs.validate == 'true' }} working-directory: polars run: uv run pytest - name: Build wheel and sdist - if: ${{ needs.preflight.outputs.validate == 'true' }} working-directory: polars run: uv run maturin build --release --sdist --out dist - name: Check artifact contents - if: ${{ needs.preflight.outputs.validate == 'true' }} run: python polars/scripts/check_artifacts.py polars/dist - name: Smoke test built wheel - if: ${{ needs.preflight.outputs.validate == 'true' }} run: | wheel="$(python - <<'PY' from pathlib import Path diff --git a/polars/Cargo.toml b/polars/Cargo.toml index c6b9b0a..4229045 100644 --- a/polars/Cargo.toml +++ b/polars/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "polars_techr" -version = "0.1.1" +version = "0.1.2" edition = "2021" description = "Polars expression plugins for techr indicators" license = "MIT" diff --git a/polars/README.md b/polars/README.md index c889f72..0d6f06c 100644 --- a/polars/README.md +++ b/polars/README.md @@ -45,6 +45,8 @@ result = df.select( ) ``` +Null input values are accepted. Indicators preserve row alignment and emit nulls according to the core rolling-window and seed recovery rules. + ## Ichimoku Notes - Standalone Ichimoku rolling-window lines such as `ichimoku_base_line` and `ichimoku_conversion_line` use `period`. diff --git a/polars/src/expressions.rs b/polars/src/expressions.rs index 6c0e205..b5236cd 100644 --- a/polars/src/expressions.rs +++ b/polars/src/expressions.rs @@ -69,16 +69,9 @@ struct IchimokuLaggingSpanKwargs { base_line_period: u32, } -fn series_to_f64_vec(series: &Series) -> PolarsResult> { +fn series_to_f64_vec(series: &Series) -> PolarsResult>> { let casted = series.cast(&DataType::Float64)?; - let values = casted.f64()?.to_vec_null_aware(); - if let Some(values) = values.left() { - Ok(values) - } else { - Err(PolarsError::ComputeError( - "null values are not supported yet".into(), - )) - } + Ok(casted.f64()?.to_vec()) } fn option_vec_to_series(values: Vec>) -> Series { diff --git a/polars/tests/test_indicators.py b/polars/tests/test_indicators.py index c800c81..d71659a 100644 --- a/polars/tests/test_indicators.py +++ b/polars/tests/test_indicators.py @@ -284,41 +284,47 @@ def test_multi_input_integer_columns_are_cast_to_float() -> None: ]) -def test_single_input_null_values_raise_compute_error() -> None: - """Reject null values for single-input indicators with a Polars compute error.""" +@pytest.mark.parametrize("lazy", [False, True]) +def test_single_input_null_values_follow_core_gap_semantics(lazy: bool) -> None: + """Accept null values for single-input indicators.""" # given - df = pl.DataFrame({"close": [1.0, None, 3.0]}) + df = pl.DataFrame({"close": [1.0, None, 3.0, 4.0]}) + + # when + result = select_expr(df, ta.sma(pl.col("close"), period=2), "sma", lazy) - # when / then - with pytest.raises( - pl.exceptions.ComputeError, - match="null values are not supported yet", - ): - df.select(ta.sma(pl.col("close"), period=2).alias("sma")) + # then + assert_values_close(result.to_list(), [None, None, None, 3.5]) -def test_multi_input_null_values_raise_compute_error() -> None: - """Reject null values for multi-input indicators with a Polars compute error.""" +@pytest.mark.parametrize("lazy", [False, True]) +def test_multi_input_null_values_follow_core_gap_semantics(lazy: bool) -> None: + """Accept null values for multi-input indicators.""" # given df = pl.DataFrame( { - "high": [11.0, 12.0, None], - "low": [1.0, 2.0, 3.0], - "close": [6.0, 7.0, 8.0], + "high": [5.0, 7.0, None, 10.0, 12.0], + "low": [1.0, 3.0, None, 6.0, 8.0], + "close": [4.0, 5.0, None, 8.0, 11.0], } ) - # when / then - with pytest.raises( - pl.exceptions.ComputeError, - match="null values are not supported yet", - ): - df.select( - ta.stochf_percent_k( - pl.col("high"), - pl.col("low"), - pl.col("close"), - fastk_period=3, - fastd_period=2, - ).alias("value") - ) + # when + result = select_expr( + df, + ta.stochf_percent_k( + pl.col("high"), + pl.col("low"), + pl.col("close"), + fastk_period=2, + fastd_period=2, + ), + "value", + lazy, + ) + + # then + assert_values_close( + result.to_list(), + [None, 66.66666666666666, None, None, 83.33333333333334], + )