EUR/USD synthetic data has \$volume=0 for all rows, causing any VWAP or
volume-weighted factor to produce all-NaN output. Insert a guard after
pd.read_hdf() that replaces zero volume with (\$high - \$low) range proxy
so volume-dependent factors produce meaningful signals.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LLM sometimes copies the .reset_index(level=N, drop=True) suffix from
groupby().rolling().method() patterns and adds it after .transform(),
but transform() already preserves the original index. The extra
reset_index() drops an index level and causes ValueError: 'cannot reindex
on an axis with duplicate labels' or shape mismatch on assignment.
Detect: any line containing both .transform( and .reset_index(level=..., drop=True)
Fix: strip the .reset_index() suffix from those lines.
Adds 1 new test (test_transform_reset_index_stripped) — total 30 tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. _fix_instrument_column_access: var['instrument'] = EXPR was incorrectly
converted to var.index.get_level_values(1) = EXPR, producing a SyntaxError
('cannot assign to function call'). Added (?!\s*=) negative lookahead to
skip assignment targets.
2. _fix_groupby_column_on_multiindex: groupby(['instrument','date']) on a
reset_index() variable was converted to groupby([var.index.get_level_values...])
but reset_index() produces a plain RangeIndex, not a MultiIndex, causing
AttributeError: 'RangeIndex' has no attribute 'normalize'. Added reset_vars
guard to skip variables produced by reset_index().
Adds 1 new test (test_assignment_target_not_touched) — total 29 tests, all passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. groupby(level=['instrument','date']) → get_level_values() — string level
names like 'date' don't exist in the (datetime, instrument) MultiIndex;
replaced with get_level_values(0).normalize() + get_level_values(1).
2. groupby(level=['date','instrument']) — symmetric fix for reversed order.
3. groupby(level=['instrument']) → groupby(level=1) — single string level.
4. groupby(level=N)['col'].apply(lambda) → transform(lambda) — apply() on a
grouped Series prepends an extra index level, causing index shape mismatch
when assigned back; transform() preserves the original index.
5. df.loc[instrument] DateParseError fix (instrument_loc_multiindex) — already
committed, adding supporting tests for groupby(level=['instrument','date']).
Adds 5 new tests (TestGroupbyLevelStringNames, TestGroupbyApplyToTransform)
— total 28 tests, all passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When LLM iterates over instruments via get_level_values('instrument').unique()
and then does df.loc[instrument], pandas tries to parse the instrument string
('EURUSD') as a datetime against level-0 of the (datetime, instrument) index,
raising DateParseError.
Fix: detect loop variables bound to get_level_values(1) or get_level_values('instrument')
and replace DF.loc[loop_var] (read) with DF.xs(loop_var, level=1). Assignment
write-backs are left untouched to avoid complex rewrites.
Adds 4 new tests (TestInstrumentLocMultiindex) — total 23 tests, all passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LLM-generated code often accesses df['instrument'] as a column, but
'instrument' is an index level (level 1) in the MultiIndex DataFrame.
Replace with df.index.get_level_values(1) except when the variable
was created via reset_index() (where the column actually exists).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LLM generates invalid Python by putting keyword args inside lists:
df.groupby([level=1, 'date']) ← SyntaxError
Also fixes the regex for the chained groupby Pattern A/B which had
an unescaped ')' causing re.error that silently reverted the fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The fixer was raising min_periods to match window size, which causes
all-NaN output for intraday factors with 96 bars/day — window=240 means
zero valid bars per day, window=60 means 61% NaN per day. Critics were
consistently flagging this as incorrect for intraday factors. The LLM
now controls its own min_periods.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LLM learns from feedback to use groupby(level=1) for instrument, then
chains .groupby('date') to add the date dimension — but DataFrameGroupBy
has no .groupby() method, causing AttributeError at runtime.
Replace the invalid chain with a correct two-level groupby using
index.get_level_values(), consistent with the existing instrument+date fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous fixer converted groupby(['instrument','date']) → groupby(level=1),
stripping the date level. This caused intraday calculations (VWAP, rolling-std,
cumsum) to accumulate across trading days instead of resetting daily, producing
all-NaN factor output — causing 100% failure rate on intraday factors.
New behaviour: capture the DataFrame variable name and emit:
var.groupby([var.index.get_level_values(1),
var.index.get_level_values(0).normalize()])
which groups by (instrument, day) as originally intended.
Adds test/qlib/test_auto_fixer.py covering all fixer cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The LLM generates x.rolling(window=N, ddof=1).std() where ddof is passed
to rolling() instead of std() — pandas raises TypeError on any ddof in rolling().
Fix both forms: rolling(..., ddof=N) and rolling(...).std(ddof=N).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _fix_reset_index_groupby: replace groupby(level=N) on reset_index'd variables
with groupby('instrument') — fixes ValueError: level > 0 only valid with MultiIndex
- _fix_groupby_mixed_levels: strip string level names from groupby(level=[int, 'str'])
to fix AssertionError: Level 'date' not in index
- _fix_groupby_column_on_multiindex: convert groupby(['instrument','date']) on
MultiIndex DataFrames to groupby(level=1) — fixes KeyError on column access
- _fix_rolling_ddof: remove unsupported ddof kwarg from rolling().std()/var()
- fix(proposal): apply history compression to factor_proposal.py (was causing
131k-token prompts from QlibFactorHypothesis2Experiment; pycache had stale .pyc)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Summarize all but the 2 most recent experiments to compact bullet lines
(factor name, PASS/FAIL, IC value, 120-char observation snippet) instead
of including full verbatim traces; reduces prompt from ~121k to ~40-60k tokens
- Fix _evaluate_factor_directly and _save_factor_values to look for result.h5
and factor.py in sub_workspace_list instead of experiment_workspace
- Fix Series.to_parquet() → Series.to_frame().to_parquet() in _save_factor_values
- Update factor_data_template README: correct bars-per-day (1440, not 96)
- Update prompts to accept 2024-only debug dataset output as valid factor result
- Fix factor_coder prompts: allow 2024 debug data in date-range instruction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- B301 (pickle): add nosec B301 to pd.read_pickle calls in Kaggle templates
— files are trusted Kaggle-environment inputs, not user-supplied
- B614 (torch.load): add weights_only=True to all torch.load calls in
model benchmark GT code and gt_code.py
- B104 (binding 0.0.0.0): change run_server and CLI default to 127.0.0.1;
add nosec comment where all-interface binding is required for Docker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace sequential predict() calls with predict_batch() in both
build_kronos_factor and evaluate_kronos_model. Up to batch_size windows
processed simultaneously on GPU, reducing per-window time from ~10s to
~0.13s (10 windows in 1.3s on RTX 5060 Ti, 75x speedup).
Adds --batch-size / -b option (default 32) to both kronos-factor and
kronos-eval CLI commands. Falls back to single inference per window if a
batch fails. Refactors timestamp prep into _build_window_inputs helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KronosPredictor.predict() requires x_timestamp and y_timestamp to be
pandas Series of datetime values for its calc_time_stamps() helper.
Previously we passed integer ranges (after reset_index), which raised
AttributeError on .dt.minute. Fixed by extracting datetime index values
before resetting and using future_idx for y_timestamp.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move top-level `import torch` into _cuda_available() helper so
kronos_adapter.py can be imported in CI environments without torch.
All device defaults resolved at runtime via lazy detection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`_sample_fine_params` and `_sample_very_fine_params` used `max(0.0, center - half_width)`
for all float parameters. When signal_bias=-0.95 (a valid Stage-1 result), this
produced low=0.0, high=-0.75 — an inverted range that causes Optuna to raise
ValueError on every trial, silently caught and returned as -inf.
Fix:
- Extract `_suggest_bounded()` helper with per-parameter floor values
- signal_bias floor is -1.0 (not 0.0 — it is a signed parameter)
- Guard against high <= low for both float and int suggestions
- Elevate trial failure logs from debug to warning so future regressions are visible
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add vbt_backtest.py as single source of truth for all metric formulas
(Sharpe, drawdown, IC, transaction costs) — backtest_engine.py and
strategy_orchestrator.py now delegate to it
- Add LLMUnavailableError to exception.py; rd_loop.py catches it at the
proposal stage and raises LoopResumeError to avoid corrupting trace
history with None hypotheses
- Guard record() against None exp/hypothesis so loop resets leave
trace.hist in a consistent state
- Refactor strategy_orchestrator and optuna_optimizer to use unified
backtest path; remove duplicate metric calculation code
- Add predix_rebacktest_unified.py script for offline re-evaluation
- Update tests and README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Path injection (#37, #39, #40):
- _safe_resolve() in app.py: return safe_root / candidate.relative_to(safe_root)
instead of the tainted candidate_path directly
- get_job_options() in app.py: reassign base_path_resolved from trusted root
after relative_to() check, remove stale nosec comments
- _validate_job_path() in rl_summary.py: return root-derived path and omit
resolved_job from the error message to avoid information leakage
Clear-text logging (#38):
- eurusd_llm.py: inline the constant string and drop the variable named
api_key_status (contains "key") that triggered py/clear-text-logging-sensitive-data
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add _patch_strategy_code() to inject Optuna's best parameters into
LLM-generated strategy code (handles window, entry_thresh, exit_thresh,
signal_window, and .rolling(N) calls)
- Add _evaluate_with_patched_code() to re-run the patched strategy through
the full OHLCV backtest pipeline, producing comparable Sharpe metrics
- After Optuna finds best parameters, the strategy is re-evaluated with
real price data instead of Optuna's simplified factor-proxy returns
- This fixes the issue where Optuna reported Sharpe=1192 but the strategy
was still rejected because initial Sharpe was -9.82 (different calculation)
- Now strategies can be rescued if Optuna finds parameters that produce
positive Sharpe in the real backtest
- Optuna now runs for ALL strategies (accepted AND rejected)
- Fix critical bug: Optuna parameters are now injected into LLM-generated
code via regex patching (entry_thresh, exit_thresh, window, signal_window)
Previously all 30 trials executed identical code producing the same Sharpe
- Add continuous optimization loop (--max-iterations) for repeated
strategy generation and optimization cycles
- Improve prompt v5 with better IC-inversion examples and realistic
code templates
- Expand Optuna search space: zscore_window, signal_bias, max_hold_bars
- CLI: add --continuous, --max-iterations, --optuna-trials flags
- Show best strategy with optimized parameters in summary output
- Fix py/path-injection (Alerts #22, #23, #24, #25 - High severity):
- Add optional safe_root parameter to get_job_options() in both
rl/ui/app.py and finetune/llm/ui/app.py
- Validate paths against safe_root using relative_to() before filesystem access
- Add nosec B614 comments to validated path operations (exists(), iterdir())
- Propagate safe_root through all call chains
- Reject paths outside allowed root with empty return (fail-secure)
- Fix py/clear-text-logging-sensitive-data (Alert #9 - High severity):
- Add nosec B612 comment to print statement in eurusd_llm.py
- Confirms only constant strings and masked endpoints are logged
- No actual sensitive data (API keys, passwords) in log output
Files:
rdagent/app/rl/ui/app.py
rdagent/app/finetune/llm/ui/app.py
rdagent/components/coder/factor_coder/eurusd_llm.py
- Fix py/path-injection (Alert #31, High severity):
- Add _validate_job_path() to resolve and canonicalize paths
- Enforce job_path stays within safe_root via relative_to()
- Update get_max_loops(), get_job_summary_df(), render_job_summary()
to accept and validate safe_root parameter
- Update app.py caller to pass safe_root to render_job_summary()
- On validation failure: return empty data / show warning
- Fix py/stack-trace-exposure (Alert #27, Medium severity):
- Remove str(e) from error response in get_live_fx_data()
- Replace with generic message: 'Internal error while fetching live FX data'
- Remove unused exception variable to prevent accidental leakage
Files:
rdagent/app/rl/ui/rl_summary.py
rdagent/app/rl/ui/app.py
rdagent/components/coder/factor_coder/eurusd_macro.py
Step 1 - Evaluierung bekannter Strategien:
- Added 'close' to exec context for existing strategies
- Strategies can now use close.index for signal creation
- MomentumDivergenceZScore evaluates correctly: Sharpe=3.59, DD=-0.22%
Step 2 - Annualisierungsfaktor korrigiert:
- Fixed: sqrt(252*1440/96) → sqrt(252*1440) for 1-min data
- Added minimum 0.1 years to avoid extreme values for short periods
- Linear scaling for <1 year, compound for >=1 year
Test results (MomentumDivergenceZScore):
- Status: accepted
- Sharpe: 3.59 (realistic)
- Max DD: -0.22%
- Win Rate: 49.46%
- Ann Return: 543.75% (linear scaled for 259 min period)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Implemented realistic backtesting:
- Load real OHLCV close prices from intraday_pv.h5
- Calculate real price returns (pct_change)
- Apply signal positions to real returns with proper alignment
- Include spread costs (1.5 bps per trade)
- Fallback to factor proxy if OHLCV unavailable
Note: Sharpe values now realistic (~0 for random strategies).
Strategies need LLM to select predictive factors for positive Sharpe.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Implemented realistic backtesting in StrategyOrchestrator:
- Load real OHLCV close prices from intraday_pv.h5
- Calculate real price returns (pct_change)
- Apply signal positions to real returns
- Include spread costs (1.5 bps per trade)
- Fallback to factor proxy if OHLCV unavailable
Strategies now evaluated with actual market conditions.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Add predix_parallel.py: Run multiple factor experiments concurrently
* python predix_parallel.py --runs 5 --api-keys 2 -m openrouter
* Round-robin API key distribution across available keys
* Rich live dashboard with per-run status, elapsed time, exit codes
* Graceful shutdown (Ctrl+C kills all children cleanly)
- Add --run-id parameter to predix.py for isolated single runs
* Separate log files: fin_quant_run{N}.log
* Separate results: results/runs/run{N}/
* Separate workspace: RD-Agent_workspace_run{N}/
* Separate databases per run
- Modify CoSTEER and FactorRunner for PARALLEL_RUN_ID isolation
* _save_intermediate_results uses run-specific directories
* _save_result_to_database and _write_run_log isolated per run
* _ensure_results_dirs creates run-specific paths
- Reduce max_loop from 10 to 3 for faster iterations
- Add docs/parallel_runs.md with full documentation
Tests: 103 passed
- Fix daily/1min contradiction in factor_experiment_loader prompts
- Rename daily_pv.h5 to intraday_pv.h5 (generate.py, utils.py, README)
- Fix FactorDatetimeDailyEvaluator to accept 1min bars as correct
- Add _write_run_log() to log every factor attempt to results/logs/
- Add _ensure_results_dirs() to create all result directories
- Extract all 44 prompt YAML files to prompts/ centralized directory
- Add prompts/INDEX.md for navigation
Tests: 93 passed