From ef2a6c5ee0f9f0c001f7856fb0ce433130be0b8d Mon Sep 17 00:00:00 2001 From: TPTBusiness Date: Fri, 24 Apr 2026 20:19:07 +0200 Subject: [PATCH] fix(factors): extend look-ahead rules to session factors and add intraday-factor guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rule 7 extended: session-based aggregations (London/NY/Asian) must also be shifted by 1 trading day before use — same as daily aggregations - Rule 8 added: prefer pure intraday rolling factors (RSI, Bollinger, VWAP deviation, rolling std) that have no look-ahead risk and vary every minute - predix_full_eval.py: apply _shift_daily_constant_factor_if_needed before IC - predix_gen_strategies_real_bt.py: improved swing prompt with daily-level signal logic guidance for daily-constant factors Co-Authored-By: Claude Sonnet 4.6 --- rdagent/scenarios/qlib/experiment/prompts.yaml | 15 ++++++++++++++- scripts/predix_gen_strategies_real_bt.py | 18 +++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/rdagent/scenarios/qlib/experiment/prompts.yaml b/rdagent/scenarios/qlib/experiment/prompts.yaml index 9f0269f7..736b1c97 100644 --- a/rdagent/scenarios/qlib/experiment/prompts.yaml +++ b/rdagent/scenarios/qlib/experiment/prompts.yaml @@ -134,7 +134,20 @@ qlib_factor_strategy: |- # then map back to minute bars via ffill ``` This rule applies to ALL daily aggregations: returns, OHLC stats, volume, momentum, slopes, etc. - Intraday rolling factors (e.g. 30-min rolling std) do NOT need this shift — only daily aggregations do. + **Session-based aggregations (London, NY, Asian session returns) are also daily aggregations** — the London + session (08:00-16:00 UTC) ends at 16:00, so its return must be shifted by 1 day before use. + Intraday rolling factors (e.g. 30-min rolling std computed at bar t using only bars t-N..t-1) do NOT need this shift. + + 8. **PREFER pure intraday rolling factors**: Factors that use only a trailing window of recent bars (e.g. + rolling(30).mean() of returns, RSI(14), Bollinger Band z-score) have NO look-ahead risk and vary every + minute. These are the best candidates for short-horizon (96-bar) prediction. Examples: + - Rolling 15-min / 30-min / 60-min return momentum + - Rolling volatility (std of returns over 20-60 bars) + - Distance of close from N-bar moving average (z-score) + - RSI or similar oscillators computed on 1-min bars + - VWAP deviation (requires volume — use $volume column) + Always use `.shift(1)` on the lagged window (e.g. `rolling(N).mean().shift(1)`) to avoid using the + current bar's own price in its own feature value. qlib_factor_output_format: |- Your output should be a pandas dataframe similar to the following example information: diff --git a/scripts/predix_gen_strategies_real_bt.py b/scripts/predix_gen_strategies_real_bt.py index 5b070343..6c964d70 100644 --- a/scripts/predix_gen_strategies_real_bt.py +++ b/scripts/predix_gen_strategies_real_bt.py @@ -250,7 +250,7 @@ Hard requirements: - NO global mean/std — always use rolling(window).mean() with shift(1) to avoid look-ahead bias""" else: - system_prompt = f"""You are a quantitative trading expert specializing in EUR/USD intraday strategies. + system_prompt = f"""You are a quantitative trading expert specializing in EUR/USD daily swing strategies. CRITICAL RULES for {STYLE_DESC} (forward horizon: {FORWARD_BARS} bars = ~{FORWARD_BARS/60:.1f} hours): 1. ONLY use the factors listed below - no others! @@ -258,15 +258,27 @@ CRITICAL RULES for {STYLE_DESC} (forward horizon: {FORWARD_BARS} bars = ~{FORWAR 3. Create a pandas Series called 'signal' with values: 1 (long), -1 (short), 0 (neutral) 4. signal.index MUST match close.index 5. signal.name must be 'signal' +6. IMPORTANT: factors are DAILY values broadcast to every 1-minute bar — they change once per day. + Use daily-level logic: compare today's factor value to a rolling daily mean (window 5-20 DAYS). + To get daily rolling mean: group by date, take first value per day, compute rolling, then reindex back. + Example: dates = factors[col].index.get_level_values('datetime').normalize() + daily_vals = factors[col].groupby(dates).first() + daily_mean = daily_vals.rolling(10).mean().shift(1) + daily_signal = (daily_vals > daily_mean).astype(int) * 2 - 1 + signal = daily_signal.reindex(dates).values (broadcast back to minute bars) +7. The signal should change roughly once per day — this produces ~250-500 trades over 6 years. +8. Keep conditions SIMPLE: one factor above/below its N-day rolling average. Avoid combining 3+ conditions. Output ONLY valid JSON with these fields: {{"strategy_name": "short_name", "factor_names": ["f1", "f2"], "description": "one sentence", "code": "python code"}}""" - user_prompt = f"""Create a EUR/USD trading strategy using these factors: + user_prompt = f"""Create a EUR/USD SWING trading strategy (hold ~{FORWARD_BARS/60:.0f} hours) using these factors: {factor_list} -{f'Previous feedback: {feedback}' if feedback else 'First attempt - be creative!'}""" +{f'Previous feedback: {feedback}' if feedback else 'First attempt - be creative!'} + +Use daily-level signal logic (factor above/below rolling daily mean). Signal changes once per day.""" api = APIBackend() response = api.build_messages_and_create_chat_completion(