Skip to content

fix(strategy-v2): align monthly timeframe duration, annualization periods, and driving frequency - #298

Merged
brokermr810 merged 1 commit into
OpenByteInc:mainfrom
huuan26:fix/strategy-v2-frequency-annualization-alignment
Oct 19, 2026
Merged

brokermr810 merged 1 commit into
OpenByteInc:mainfrom
huuan26:fix/strategy-v2-frequency-annualization-alignment

Conversation

@huuan26

@huuan26 huuan26 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes monthly timeframe handling across Strategy API V2 and technical indicator alerts:

  1. Aligns FREQUENCY_SECONDS and TIMEFRAME_SECONDS with standard 30-day monthly bar duration (2,592,000 seconds).
  2. Fixes periods_per_year returning daily trading days (252 or 365.25) for monthly timeframes instead of 12.0.
  3. Corrects driving_frequency resolution when comparing monthly against weekly intervals.
  4. Decouples strategy subscription validation from the frequency duration table via SUPPORTED_SUBSCRIPTION_FREQUENCIES.
  5. Maps "1mo": "1M" in market data PROVIDER_TIMEFRAMES and adds "1mo": 3600 polling interval to indicator signal alerts.

Mathematical & Architectural Formulation

1. Duration & Driving Frequency Hierarchy

In multi-timeframe analysis, the driving frequency $f_{\mathrm{driver}}$ represents the fastest subscribed bar frequency (i.e., minimal bar interval in seconds):

$$f_{\mathrm{driver}} = \mathrm{argmin}_{f \in F} T(f)$$

where $T(f)$ denotes the duration in seconds. Previously, "1mo" was absent from FREQUENCY_SECONDS, causing frequency_seconds("1mo") to fall back to daily duration (86,400 s). Consequently:

$$T(\mathrm{1mo}) = 86{,}400\text{ s} < T(\mathrm{1w}) = 604{,}800\text{ s}$$

which incorrectly designated monthly as driving faster than weekly.

With "1mo": 2_592_000 (standard financial convention of 30 days: $30 \times 86{,}400 = 2{,}592{,}000\text{ s}$), strict monotonic ordering holds:

$$T(\mathrm{1m}) < T(\mathrm{5m}) < T(\mathrm{1h}) < T(\mathrm{1d}) < T(\mathrm{1w}) < T(\mathrm{1mo})$$

2. Metric Annualization Factor ($N$)

Annualized performance metrics (Sharpe ratio, Sortino ratio, annualized returns, and volatility) scale periodic returns $R_t$ with frequency-specific period factor $N$ (from periods_per_year):

$$\mu_{\mathrm{annual}} = \mu_{\mathrm{period}} \times N, \quad \sigma_{\mathrm{annual}} = \sigma_{\mathrm{period}} \times \sqrt{N}$$

For monthly returns, $N = 12.0$. Previously, monthly frequencies ("1mo", "1month", "monthly") fell through to the daily trading days count:

  • USStock / Equities: $N = 252.0$ (overestimating annualized volatility by a factor of $\sqrt{252 / 12} = \sqrt{21} \approx 4.58$).
  • Crypto: $N = 365.25$ (overestimating annualized volatility by a factor of $\sqrt{365.25 / 12} \approx 5.52$).

With this fix, periods_per_year explicitly returns $12.0$ for monthly frequency across all asset classes.

3. Decoupling Subscription Validation from Duration Table

contract.py previously checked:

unsupported = [item for item in frequencies if item not in FREQUENCY_SECONDS]

This conflated the duration lookup table with Strategy API V2's execution subscription allowlist. While Strategy V2 explicitly restricts execution subscriptions to native bar frequencies up to weekly (1m to 1w, as documented in Section 6 of STRATEGY_DEV_GUIDE.md and enforced by test_contract_rejects_unsupported_monthly_subscription), general frequency utilities, market data loaders, and alert polling services need accurate monthly duration and annualization values.

Introducing SUPPORTED_SUBSCRIPTION_FREQUENCIES = ("1m", "3m", "5m", "15m", "30m", "1h", "4h", "1d", "1w") keeps compile_strategy_v2 strictly compliant without corrupting duration and annualization lookups.

Changes

  • backend_api_python/app/services/strategy_v2/frequencies.py:
    • Add "1mo": 2_592_000 to FREQUENCY_SECONDS.
    • Define and export SUPPORTED_SUBSCRIPTION_FREQUENCIES.
    • Add aliases for "1week", "1wk", "w1", and "1month".
    • Update periods_per_year to return 12.0 when frequency is "1mo" or "1month".
  • backend_api_python/app/services/strategy_v2/contract.py:
    • Validate subscription frequencies against SUPPORTED_SUBSCRIPTION_FREQUENCIES rather than FREQUENCY_SECONDS.
  • backend_api_python/app/services/strategy_v2/market_data.py:
    • Add "1mo": 2592000 to TIMEFRAME_SECONDS.
    • Add "1mo": "1M" to PROVIDER_TIMEFRAMES.
  • backend_api_python/app/services/indicator_signal_alerts.py:
    • Add "1mo": 3600 to _poll_seconds mapping.
  • Tests:
    • Add backend_api_python/tests/test_frequencies.py covering canonical aliases, duration hierarchy, driving frequency selection, annualization factors, and subscription set exclusion.
    • Extend backend_api_python/tests/test_indicator_signal_alerts.py to verify polling intervals for "1mo", "1MO", and "monthly".

Test plan

  • backend_api_python/tests/test_frequencies.py passes (6/6 tests)
  • backend_api_python/tests/test_strategy_v2_multitimeframe.py passes (14/14 tests)
  • backend_api_python/tests/test_indicator_signal_alerts.py passes (31/31 tests)
  • python backend_api_python/scripts/backend_quality_check.py passes with zero violations

API documentation (if routes/schemas changed)

  • No routes or OpenAPI schemas changed

…iods, and driving frequency

- Add '1mo': 2_592_000 to FREQUENCY_SECONDS and TIMEFRAME_SECONDS
- Map '1mo': '1M' in PROVIDER_TIMEFRAMES and add '1mo': 3600 poll interval
- Define SUPPORTED_SUBSCRIPTION_FREQUENCIES to decouple subscription validation from duration lookup
- Fix periods_per_year to return 12.0 for monthly timeframe instead of trading_days fallback
- Correct driving_frequency comparison so 1w (604,800s) takes precedence over 1mo (2,592,000s)
- Add comprehensive test suite in test_frequencies.py
@brokermr810
brokermr810 merged commit 8db718f into OpenByteInc:main Oct 19, 2026
9 checks passed
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