Skip to content
Merged
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
51 changes: 45 additions & 6 deletions .github/workflows/polars-ci.yml
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 당장은 polars plugin쪽에서 non-nullable을 막고 있어서, core쪽만 nullable을 허용하게 되면 CI가 깨지게 됩니다.
그래서 일단 core 작업하는 동안은 임시로 비활성화 해두고, 그 다음 작업으로 바로 plugin쪽으로 넘어가서, 이 파일 변경사항을 되돌리고 작업 재개하려고 합니다

Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ name: Polars CI

on:
pull_request:
types: [review_requested, ready_for_review]
types: [review_requested, ready_for_review, synchronize]
paths:
- ".github/workflows/polars-*.yml"
- "Cargo.toml"
- "Makefile"
- "core/**"
- "polars/**"

concurrency:
Expand All @@ -18,42 +17,82 @@ 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:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
- 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
with:
python-version: "3.10"
- uses: astral-sh/setup-uv@v8.0.0
- uses: dtolnay/rust-toolchain@stable
- 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

- 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
Expand Down
80 changes: 44 additions & 36 deletions core/src/indicators/bband.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use crate::indicators::sma::sma;

use crate::utils::round_scalar;
use crate::utils::{rolling_mean_stddev_strict, round_scalar};

pub fn bband(
data: &[f64],
data: &[Option<f64>],
period: usize,
sigma: Option<f64>,
) -> (Vec<Option<f64>>, Vec<Option<f64>>, Vec<Option<f64>>) {
Expand All @@ -12,55 +11,42 @@ pub fn bband(
(upper_band, center, lower_band)
}

pub fn bband_middle(data: &[f64], period: usize) -> Vec<Option<f64>> {
pub fn bband_middle(data: &[Option<f64>], period: usize) -> Vec<Option<f64>> {
sma(data, period)
}

pub fn bband_upper(data: &[f64], period: usize, sigma: Option<f64>) -> Vec<Option<f64>> {
pub fn bband_upper(data: &[Option<f64>], period: usize, sigma: Option<f64>) -> Vec<Option<f64>> {
let (upper_band, _) = bband_bands(data, period, sigma);
upper_band
}

pub fn bband_lower(data: &[f64], period: usize, sigma: Option<f64>) -> Vec<Option<f64>> {
pub fn bband_lower(data: &[Option<f64>], period: usize, sigma: Option<f64>) -> Vec<Option<f64>> {
let (_, lower_band) = bband_bands(data, period, sigma);
lower_band
}

fn bband_bands(
data: &[f64],
data: &[Option<f64>],
period: usize,
sigma: Option<f64>,
) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
let mut upper_band = vec![None; data.len()];
let mut lower_band = vec![None; data.len()];
let mut sum = 0.0;
let mut sum_sq = 0.0;
let sigma = sigma.unwrap_or(2.0);
let (means, stddevs) = rolling_mean_stddev_strict(data, period);

if data.len() < period {
return (upper_band, lower_band);
}

for i in 0..data.len() {
sum += data[i];
sum_sq += data[i] * data[i];

if i >= period {
sum -= data[i - period];
sum_sq -= data[i - period] * data[i - period];
}

if i >= period - 1 {
let mean = sum / period as f64;
let variance = (sum_sq / period as f64) - (mean * mean);
let stddev = variance.sqrt();
let deviation = sigma * stddev;
upper_band[i] = Some(round_scalar(mean + deviation, 8));
lower_band[i] = Some(round_scalar(mean - deviation, 8));
}
}

(upper_band, lower_band)
means
.into_iter()
.zip(stddevs)
.map(|(mean, stddev)| match (mean, stddev) {
(Some(mean), Some(stddev)) => {
let deviation = sigma * stddev;
(
Some(round_scalar(mean + deviation, 8)),
Some(round_scalar(mean - deviation, 8)),
)
}
_ => (None, None),
})
.unzip()
}

#[cfg(test)]
Expand All @@ -73,7 +59,7 @@ mod tests {
fn test_bband() {
let test_cases = vec!["005930", "TSLA"];
for symbol in test_cases {
let input = testutils::load_data(&format!("../data/{}.json", symbol), "c");
let input = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "c");
let (upper, middle, lower) = bband(&input, 20, None);

let expected_upper = testutils::load_expected::<Option<f64>>(&format!(
Expand Down Expand Up @@ -109,4 +95,26 @@ mod tests {
);
}
}

#[test]
fn test_bband_with_interior_gap_invalidates_full_window() {
let input = vec![Some(1.0), Some(2.0), None, Some(4.0), Some(5.0)];

let (upper, middle, lower) = bband(&input, 2, Some(2.0));

assert_eq!(middle, vec![None, Some(1.5), None, None, Some(4.5)]);
assert_eq!(upper, vec![None, Some(2.5), None, None, Some(5.5)]);
assert_eq!(lower, vec![None, Some(0.5), None, None, Some(3.5)]);
}

#[test]
fn test_bband_full_window_invalidation() {
let input = vec![None, Some(2.0), None, Some(4.0)];

let (upper, middle, lower) = bband(&input, 2, None);

assert_eq!(upper, vec![None, None, None, None]);
assert_eq!(middle, vec![None, None, None, None]);
assert_eq!(lower, vec![None, None, None, None]);
}
}
4 changes: 2 additions & 2 deletions core/src/indicators/cv.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::indicators::ema::ema;
use crate::indicators::ema::ema_dense;

pub fn cv(highs: &[f64], lows: &[f64], period: usize) -> Vec<Option<f64>> {
let mut cv = vec![None; highs.len()];
Expand All @@ -9,7 +9,7 @@ pub fn cv(highs: &[f64], lows: &[f64], period: usize) -> Vec<Option<f64>> {
}

let high_low_diffs: Vec<f64> = highs.iter().zip(lows.iter()).map(|(h, l)| h - l).collect();
let ema_high_low_diffs = ema(&high_low_diffs, period);
let ema_high_low_diffs = ema_dense(&high_low_diffs, period);

for i in period * 2 - 1..len {
if let (Some(current_ema), Some(previous_ema)) =
Expand Down
37 changes: 33 additions & 4 deletions core/src/indicators/disparity.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
use crate::indicators::sma::sma;

pub fn disparity(data: &[f64], period: usize) -> Vec<Option<f64>> {
pub fn disparity(data: &[Option<f64>], period: usize) -> Vec<Option<f64>> {
let len = data.len();
let mut result = vec![None; len];

if len < period {
if len < period || period == 0 {
return result;
}

let sma = sma(data, period);

for i in period - 1..len {
let Some(value) = data[i] else {
continue;
};

if let Some(sma_value) = sma[i] {
if sma_value != 0.0 {
result[i] = Some((data[i] / sma_value) * 100.0);
result[i] = Some((value / sma_value) * 100.0);
}
}
}
Expand All @@ -31,7 +35,7 @@ mod tests {
fn test_disparity() {
let test_cases = vec!["005930", "TSLA"];
for symbol in test_cases {
let close = testutils::load_data(&format!("../data/{}.json", symbol), "c");
let close = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "c");
let result = disparity(&close, 20);
let expected = testutils::load_expected::<Option<f64>>(&format!(
"../data/expected/disparity_{}.json",
Expand All @@ -46,4 +50,29 @@ mod tests {
);
}
}

#[test]
fn test_disparity_with_interior_gap_invalidates_window() {
let input = vec![Some(1.0), Some(2.0), None, Some(4.0), Some(5.0)];
let expected = vec![
None,
Some(133.33333333333331),
None,
None,
Some(111.11111111111111),
];

let result = disparity(&input, 2);

assert_eq!(result, expected);
}

#[test]
fn test_disparity_full_window_invalidation() {
let input = vec![None, Some(2.0), None, Some(4.0)];

let result = disparity(&input, 2);

assert_eq!(result, vec![None, None, None, None]);
}
}
4 changes: 2 additions & 2 deletions core/src/indicators/efi.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::indicators::ema::ema;
use crate::indicators::ema::ema_dense;

pub fn efi(closes: &[f64], volumes: &[f64], period: usize) -> Vec<Option<f64>> {
let len = closes.len();
Expand All @@ -22,7 +22,7 @@ pub fn efi(closes: &[f64], volumes: &[f64], period: usize) -> Vec<Option<f64>> {
*efi_val = Some(force_val);
});
} else {
let ema_result = ema(&force, period);
let ema_result = ema_dense(&force, period);
efi.iter_mut()
.skip(1)
.zip(ema_result.into_iter())
Expand Down
Loading
Loading