Skip to content

♻️ core 상태형 지표 nullable 입력 전환#16

Merged
mingi3314 merged 8 commits into
mainfrom
core-nullable-stateful
Apr 27, 2026
Merged

♻️ core 상태형 지표 nullable 입력 전환#16
mingi3314 merged 8 commits into
mainfrom
core-nullable-stateful

Conversation

@mingi3314
Copy link
Copy Markdown
Collaborator

@mingi3314 mingi3314 commented Apr 21, 2026

요약

  • 이 PR은 #15 위에 쌓인 stacked PR입니다. 리뷰는 core-nullable-foundations 대비로 봐주시면 됩니다.
  • foundation layer에서 정의한 nullable contract를 상태형, pairwise, cumulative 지표에 적용합니다.
  • gap을 건너뛰며 series를 압축하지 않고, 현재 row, 이전 row, 내부 state의 유효성에 따라 None을 반환하는 규칙을 맞춥니다.

변경 사항

  • rsi, atr, dmi, adx, adxr를 nullable 입력 기준으로 전환합니다.
  • ad, cmf, co, efi, eom, nvi, obv, pvi, ultosc, vr 등 pairwise/cumulative family를 같은 계약으로 정리합니다.
  • psar를 포함한 stateful indicator가 gap row에서 output만 None을 내고, 다음 valid row에서 이전 state를 이어서 재개하도록 맞춥니다.
  • 필요한 nullable helper를 core/src/utils.rs에 추가합니다.

리뷰 포인트

  • current row나 required predecessor가 비면 해당 row는 None이어야 합니다.
  • recursive/stateful 계산은 gap을 사이에 두고 series를 압축하지 않은 채 정렬을 유지해야 합니다.
  • accumulator 기반 지표는 invalid row에서 상태를 억지로 갱신하지 않아야 합니다.
  • psar, dmi, atr, rsi가 대표적인 확인 포인트입니다.

범위 외

  • plugin/polars는 여전히 이번 stack 범위 밖입니다.
  • composite/derived indicator의 최종 정리는 상위 PR #17에서 다룹니다.

테스트 계획

  • cargo fmt --package techr-core --check
  • cargo test -p techr-core

Copy link
Copy Markdown
Collaborator Author

mingi3314 commented Apr 21, 2026

@mingi3314 mingi3314 changed the base branch from core-nullable-foundations to graphite-base/16 April 23, 2026 00:56
@mingi3314 mingi3314 force-pushed the core-nullable-stateful branch from f327ee2 to f266b37 Compare April 23, 2026 00:57
@mingi3314 mingi3314 changed the base branch from graphite-base/16 to core-nullable-foundations April 23, 2026 00:57
@mingi3314 mingi3314 force-pushed the core-nullable-stateful branch from f266b37 to 3bbbf2f Compare April 23, 2026 01:47
@mingi3314 mingi3314 changed the title Support nullable core stateful indicators ♻️ core 상태형 지표 nullable 입력 전환 Apr 23, 2026
@mingi3314 mingi3314 force-pushed the core-nullable-stateful branch from 3bbbf2f to 4777d2d Compare April 23, 2026 03:19
@mingi3314 mingi3314 requested a review from sjquant April 23, 2026 08:57
@mingi3314 mingi3314 self-assigned this Apr 23, 2026
@alphaprime-dev-discord
Copy link
Copy Markdown

Copy link
Copy Markdown
Collaborator

@sjquant sjquant left a comment

Choose a reason for hiding this comment

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

Image

[P2] ULTOSC가 기존 1-pass rolling 계산에서 6번의 rolling_sum_strict pass로 바뀌면서 성능 회귀가 큽니다.

기존 구현은 short/medium/long의 BP/TR rolling sum을 한 루프에서 갱신했는데, 현재 구현은 buying_pressures/true_ranges를 만든 뒤 short/medium/long 각각에 대해 총 6번 rolling_sum_strict를 수행합니다. all-Some release 벤치 기준 200k rows에서 main 약 0.93ms, PR 약 2.90ms로 약 3.1배 느려졌습니다. nullable strict semantics는 각 window별 sum과 valid_count를 유지하는 방식으로도 한 pass에 처리할 수 있어 보입니다.

[P2] VR이 3개의 중간 벡터와 3번의 rolling_sum_strict scan을 사용하면서 성능 회귀가 큽니다.

기존 구현은 up/down/same rolling total을 한 루프에서 갱신했는데, 현재 구현은 up/down/same 전체 벡터를 먼저 만들고 각각 rolling_sum_strict를 돌린 뒤 다시 결과를 합칩니다. all-Some release 벤치 기준 200k rows에서 main 약 0.52ms, PR 약 1.70ms로 약 3.3배 느려졌습니다. 각 bucket의 rolling sum과 valid_count를 streaming으로 유지하면 strict window invalidation을 보존하면서 allocation과 반복 scan을 줄일 수 있습니다.

[P2] CMF가 기존 rolling sum 2개를 한 루프에서 유지하던 구조에서 여러 full-array pass 구조로 바뀌었습니다.

현재 구현은 money_flow_volume 전체 벡터를 만든 뒤 volume_sums와 money_flow_sums를 rolling_sum_strict로 각각 계산하고, 마지막에 한 번 더 순회해 결과를 만듭니다. 기존 CMF는 sum_money_flow_volume과 sum_volume을 한 루프에서 갱신했습니다. all-Some release 벤치 기준 200k rows에서 main 약 0.61ms, PR 약 1.23ms로 약 2.0배 느려졌습니다. nullable strict contract는 window valid_count와 두 rolling sum을 한 pass에서 유지하는 방식으로 구현할 수 있어 보입니다.

[P2] PSAR hot loop에서 길이 2짜리 상태를 Vec로 관리하면서 불필요한 동적 조작 비용이 들어갑니다.

현재 구현은 last_valid_highs/last_valid_lows를 Vec로 두고 매 유효 row마다 remove(0)와 push를 호출합니다. 길이가 2라 복잡도상 큰 문제는 아니지만, PSAR는 원래 매우 가벼운 state machine이라 이 비용이 상대적으로 크게 드러납니다. all-Some release 벤치 기준 200k rows에서 main 약 0.35ms, PR 약 0.90ms로 약 2.6배 느려졌습니다. prev_high_1/prev_high_2, prev_low_1/prev_low_2 같은 고정 슬롯 또는 [f64; 2] ring buffer로 바꾸면 nullable semantics를 유지하면서 비용을 줄일 수 있습니다.

[P3] CO는 AD 전체 벡터를 만든 뒤 다시 scan하고 있어 fusion 여지가 있습니다.

현재 구현은 먼저 ad(...)로 전체 AD series를 만든 다음 short/long EMA를 다시 계산합니다. 기존 구조와 크게 다르지는 않지만, nullable 전환 후 all-Some release 벤치 기준 200k rows에서 main 약 0.63ms, PR 약 0.95ms로 약 1.5배 느려졌습니다. CO는 AD 누적값을 순차적으로만 필요로 하므로 CLV/AD 누적과 short/long EMA 갱신을 한 루프에 합칠 수 있습니다. 특히 period_short == period_long 경로는 AD 값을 실제로 계산하지 않고 유효 row에만 Some(0.0)을 내도 됩니다.

======================

성능회귀 이슈가 있는 경우에 대해 AI 리뷰 코멘트를 받아서 공유드립니다.

@mingi3314 mingi3314 force-pushed the core-nullable-stateful branch from c84e6f4 to 307a195 Compare April 24, 2026 01:43
@mingi3314
Copy link
Copy Markdown
Collaborator Author

mingi3314 commented Apr 24, 2026

성능 회귀 코멘트 반영했습니다. 최신 307a195 에서 ULTOSC / VR / CMF / CO는 nullable strict semantics를 유지한 채 streaming/one-pass update 쪽으로 다시 정리했고, PSAR는 길이 2 상태를 Vec 대신 고정 슬롯으로 바꿨습니다.

Copy link
Copy Markdown
Collaborator

@sjquant sjquant left a comment

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 Author

mingi3314 commented Apr 27, 2026

Merge activity

  • Apr 27, 2:16 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 27, 2:17 AM UTC: Graphite rebased this pull request as part of a merge.
  • Apr 27, 2:18 AM UTC: @mingi3314 merged this pull request with Graphite.

@mingi3314 mingi3314 changed the base branch from core-nullable-foundations to graphite-base/16 April 27, 2026 02:16
mingi3314 added a commit that referenced this pull request Apr 27, 2026
## 요약
- 이 PR은 `techr-core`의 nullable 입력 마이그레이션을 시작하는 bottom PR입니다.
- 공개 indicator API의 결측치 계약을 `NaN`이 아닌 `&[Option<f64>]` aligned series로 정리하고, 이후 stacked PR인 `#16`, `#17`이 이 규칙 위에서 stateful/composite 지표를 순차적으로 올립니다.
- 범위는 foundation helper와 단일 시계열 기반 지표 중심이며, `plugin/polars`의 실제 nullable adapter 마이그레이션은 이번 스택에서 제외합니다.

## 배경
- 기존 `core` 함수들은 대부분 dense `&[f64]` 입력을 전제하고 있었습니다.
- 하지만 실제 시계열 처리에서는 결측이 섞인 aligned series를 직접 받을 수 있어야 하고, 표준 라이브러리 성격의 API라면 missing을 `NaN`이 아니라 명시적인 타입으로 다루는 편이 계약이 더 분명합니다.
- 이 스택의 목표는 `techr-core` 전체에서 nullable aligned input을 표준 계약으로 만들고, gap을 건너뛰며 series를 압축하지 않는 일관된 의미론을 정착시키는 것입니다.

## 이 PR에서 하는 일
- `core/src/utils.rs`에 nullable rolling/helper primitive를 추가해 foundation semantics를 먼저 고정합니다.
- `sma`, `ema`, `wma`, `bband`, `env`, `disparity`를 `&[Option<f64>]` 입력 기준으로 전환합니다.
- foundation helper를 사용하는 일부 downstream indicator의 public 시그니처도 같은 계약에 맞게 정렬합니다 (`macd`, `ppo`, `pvo`, `massi`, `sonar` 등).
- 공개 API의 결측치 표현으로 `NaN`을 새로 도입하지 않도록 정리합니다.

## 핵심 규칙
- 출력 길이는 입력 정렬을 그대로 유지합니다.
- rolling 계산은 필요한 lookback window 안에 gap이 있으면 해당 row를 `None`으로 반환합니다.
- recursive smoothing은 gap row에서 output만 `None`이고 내부 상태는 유지합니다.
- valid 값만 따로 압축해서 계산한 뒤 다시 원래 위치에 펴는 방식을 public contract로 사용하지 않습니다.

## 범위 외
- `plugin/polars` nullable adapter 마이그레이션은 후속 작업으로 미룹니다.
- 다만 현재 CI에서 `Polars CI`가 core-only PR에도 실행되기 때문에, 이 PR에는 non-polars 변경에서 `validate`를 성공적인 no-op으로 처리하도록 하는 최소한의 workflow 조정이 함께 포함됩니다.
- 이 workflow 변경은 core nullable 전환 자체의 기능 스코프가 아니라, deferred plugin migration이 현재 스택을 막지 않도록 하는 CI 보정입니다.

## 리뷰 가이드
- 먼저 `core/src/utils.rs`, `sma.rs`, `ema.rs`, `wma.rs`, `bband.rs`, `disparity.rs`를 봐주시면 됩니다.
- 그 다음 downstream 시그니처 정렬과 `.github/workflows/polars-ci.yml` 변경을 확인하시면 충분합니다.
- 이 PR만 `main` 기준으로 리뷰하면 되고, 상위 PR인 `#16`, `#17`은 각각 이 PR을 base로 봐주시면 됩니다.

## 테스트 계획
- [x] `cargo fmt --package techr-core --check`
- [x] `cargo test -p techr-core`
- [x] GitHub Actions `Polars CI`가 non-polars 변경에서 성공적으로 통과하는지 확인
@mingi3314 mingi3314 changed the base branch from graphite-base/16 to main April 27, 2026 02:16
@mingi3314 mingi3314 force-pushed the core-nullable-stateful branch from 1afb218 to aa4fc2b Compare April 27, 2026 02:17
@mingi3314 mingi3314 merged commit ab29c96 into main Apr 27, 2026
mingi3314 added a commit that referenced this pull request Apr 27, 2026
## 요약
- 이 PR은 `#16` 위에 쌓인 stacked PR입니다. 리뷰는 `core-nullable-stateful` 대비로 봐주시면 됩니다.
- 복합/파생 indicator를 nullable contract에 맞추고, `techr-core` 전반의 public API 정합성을 마무리합니다.
- 하위 helper/stateful layer에서 정의된 gap semantics가 상위 지표에서도 그대로 보존되도록 정리합니다.

## 변경 사항
- `macd`, `ppo`, `pvo`, `massi`, `sonar`, `stochf`, `stochs`, `stochrsi` 등 composite indicator를 nullable 입력 기준으로 전환합니다.
- `aroon`, `aroonosc`, `cci`, `cv`, `ichimoku`, `pchan`, `willr`, `roc`, `mom`, `psl`, `erbull`, `erbear` 등 남아 있던 파생 indicator를 같은 계약으로 맞춥니다.
- 내부 helper 사용 경로에서 dense-only 가정이나 `NaN` 기반 우회가 남아 있던 부분을 정리합니다.
- 길이 불일치와 gap 전파가 섞인 경계 케이스를 보강합니다.

## 리뷰 포인트
- composite indicator가 하위 indicator의 gap semantics를 그대로 전파하는지
- rolling extrema 계열이 gap이 들어간 window를 valid 값으로 오판하지 않는지
- signal/oscillator 계열이 nullable intermediate를 압축하지 않고 aligned output을 유지하는지
- `stoch*`, `macd/ppo/pvo`, `ichimoku`, `aroon` 계열이 대표적인 확인 포인트입니다.

## 범위 외
- `plugin/polars` nullable adapter 마이그레이션은 후속 작업입니다.

## 테스트 계획
- [x] `cargo fmt --package techr-core --check`
- [x] `cargo test -p techr-core`
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