Skip to content
All strategies

Strategies

ETH OKX vn.py AtrRsiStrategy — Frozen Source-Default Translation

Outcome: Abandoned

EthOkxFrozenAtrRsiStrategy

Outcome Summary

EthOkxFrozenAtrRsiStrategy attempted a frozen translation of vn.py’s ATR/RSI strategy with capital-relative sizing. Its five-day verification smoke test produced -0.139% per trade over 38 closed positions and a profit factor of 0.626. After three iterations, QA still found entry and trailing-stop execution inconsistent with the source, leading to abandonment before full backtesting or optimization.

Hypothesis

Faithful signal and order translation of AtrRsiStrategy by 用Python的交易员, MIT licensed. Source: https://github.com/vnpy/vnpy_ctastrategy/blob/6ef76981624bf55b2ea978f8587f74d633aafc72/vnpy_ctastrategy/strategies/atr_rsi_strategy.py. Preserve atr_length=22, atr_ma_length=10, rsi_length=5, rsi_entry=16, trailing_percent=0.8 and fixed_size=1. Use ETHUSDT.OKX perpetual with completed 1-MINUTE bars, matching the source's default tick-to-minute BarGenerator. No higher-timeframe aggregation, additional filters or parameter……Show moreShow less

Faithful signal and order translation of AtrRsiStrategy by 用Python的交易员, MIT licensed. Source: https://github.com/vnpy/vnpy_ctastrategy/blob/6ef76981624bf55b2ea978f8587f74d633aafc72/vnpy_ctastrategy/strategies/atr_rsi_strategy.py. Preserve atr_length=22, atr_ma_length=10, rsi_length=5, rsi_entry=16, trailing_percent=0.8 and fixed_size=1. Use ETHUSDT.OKX perpetual with completed 1-MINUTE bars, matching the source's default tick-to-minute BarGenerator. No higher-timeframe aggregation, additional filters or parameter fitting. Platform sizing is the explicitly authorized adaptation: fixed_size=1 represents one capital-relative allocation, sized at entry to 10% of current equity in notional, rounded down to instrument quantity precision. Hold that quantity until exit; never pyramid. Config leverage=1.0. Starting equity is $100,000. Source initialization requests ten days of historical bars; maintain a rolling 100-bar ArrayManager-equivalent buffer and prohibit trading until it is initialized. Logging and event callbacks do not generate signals; order and stop-order callbacks are otherwise no-ops.

The submission supplies executable entry and exit rules. The user's faithful-reproduction requirement takes precedence over inventing a different mechanism to meet diversity quotas. OKX is underrepresented at 2.3% versus its 5% target, and the strategy allows both directions against a portfolio that is 82.6% long-only. Corpus concentration is empty, so instrument-level novelty cannot be established. ETH keeps the literal five-price-unit order offset more proportionate than a low-priced altcoin. This tests volatility-expansion momentum, rather than the failed BTC Connors-RSI reversion. Economic viability remains unverified; the source defaults must be allowed to fail without post-hoc rescue.

verification_loop: Verification failed (Layer 4 — QA review) [class=hypothesis_mismatch]: - [edge_concern] The five-day smoke test returned -0.139% per trade over 38 closed positions, with PF 0.626. Full-history evaluation should examine turnover and trailing-stop giveback; these results do not independently block the strategy. - [critical] The crossed-stop fallback does not preserve source execution. _submit_exit() immediately calls close_position(); OKX has zero configured latency, so Nautilus executes against……Show moreShow less

verification_loop: Verification failed (Layer 4 — QA review) [class=hypothesis_mismatch]: - [edge_concern] The five-day smoke test returned -0.139% per trade over 38 closed positions, with PF 0.626. Full-history evaluation should examine turnover and trailing-stop giveback; these results do not independently block the strategy. - [critical] The crossed-stop fallback does not preserve source execution. _submit_exit() immediately calls close_position(); OKX has zero configured latency, so Nautilus executes against the current market at the decision bar's close, rather than the next bar's open. Furthermore, vn.py checks the renewed stop against the next bar's range: a gap back above a long stop can leave it untriggered. An unconditional market exit changes both timing and trigger conditions. Source: https://raw.githubusercontent.com/vnpy/vnpy_ctastrategy/6ef76981624bf55b2ea978f8587f74d633aafc72/vnpy_ctastrategy/backtesting.py (line 163) — fix: Implement pending source-style stops evaluated against subsequent market data, preserving conditional triggering and gap-price execution. Add an engine execution hook if necessary; do not substitute an immediate market close. Verify ordinary crossing, gap-through, and gap-away cases for both directions. - [critical] BUY limits at close+$5 and SELL limits at close-$5 are marketable when submitted to Nautilus's current close-price book. With post_only=False and zero OKX latency, they can fill on the signal bar. The source backtester matches existing orders before invoking on_bar, so newly generated limits first become eligible on the subsequent bar, with their price constraint still enforced. This changes entries, gap handling, and subsequent trailing-stop state. Source: https://raw.githubusercontent.com/vnpy/vnpy_ctastrategy/6ef76981624bf55b2ea978f8587f74d633aafc72/vnpy_ctastrategy/backtesting.py (line 144) — fix: Provide subsequent-bar activation for the original limit price and entry-sized quantity. Validate that no entry fills on the signal bar and that a next-bar gap beyond the limit prevents an opening fill unless the subsequent range reaches the limit.

Implementation

Frozen translation of vn.py AtrRsiStrategy on ETHUSDT.OKX 1-minute bars. When ATR(22) is above its 10-bar mean and RSI(5) is above 66 (or below 34), it enters with a limit order at close +/- $5 (or the mirror for shorts). It exits only through a 0.8% trailing stop that is renewed every bar. Each entry is sized at 10% of equity in notional.

Iteration 3 fixes QA's critical defect. The venue rejects a renewed trailing stop that is already through the bar close (reject_stop_orders=True), which left the position with no exit. Now, when the stop level is already crossed (stop >= close for longs, stop <= close for shorts), the strategy closes with a reduce-only market order on the next bar. This matches vn.py, where such a stop triggers on the next bar at about the open. Otherwise the renewed STOP_MARKET rests at the venue as before. As a safety net, on_order_rejected flattens the position if any STOP_MARKET is rejected. Entry logic, indicators, sizing and every frozen parameter are unchanged. QA also raised an edge concern: 1-minute bars, taker on both legs, and up to 0.8% given back per trade. That is a property of the frozen source defaults, which the hypothesis says must be allowed to fail without rescue. No tuning was done.

Verification Results

Verification failed (Layer 4 — QA review) [class=hypothesis_mismatch]: - [edge_concern] The five-day smoke test returned -0.139% per trade over 38 closed positions, with PF 0.626. Full-history evaluation should examine turnover and trailing-stop giveback; these results do not independently block the strategy. - [critical] The crossed-stop fallback does not preserve source execution. _submit_exit() immediately calls close_position(); OKX has zero configured latency, so Nautilus executes against the current market……Show moreShow less

Verification failed (Layer 4 — QA review) [class=hypothesis_mismatch]: - [edge_concern] The five-day smoke test returned -0.139% per trade over 38 closed positions, with PF 0.626. Full-history evaluation should examine turnover and trailing-stop giveback; these results do not independently block the strategy. - [critical] The crossed-stop fallback does not preserve source execution. _submit_exit() immediately calls close_position(); OKX has zero configured latency, so Nautilus executes against the current market at the decision bar's close, rather than the next bar's open. Furthermore, vn.py checks the renewed stop against the next bar's range: a gap back above a long stop can leave it untriggered. An unconditional market exit changes both timing and trigger conditions. Source: https://raw.githubusercontent.com/vnpy/vnpy_ctastrategy/6ef76981624bf55b2ea978f8587f74d633aafc72/vnpy_ctastrategy/backtesting.py (line 163) — fix: Implement pending source-style stops evaluated against subsequent market data, preserving conditional triggering and gap-price execution. Add an engine execution hook if necessary; do not substitute an immediate market close. Verify ordinary crossing, gap-through, and gap-away cases for both directions. - [critical] BUY limits at close+$5 and SELL limits at close-$5 are marketable when submitted to Nautilus's current close-price book. With post_only=False and zero OKX latency, they can fill on the signal bar. The source backtester matches existing orders before invoking on_bar, so newly generated limits first become eligible on the subsequent bar, with their price constraint still enforced. This changes entries, gap handling, and subsequent trailing-stop state. Source: https://raw.githubusercontent.com/vnpy/vnpy_ctastrategy/6ef76981624bf55b2ea978f8587f74d633aafc72/vnpy_ctastrategy/backtesting.py (line 144) — fix: Provide subsequent-bar activation for the original limit price and entry-sized quantity. Validate that no entry fills on the signal bar and that a next-bar gap beyond the limit prevents an opening fill unless the subsequent range reaches the limit.

Implement pending source-style stops evaluated against subsequent market data, preserving conditional triggering and gap-price execution. Add an engine execution hook if necessary; do not substitute an immediate market close. Verify ordinary crossing, gap-through, and gap-away cases for both directions.

The crossed-stop fallback does not preserve source execution. _submit_exit() immediately calls close_position(); OKX has zero configured latency, so Nautilus executes against the current market at the decision bar's close, rather than the next bar's open. Furthermore, vn.py checks the renewed stop against the next bar's range: a gap back above a long stop can leave it untriggered. An unconditional market exit changes both timing and trigger conditions. Source: https://raw.githubusercontent.com/vnpy/vnpy_ctastrategy/6ef76981624bf55b2ea978f8587f74d633aafc72/vnpy_ctastrategy/backtesting.py

Provide subsequent-bar activation for the original limit price and entry-sized quantity. Validate that no entry fills on the signal bar and that a next-bar gap beyond the limit prevents an opening fill unless the subsequent range reaches the limit.

BUY limits at close+$5 and SELL limits at close-$5 are marketable when submitted to Nautilus's current close-price book. With post_only=False and zero OKX latency, they can fill on the signal bar. The source backtester matches existing orders before invoking on_bar, so newly generated limits first become eligible on the subsequent bar, with their price constraint still enforced. This changes entries, gap handling, and subsequent trailing-stop state. Source: https://raw.githubusercontent.com/vnpy/vnpy_ctastrategy/6ef76981624bf55b2ea978f8587f74d633aafc72/vnpy_ctastrategy/backtesting.py

Iteration History

Verification failed (Layer 1.5 — hypothesis/config consistency) [class=hypothesis_mismatch]: - optimization_plan fixes leverage at 1.0

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] Sandbox: 38 trades in 5 days, avg_trade_return_pct -0.139%, PF 0.63, commissions 43% of gross. Both entry (a limit at close±$5, so it crosses and fills as taker) and exit (stop-market) pay taker fees, about 0.10% round trip. Each trade gives back up to 0.8% through the trailing stop, and a 1-minute RSI(5)>66 signal has to clear that cost about 7 times a day. Expect thousands of trades over the ~2,444-day history. After the stop fix,……Show moreShow less

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] Sandbox: 38 trades in 5 days, avg_trade_return_pct -0.139%, PF 0.63, commissions 43% of gross. Both entry (a limit at close±$5, so it crosses and fills as taker) and exit (stop-market) pay taker fees, about 0.10% round trip. Each trade gives back up to 0.8% through the trailing stop, and a 1-minute RSI(5)>66 signal has to clear that cost about 7 times a day. Expect thousands of trades over the ~2,444-day history. After the stop fix, check gross per-trade return against the 0.15% floor first. With trade counts this high, a PF near 1 would be a falsified measurement, not something to tune. - [critical] The trailing stop is resubmitted on every bar as a standalone STOP_MARKET at intra_trade_high*(1-0.8%) for longs (or intra_trade_low*(1+0.8%) for shorts). That level includes the current bar's high (or low). If the bar closes more than 0.8% below its own high, the new long stop is already in the market when it is submitted. The OKX backtest venue runs with NautilusTrader's default reject_stop_orders=True; in engine.pyx _process_stop_market_order, a triggered stop is REJECTED, not filled. should_exit() always returns False, so the position has no exit that bar. On later bars the stop level can only rise (longs) or fall (shorts), so it stays in the market and keeps getting rejected until price recovers above it. The position can bleed without limit. In vn.py, the same stop triggers on the next bar and fills at about the open. The hypothesis's only exit, the 0.8% trailing stop, is therefore not implemented reliably. This hits exactly the volatile ATR>ATR_MA regime the strategy selects, including a signal bar with a long wick being the first stop bar. The 5-day sandbox did not happen to hit it (largest loss -0.96% of notional), but over ~2,444 days of 1-minute ETH it will. (line 186) — fix: Before calling _submit_stop_market, test whether the stop is already crossed. For a long, check stop >= current_close; for a short, check stop <= current_close. If it is, call self._submit_exit(position) (a reduce-only market close), which matches vn.py's next-bar stop trigger. Otherwise submit the resting stop as you do now. As a safety net, also track rejected stop orders, e.g. in on_order_rejected, and flatten the position. - [warning] entry_price_offset is a fixed $5. That is about 0.13-0.33% of ETH's price across its history, so the entry limit always crosses and fills as taker at the next bar. This is faithful to the source and is not a defect. Note that vn.py fills at min(limit, next open), and Nautilus behaves similarly. (line 133) — fix: No change needed; recorded for the analyst.

Before calling _submit_stop_market, test whether the stop is already crossed. For a long, check stop >= current_close; for a short, check stop <= current_close. If it is, call self._submit_exit(position) (a reduce-only market close), which matches vn.py's next-bar stop trigger. Otherwise submit the resting stop as you do now. As a safety net, also track rejected stop orders, e.g. in on_order_rejected, and flatten the position.

The trailing stop is resubmitted on every bar as a standalone STOP_MARKET at intra_trade_high*(1-0.8%) for longs (or intra_trade_low*(1+0.8%) for shorts). That level includes the current bar's high (or low). If the bar closes more than 0.8% below its own high, the new long stop is already in the market when it is submitted. The OKX backtest venue runs with NautilusTrader's default reject_stop_orders=True; in engine.pyx _process_stop_market_order, a triggered stop is REJECTED, not filled. should_exit() always……Show moreShow less

The trailing stop is resubmitted on every bar as a standalone STOP_MARKET at intra_trade_high*(1-0.8%) for longs (or intra_trade_low*(1+0.8%) for shorts). That level includes the current bar's high (or low). If the bar closes more than 0.8% below its own high, the new long stop is already in the market when it is submitted. The OKX backtest venue runs with NautilusTrader's default reject_stop_orders=True; in engine.pyx _process_stop_market_order, a triggered stop is REJECTED, not filled. should_exit() always returns False, so the position has no exit that bar. On later bars the stop level can only rise (longs) or fall (shorts), so it stays in the market and keeps getting rejected until price recovers above it. The position can bleed without limit. In vn.py, the same stop triggers on the next bar and fills at about the open. The hypothesis's only exit, the 0.8% trailing stop, is therefore not implemented reliably. This hits exactly the volatile ATR>ATR_MA regime the strategy selects, including a signal bar with a long wick being the first stop bar. The 5-day sandbox did not happen to hit it (largest loss -0.96% of notional), but over ~2,444 days of 1-minute ETH it will.

No change needed; recorded for the analyst.

entry_price_offset is a fixed $5. That is about 0.13-0.33% of ETH's price across its history, so the entry limit always crosses and fills as taker at the next bar. This is faithful to the source and is not a defect. Note that vn.py fills at min(limit, next open), and Nautilus behaves similarly.

Outcome Summary

A faithful translation must preserve subsequent-bar order eligibility, conditional stop triggering and gap execution, as well as indicator calculations.

After three iterations, Layer 4 QA rejected the translation for hypothesis mismatch: crossed-stop market exits changed source timing and trigger conditions, while marketable entry limits could fill on the signal bar. The smoke-test losses were an edge concern, not an independent blocker.

Translate vn.py’s source-default ATR/RSI strategy to ETHUSDT.OKX one-minute perpetual bars, preserving its signals and orders while sizing entries at 10% of equity with 1.0 leverage.

A five-day verification smoke test recorded -0.139% per trade across 38 closed positions, with a profit factor of 0.626. Full backtesting, optimization, analyst evaluation and risk review were not reached.

Abandon Reason

verification_loop: Verification failed (Layer 4 — QA review) [class=hypothesis_mismatch]: - [edge_concern] The five-day smoke test returned -0.139% per trade over 38 closed positions, with PF 0.626. Full-history evaluation should examine turnover and trailing-stop giveback; these results do not independently block the strategy. - [critical] The crossed-stop fallback does not preserve source execution. _submit_exit() immediately calls close_position(); OKX has zero configured latency, so Nautilus executes against……Show moreShow less

verification_loop: Verification failed (Layer 4 — QA review) [class=hypothesis_mismatch]: - [edge_concern] The five-day smoke test returned -0.139% per trade over 38 closed positions, with PF 0.626. Full-history evaluation should examine turnover and trailing-stop giveback; these results do not independently block the strategy. - [critical] The crossed-stop fallback does not preserve source execution. _submit_exit() immediately calls close_position(); OKX has zero configured latency, so Nautilus executes against the current market at the decision bar's close, rather than the next bar's open. Furthermore, vn.py checks the renewed stop against the next bar's range: a gap back above a long stop can leave it untriggered. An unconditional market exit changes both timing and trigger conditions. Source: https://raw.githubusercontent.com/vnpy/vnpy_ctastrategy/6ef76981624bf55b2ea978f8587f74d633aafc72/vnpy_ctastrategy/backtesting.py (line 163) — fix: Implement pending source-style stops evaluated against subsequent market data, preserving conditional triggering and gap-price execution. Add an engine execution hook if necessary; do not substitute an immediate market close. Verify ordinary crossing, gap-through, and gap-away cases for both directions. - [critical] BUY limits at close+$5 and SELL limits at close-$5 are marketable when submitted to Nautilus's current close-price book. With post_only=False and zero OKX latency, they can fill on the signal bar. The source backtester matches existing orders before invoking on_bar, so newly generated limits first become eligible on the subsequent bar, with their price constraint still enforced. This changes entries, gap handling, and subsequent trailing-stop state. Source: https://raw.githubusercontent.com/vnpy/vnpy_ctastrategy/6ef76981624bf55b2ea978f8587f74d633aafc72/vnpy_ctastrategy/backtesting.py (line 144) — fix: Provide subsequent-bar activation for the original limit price and entry-sized quantity. Validate that no entry fills on the signal bar and that a next-bar gap beyond the limit prevents an opening fill unless the subsequent range reaches the limit.

Analysis

AtrRsiStrategy

Backtest and paper results are hypothetical. Trading involves risk of loss.