Skip to content

⚡ Optimize aligned signal calculations#10

Merged
sjquant merged 4 commits intomainfrom
sjquant/optimize-polars-boundary
Apr 17, 2026
Merged

⚡ Optimize aligned signal calculations#10
sjquant merged 4 commits intomainfrom
sjquant/optimize-polars-boundary

Conversation

@sjquant
Copy link
Copy Markdown
Collaborator

@sjquant sjquant commented Apr 15, 2026

요약

여러 지표의 signal 계산 경로에서 임시 압축 벡터를 만들지 않고, 기존 Vec<Option<f64>> 정렬을 유지한 채 EMA/SMA를 직접 계산하도록 최적화했습니다.

변경 사항

  • core/src/indicators/ema.rsema_aligned 추가
  • core/src/indicators/sma.rssma_aligned 추가
  • 다음 signal 계산 경로를 aligned helper 기반으로 전환
    • macd_signal
    • ppo_signal
    • pvo_signal
    • obv_signal
    • sonar signal
    • nvi signal
    • pvi signal
    • massi 내부 EMA 경로
    • eom signal SMA 경로
  • 공개 API와 반환 형태는 유지

변경 이유

기존 구현은 공통적으로 다음 비용을 반복해서 지불하고 있었습니다.

  • filter_map(...).collect()로 유효 구간만 새 벡터로 압축
  • 압축된 벡터에 EMA 또는 SMA 적용
  • 계산 결과를 원래 offset에 맞춰 다시 복사

하지만 이 지표들은 대부분 첫 유효 값 이후 구간이 이미 정렬된 형태이므로, 임시 벡터를 만들지 않고도 같은 결과를 계산할 수 있습니다.

벤치마크

origin/main (cb2e4ff) 대비, techr-core release 벤치 1,000,000 rows, 5회 실행 중앙값 기준입니다.

항목 origin/main this branch 변화
macd_signal 12.8ms 9.6ms -25.0%
macd_hist 13.0ms 9.9ms -23.8%
ppo_signal 14.0ms 9.7ms -30.7%
ppo_hist 14.2ms 9.7ms -31.7%
pvo_signal 13.4ms 9.9ms -26.1%
pvo_hist 12.4ms 9.8ms -21.0%
obv_signal 6.4ms 3.7ms -42.2%
sonar_signal 9.5ms 6.7ms -29.5%
nvi_signal 7.9ms 5.0ms -36.7%
pvi_signal 6.8ms 4.2ms -38.2%
massi_signal 17.0ms 11.9ms -30.0%
eom_signal 10.0ms 6.2ms -38.0%

벤치마크 스크립트

use std::time::Instant;

const RUNS: usize = 5;

type Case<'a> = (&'a str, Box<dyn Fn() + 'a>);

fn make_series(len: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
    let mut highs = Vec::with_capacity(len);
    let mut lows = Vec::with_capacity(len);
    let mut closes = Vec::with_capacity(len);
    let mut volumes = Vec::with_capacity(len);
    for i in 0..len {
        let angle = i as f64 * 0.0007;
        let base = 100.0 + angle.sin() * 15.0 + angle.cos() * 9.0 + (i % 251) as f64 * 0.02;
        highs.push(base + 2.5 + ((i % 11) as f64 * 0.01));
        lows.push(base - 2.5 - ((i % 13) as f64 * 0.01));
        closes.push(base + ((i % 7) as f64 - 3.0) * 0.05);
        volumes.push(10_000.0 + ((i % 113) as f64 * 17.0) + angle.sin().abs() * 500.0);
    }
    (highs, lows, closes, volumes)
}

fn median(values: &mut [f64]) -> f64 {
    values.sort_by(|a, b| a.partial_cmp(b).unwrap());
    values[values.len() / 2]
}

fn measure(case: &dyn Fn()) -> f64 {
    let start = Instant::now();
    case();
    start.elapsed().as_secs_f64() * 1000.0
}

fn main() {
    let (highs, lows, closes, volumes) = make_series(1_000_000);
    let cases: Vec<Case> = vec![
        ("macd_signal", Box::new(|| { let _ = techr::macd(&closes, 12, 26, 9).1; })),
        ("macd_hist", Box::new(|| { let _ = techr::macd(&closes, 12, 26, 9).2; })),
        ("ppo_signal", Box::new(|| { let _ = techr::ppo(&closes, 12, 26, 9).1; })),
        ("ppo_hist", Box::new(|| { let _ = techr::ppo(&closes, 12, 26, 9).2; })),
        ("pvo_signal", Box::new(|| { let _ = techr::pvo(&volumes, 12, 26, 9).1; })),
        ("pvo_hist", Box::new(|| { let _ = techr::pvo(&volumes, 12, 26, 9).2; })),
        ("obv_signal", Box::new(|| { let _ = techr::obv(&closes, &volumes, 9).1; })),
        ("sonar_signal", Box::new(|| { let _ = techr::sonar(&closes, 9, 6, 5).1; })),
        ("nvi_signal", Box::new(|| { let _ = techr::nvi(&closes, &volumes, 255).1; })),
        ("pvi_signal", Box::new(|| { let _ = techr::pvi(&closes, &volumes, 255).1; })),
        ("massi_signal", Box::new(|| { let _ = techr::massi(&highs, &lows, 9, 25, 9).1; })),
        ("eom_signal", Box::new(|| { let _ = techr::eom(&highs, &lows, &volumes, 14, 3, 10_000.0).1; })),
    ];

    for (_, case) in &cases {
        case();
    }

    for (name, case) in cases {
        let mut samples = (0..RUNS).map(|_| measure(&*case)).collect::<Vec<_>>();
        let median = median(&mut samples);
        println!("{}_samples_ms={:?}", name, samples.iter().map(|v| format!("{v:.1}")).collect::<Vec<_>>());
        println!("{}_median_ms={median:.1}", name);
    }
}

검증

  • cargo test --manifest-path core/Cargo.toml
  • cargo check --manifest-path polars/Cargo.toml
  • cd polars && uv run pytest

@sjquant sjquant self-assigned this Apr 16, 2026
@sjquant sjquant force-pushed the sjquant/optimize-polars-boundary branch from d785240 to 86b3778 Compare April 16, 2026 11:03
@sjquant sjquant changed the title ⚡ Optimize MACD signal path ⚡ Optimize aligned signal calculations Apr 16, 2026
@sjquant sjquant changed the title ⚡ Optimize aligned signal calculations ⚡ 정렬된 signal 계산 경로 최적화 Apr 16, 2026
@sjquant sjquant changed the title ⚡ 정렬된 signal 계산 경로 최적화 ⚡ Optimize aligned signal calculations Apr 16, 2026
@sjquant sjquant requested a review from mingi3314 April 16, 2026 15:24
@alphaprime-dev-discord
Copy link
Copy Markdown

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.

이번 작업이 적용되는 친구들 중 분리안되어있는 친구들은 다 분리해뒀습니다.

Copy link
Copy Markdown
Collaborator

@mingi3314 mingi3314 left a comment

Choose a reason for hiding this comment

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

👍👍👍

@sjquant sjquant merged commit 301d08c into main Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants