mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 23:47:46 +00:00
refactor: rename project from Predix to NexQuant
Rename all source files, scripts, tests, documentation, and configuration from Predix/predix to NexQuant/nexquant across the entire codebase.
This commit is contained in:
@@ -5,8 +5,8 @@ import numpy as np
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
|
||||
OHLCV_PATH = Path('/home/nico/Predix/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
FACTORS_DIR = Path('/home/nico/Predix/results/factors')
|
||||
OHLCV_PATH = Path('/home/nico/NexQuant/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
FACTORS_DIR = Path('/home/nico/NexQuant/results/factors')
|
||||
VALUES_DIR = FACTORS_DIR / 'values'
|
||||
|
||||
print("=" * 70)
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
Option A: Generate Kronos predicted-return factor from EUR/USD 1-min data.
|
||||
|
||||
Runs Kronos-mini inference in daily strides (96 bars/day) over all available
|
||||
OHLCV data and saves the resulting factor for use in Predix's factor pipeline.
|
||||
OHLCV data and saves the resulting factor for use in NexQuant's factor pipeline.
|
||||
|
||||
Usage:
|
||||
conda activate predix
|
||||
conda activate nexquant
|
||||
python scripts/kronos_factor_gen.py
|
||||
python scripts/kronos_factor_gen.py --context 512 --pred 96 --device cuda
|
||||
python scripts/kronos_factor_gen.py --device cpu # slower but no GPU needed
|
||||
@@ -71,7 +71,7 @@ def main():
|
||||
print(f"\nSample (first 5):")
|
||||
print(factor_df.head())
|
||||
|
||||
# Save metadata for predix.py top / best integration
|
||||
# Save metadata for nexquant.py top / best integration
|
||||
meta = {
|
||||
"factor_name": f"KronosPredReturn_p{args.pred}",
|
||||
"description": f"Kronos-mini predicted return, {args.pred}-bar horizon",
|
||||
|
||||
@@ -6,7 +6,7 @@ Computes IC (Information Coefficient) and hit rate for Kronos predictions
|
||||
vs actual realized returns. Results are printed for comparison with LightGBM.
|
||||
|
||||
Usage:
|
||||
conda activate predix
|
||||
conda activate nexquant
|
||||
python scripts/kronos_model_eval.py
|
||||
python scripts/kronos_model_eval.py --pred 30 --context 512 --device cuda
|
||||
"""
|
||||
|
||||
@@ -10,8 +10,8 @@ For each accepted strategy, add:
|
||||
- Generate Live Trading report
|
||||
|
||||
Usage:
|
||||
python predix_add_risk_management.py
|
||||
python predix_add_risk_management.py --live # Mark as live-ready
|
||||
python nexquant_add_risk_management.py
|
||||
python nexquant_add_risk_management.py --live # Mark as live-ready
|
||||
"""
|
||||
import os, sys, json, time
|
||||
from pathlib import Path
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
NexQuant Auto-Pilot — vollautomatischer Strategie-Generator.
|
||||
|
||||
Läuft unbegrenzt, kein menschlicher Eingriff nötig.
|
||||
Jede Runde: Factors laden → LLM Code → Pre-Flight → Backtest → Optuna → Ensemble
|
||||
Bei Crash: auto-restart nach 30s.
|
||||
|
||||
Usage:
|
||||
python scripts/nexquant_autopilot.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json, logging, os, sys, time, traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np, pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
# Load .env before any rdagent imports (required for pydantic-settings)
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
_env_path = Path(__file__).resolve().parent.parent / ".env"
|
||||
load_dotenv(_env_path)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("autopilot")
|
||||
|
||||
LOG_FILE = Path(__file__).resolve().parent.parent / "git_ignore_folder" / "logs" / f"autopilot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
fh = logging.FileHandler(str(LOG_FILE))
|
||||
fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
|
||||
logger.addHandler(fh)
|
||||
|
||||
BATCH_SIZE = 2
|
||||
OPTUNA_TRIALS = 10
|
||||
COOLDOWN = 30
|
||||
MAX_CONSECUTIVE_FAILS = 5
|
||||
|
||||
def main_round(style: str, round_num: int) -> int:
|
||||
"""Run one round. Returns number of accepted strategies."""
|
||||
from rdagent.scenarios.qlib.local.strategy_orchestrator import StrategyOrchestrator
|
||||
|
||||
accepted_count = 0
|
||||
try:
|
||||
orch = StrategyOrchestrator(
|
||||
top_factors=20, trading_style=style,
|
||||
min_sharpe=0.1, use_optuna=True, optuna_trials=OPTUNA_TRIALS,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Orchestrator init failed: {e}")
|
||||
return 0
|
||||
|
||||
try:
|
||||
results = orch.generate_strategies(count=BATCH_SIZE, workers=1)
|
||||
except Exception as e:
|
||||
logger.error(f"generate_strategies failed: {e}")
|
||||
return 0
|
||||
|
||||
for r in results:
|
||||
status = r.get("status", "?")
|
||||
if status == "accepted":
|
||||
accepted_count += 1
|
||||
logger.info(f" ✓ {r.get('strategy_name','?')[:40]:40s} S={r.get('sharpe_ratio',0):.1f} OOS={r.get('oos_sharpe',0):.1f}")
|
||||
else:
|
||||
reason = r.get("reason", "?")[:80]
|
||||
logger.debug(f" ✗ {r.get('strategy_name','?')[:40]:40s} {reason}")
|
||||
|
||||
if accepted_count >= 2:
|
||||
try:
|
||||
ensemble = orch.build_ensemble(results)
|
||||
if ensemble and ensemble.get("status") == "success":
|
||||
logger.info(f" Ensemble: S={ensemble['sharpe_ratio']:.1f} OOS={ensemble['oos_sharpe']:.1f} ({len(ensemble['members'])} members)")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return accepted_count
|
||||
|
||||
|
||||
def main():
|
||||
print(f"\n{'='*50}")
|
||||
print(f" NexQuant Auto-Pilot")
|
||||
print(f" Log: {LOG_FILE}")
|
||||
print(f" Batch: {BATCH_SIZE} | Optuna: {OPTUNA_TRIALS} trials")
|
||||
print(f"{'='*50}\n")
|
||||
|
||||
round_num = 0
|
||||
total_accepted = 0
|
||||
consecutive_fails = 0
|
||||
start_time = datetime.now()
|
||||
styles = ["swing", "daytrading"]
|
||||
|
||||
while True:
|
||||
round_num += 1
|
||||
style = styles[round_num % 2]
|
||||
print(f"\n[Round {round_num}] {style} | {datetime.now().strftime('%H:%M:%S')}", flush=True)
|
||||
|
||||
try:
|
||||
accepted = main_round(style, round_num)
|
||||
total_accepted += accepted
|
||||
|
||||
if accepted == 0:
|
||||
consecutive_fails += 1
|
||||
else:
|
||||
consecutive_fails = 0
|
||||
|
||||
elapsed = (datetime.now() - start_time).total_seconds()
|
||||
rate = total_accepted / (elapsed / 3600) if elapsed > 0 else 0
|
||||
print(f" Accepted: {accepted} | Total: {total_accepted} | Rate: {rate:.1f}/h | Fails: {consecutive_fails}", flush=True)
|
||||
|
||||
if consecutive_fails >= MAX_CONSECUTIVE_FAILS:
|
||||
logger.warning(f"{consecutive_fails} consecutive failures — cooling down {COOLDOWN*2}s")
|
||||
time.sleep(COOLDOWN * 2)
|
||||
consecutive_fails = 0
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n\nStopped after {round_num} rounds. Total accepted: {total_accepted}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Round {round_num} crashed: {e}\n{traceback.format_exc()[-500:]}")
|
||||
consecutive_fails += 1
|
||||
time.sleep(COOLDOWN)
|
||||
|
||||
time.sleep(COOLDOWN)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,14 +1,14 @@
|
||||
"""
|
||||
Predix Batch Backtest Script - Extract and backtest existing factors.
|
||||
NexQuant Batch Backtest Script - Extract and backtest existing factors.
|
||||
|
||||
Scans generated factor code from workspaces, runs Qlib backtests directly
|
||||
(bypassing CoSTEER), and saves results to JSON + SQLite.
|
||||
|
||||
Usage:
|
||||
python predix_batch_backtest.py --factors 100 # Backtest top 100 factors
|
||||
python predix_batch_backtest.py --all # Backtest all discovered factors
|
||||
python predix_batch_backtest.py --parallel 5 # 5 parallel backtests
|
||||
python predix_batch_backtest.py --scan-only # Only scan, don't run backtests
|
||||
python nexquant_batch_backtest.py --factors 100 # Backtest top 100 factors
|
||||
python nexquant_batch_backtest.py --all # Backtest all discovered factors
|
||||
python nexquant_batch_backtest.py --parallel 5 # 5 parallel backtests
|
||||
python nexquant_batch_backtest.py --scan-only # Only scan, don't run backtests
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -660,7 +660,7 @@ def _run_factor_directly(factor_info: FactorInfo) -> Optional[BacktestResult]:
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="predix_factor_") as tmp_dir:
|
||||
with tempfile.TemporaryDirectory(prefix="nexquant_factor_") as tmp_dir:
|
||||
ws = Path(tmp_dir)
|
||||
|
||||
# Write factor code
|
||||
@@ -742,7 +742,7 @@ def _run_qlib_single(factor_info: FactorInfo) -> BacktestResult:
|
||||
import tempfile
|
||||
|
||||
# Create temp workspace
|
||||
with tempfile.TemporaryDirectory(prefix="predix_bt_") as tmp_dir:
|
||||
with tempfile.TemporaryDirectory(prefix="nexquant_bt_") as tmp_dir:
|
||||
ws = Path(tmp_dir)
|
||||
|
||||
# Write factor code
|
||||
@@ -1182,7 +1182,7 @@ def main(
|
||||
Metric for ranking ('ic' or 'sharpe')
|
||||
"""
|
||||
console.print(Panel(
|
||||
"[bold cyan]Predix Batch Backtest Runner[/bold cyan]\n"
|
||||
"[bold cyan]NexQuant Batch Backtest Runner[/bold cyan]\n"
|
||||
f"Scanning workspaces for generated factors...",
|
||||
border_style="cyan",
|
||||
))
|
||||
@@ -1196,7 +1196,7 @@ def main(
|
||||
if not all_factors_list:
|
||||
console.print("\n[red]No factors found in workspaces![/red]")
|
||||
console.print(
|
||||
"[yellow]Ensure factors have been generated via `predix.py quant` first.[/yellow]"
|
||||
"[yellow]Ensure factors have been generated via `nexquant.py quant` first.[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1407,7 +1407,7 @@ if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Predix Batch Backtest - Extract and backtest existing factors"
|
||||
description="NexQuant Batch Backtest - Extract and backtest existing factors"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--factors", "-n",
|
||||
@@ -12,9 +12,9 @@ Features:
|
||||
- Daytrading AND swing style alternating
|
||||
|
||||
Usage:
|
||||
python scripts/predix_continuous_strategies.py
|
||||
python scripts/predix_continuous_strategies.py --style daytrading --rounds 100
|
||||
python scripts/predix_continuous_strategies.py --style both --workers 4
|
||||
python scripts/nexquant_continuous_strategies.py
|
||||
python scripts/nexquant_continuous_strategies.py --style daytrading --rounds 100
|
||||
python scripts/nexquant_continuous_strategies.py --style both --workers 4
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -105,7 +105,7 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" Predix Continuous Strategy Generator")
|
||||
print(f" NexQuant Continuous Strategy Generator")
|
||||
print(f" Style: {args.style} | Workers: {args.workers}")
|
||||
print(f" Min Sharpe: {args.min_sharpe} | Batch: {args.batch_size}")
|
||||
print(f" ML every {args.ml_rounds} rounds")
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python
|
||||
"""Fast rebacktest: only strategies with factor parquets, skip already-done."""
|
||||
import json, sys, pandas as pd, subprocess, tempfile, numpy as np
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
||||
|
||||
OHLCV = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors/values")
|
||||
STRAT_DIR = Path("results/strategies_new")
|
||||
|
||||
# Pre-build factor name → path map
|
||||
fmap = {p.stem: str(p) for p in FACTORS_DIR.glob("*.parquet")}
|
||||
|
||||
# Load close once
|
||||
print("Loading OHLCV...")
|
||||
ohlcv = pd.read_hdf(str(OHLCV), key="data")
|
||||
close = ohlcv["$close"].dropna()
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close = close.droplevel(-1)
|
||||
close = close.astype(float).sort_index()
|
||||
print(f"{len(close):,} bars")
|
||||
|
||||
# Build work list
|
||||
work = []
|
||||
for f in sorted(STRAT_DIR.glob("*.json")):
|
||||
try:
|
||||
d = json.loads(f.read_text())
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("reevaluation_status") == "verified_v2":
|
||||
continue
|
||||
names = d.get("factor_names", [])
|
||||
code = d.get("code", "")
|
||||
if not names or not code:
|
||||
continue
|
||||
paths = []
|
||||
for n in names:
|
||||
p = fmap.get(n) or fmap.get(n.replace("/", "_")[:150])
|
||||
if p:
|
||||
paths.append((n, p))
|
||||
if len(paths) >= 2:
|
||||
work.append((f, d, paths))
|
||||
|
||||
print(f"{len(work)} strategies to process")
|
||||
|
||||
if not work:
|
||||
print("All done!")
|
||||
sys.exit(0)
|
||||
|
||||
ok = skip = fail = 0
|
||||
start = datetime.now()
|
||||
|
||||
for i, (f, data, factor_paths) in enumerate(work):
|
||||
name = data.get("strategy_name", f.stem)[:45]
|
||||
code = data.get("code", "")
|
||||
|
||||
# Load factor series
|
||||
series = {}
|
||||
for fn, fp in factor_paths:
|
||||
try:
|
||||
s = pd.read_parquet(fp).iloc[:, 0]
|
||||
series[fn] = s
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(series) < 2:
|
||||
skip += 1
|
||||
continue
|
||||
|
||||
df = pd.DataFrame(series).sort_index()
|
||||
if isinstance(df.index, pd.MultiIndex):
|
||||
df = df.droplevel(-1)
|
||||
|
||||
try:
|
||||
df_1m = df.reindex(close.index).ffill()
|
||||
except Exception:
|
||||
skip += 1
|
||||
continue
|
||||
|
||||
valid = df_1m.notna().any(axis=1)
|
||||
if valid.sum() < 1000:
|
||||
skip += 1
|
||||
continue
|
||||
|
||||
ca = close.loc[valid]
|
||||
fa = df_1m.loc[valid]
|
||||
|
||||
# Execute strategy code
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
tdp = Path(td)
|
||||
fa.to_parquet(str(tdp / "factors.parquet"))
|
||||
ca.to_pickle(str(tdp / "close.pkl"))
|
||||
|
||||
exec_script = (
|
||||
"import pandas as pd, numpy as np\n"
|
||||
"factors = pd.read_parquet('factors.parquet')\n"
|
||||
"close = pd.read_pickle('close.pkl')\n"
|
||||
"df = factors\n"
|
||||
+ code +
|
||||
"\nif 'signal' not in dir():\n"
|
||||
" raise SystemExit(1)\n"
|
||||
"pd.Series(signal).fillna(0).to_pickle('signal.pkl')\n"
|
||||
)
|
||||
(tdp / "run.py").write_text(exec_script)
|
||||
r = subprocess.run(
|
||||
["python", "run.py"],
|
||||
capture_output=True, text=True, timeout=60, cwd=str(tdp),
|
||||
)
|
||||
if r.returncode != 0:
|
||||
fail += 1
|
||||
continue
|
||||
sig = pd.read_pickle(tdp / "signal.pkl")
|
||||
except Exception:
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
sig = sig.reindex(ca.index).ffill().fillna(0)
|
||||
result = backtest_signal(ca, sig, txn_cost_bps=2.14)
|
||||
except Exception:
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
# Write back
|
||||
data["reevaluation_status"] = "verified_v2"
|
||||
data["sharpe_ratio"] = result.get("sharpe")
|
||||
data["max_drawdown"] = result.get("max_drawdown")
|
||||
data["win_rate"] = result.get("win_rate")
|
||||
data["total_return"] = result.get("total_return")
|
||||
data["summary"] = {
|
||||
**data.get("summary", {}),
|
||||
"sharpe": result.get("sharpe"),
|
||||
"max_drawdown": result.get("max_drawdown"),
|
||||
"win_rate": result.get("win_rate"),
|
||||
"monthly_return_pct": result.get("monthly_return_pct"),
|
||||
"real_n_trades": result.get("n_trades"),
|
||||
"total_return": result.get("total_return"),
|
||||
"annualized_return": result.get("annualized_return"),
|
||||
"engine": "verified_v2",
|
||||
"txn_cost_bps": 2.14,
|
||||
}
|
||||
f.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
ok += 1
|
||||
|
||||
elapsed = (datetime.now() - start).total_seconds()
|
||||
rate = ok / elapsed * 60 if elapsed > 0 else 0
|
||||
print(f" [{ok:4d}/{len(work)}] {rate:5.0f}/min {name:45s} "
|
||||
f"S={result['sharpe']:6.1f} DD={result['max_drawdown']:7.2%} "
|
||||
f"WR={result['win_rate']:5.1%} T={result['n_trades']:4d}")
|
||||
|
||||
elapsed = (datetime.now() - start).total_seconds()
|
||||
print(f"\nDONE: ok={ok} skip={skip} fail={fail} in {elapsed:.0f}s")
|
||||
@@ -1,13 +1,13 @@
|
||||
"""
|
||||
Predix Full Data Factor Evaluator - Evaluate factors with FULL 1min data.
|
||||
NexQuant Full Data Factor Evaluator - Evaluate factors with FULL 1min data.
|
||||
|
||||
Evaluates factors using the complete intraday_pv.h5 dataset (2022-2026, ~2.26M rows)
|
||||
instead of the debug dataset (2024 only, ~371K rows).
|
||||
|
||||
Usage:
|
||||
python predix_full_eval.py --top 100 # Evaluate top 100 factors with full data
|
||||
python predix_full_eval.py --all # Evaluate all factors
|
||||
python predix_full_eval.py --parallel 4 # 4 parallel workers
|
||||
python nexquant_full_eval.py --top 100 # Evaluate top 100 factors with full data
|
||||
python nexquant_full_eval.py --all # Evaluate all factors
|
||||
python nexquant_full_eval.py --parallel 4 # 4 parallel workers
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -271,7 +271,7 @@ def evaluate_factor_full(factor: FactorInfo, full_data: pd.DataFrame,
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="predix_full_") as tmp_dir:
|
||||
with tempfile.TemporaryDirectory(prefix="nexquant_full_") as tmp_dir:
|
||||
ws = Path(tmp_dir)
|
||||
|
||||
try:
|
||||
@@ -628,7 +628,7 @@ def main(
|
||||
) -> None:
|
||||
"""Main entry point."""
|
||||
console.print(Panel(
|
||||
"[bold cyan]Predix Full Data Factor Evaluator[/bold cyan]\n"
|
||||
"[bold cyan]NexQuant Full Data Factor Evaluator[/bold cyan]\n"
|
||||
f"Using FULL 1min data: {FULL_DATA_FILE}",
|
||||
border_style="cyan",
|
||||
))
|
||||
@@ -679,7 +679,7 @@ if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Predix Full Data Factor Evaluator"
|
||||
description="NexQuant Full Data Factor Evaluator"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--top", "-n",
|
||||
+7
-7
@@ -7,13 +7,13 @@ each with real backtesting on OHLCV data.
|
||||
|
||||
Usage:
|
||||
# Swing trading (96-bar forward returns)
|
||||
python predix_gen_strategies_real_bt.py 10
|
||||
python nexquant_gen_strategies_real_bt.py 10
|
||||
|
||||
# Daytrading with FTMO constraints (12-bar forward returns)
|
||||
TRADING_STYLE=daytrading python predix_gen_strategies_real_bt.py 5
|
||||
TRADING_STYLE=daytrading python nexquant_gen_strategies_real_bt.py 5
|
||||
|
||||
# With parallel workers (default: CPU count)
|
||||
TRADING_STYLE=daytrading WORKERS=4 python predix_gen_strategies_real_bt.py 20
|
||||
TRADING_STYLE=daytrading WORKERS=4 python nexquant_gen_strategies_real_bt.py 20
|
||||
"""
|
||||
import os, sys, json, time, math, random, logging, warnings, subprocess
|
||||
from pathlib import Path
|
||||
@@ -42,9 +42,9 @@ except Exception:
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
OHLCV_PATH = Path('/home/nico/Predix/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
FACTORS_DIR = Path('/home/nico/Predix/results/factors')
|
||||
STRATEGIES_DIR = Path('/home/nico/Predix/results/strategies_new')
|
||||
OHLCV_PATH = Path('/home/nico/NexQuant/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
FACTORS_DIR = Path('/home/nico/NexQuant/results/factors')
|
||||
STRATEGIES_DIR = Path('/home/nico/NexQuant/results/strategies_new')
|
||||
STRATEGIES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Trading style
|
||||
@@ -632,7 +632,7 @@ def main(target_count=10):
|
||||
|
||||
# Generate PDF report
|
||||
try:
|
||||
from predix_strategy_report import StrategyPerformanceReporter
|
||||
from nexquant_strategy_report import StrategyPerformanceReporter
|
||||
reporter = StrategyPerformanceReporter(strategy)
|
||||
reporter.generate_report()
|
||||
except:
|
||||
@@ -1,16 +1,16 @@
|
||||
"""
|
||||
Predix Parallel Runner - Run multiple factor experiments concurrently.
|
||||
NexQuant Parallel Runner - Run multiple factor experiments concurrently.
|
||||
|
||||
Spawns N subprocesses, each running `predix.py quant` with isolated config:
|
||||
Spawns N subprocesses, each running `nexquant.py quant` with isolated config:
|
||||
- Separate log files (fin_quant_run1.log, fin_quant_run2.log, etc.)
|
||||
- Separate result directories (results/runs/run1/, results/runs/run2/, etc.)
|
||||
- Separate workspace directories
|
||||
- API key distribution across multiple keys (round-robin)
|
||||
|
||||
Usage:
|
||||
python predix_parallel.py --runs 5 --api-keys 2
|
||||
python predix_parallel.py --runs 3 --model openrouter
|
||||
python predix_parallel.py --runs 5 --model local --api-keys 1
|
||||
python nexquant_parallel.py --runs 5 --api-keys 2
|
||||
python nexquant_parallel.py --runs 3 --model openrouter
|
||||
python nexquant_parallel.py --runs 5 --model local --api-keys 1
|
||||
"""
|
||||
import os
|
||||
import signal
|
||||
@@ -188,7 +188,7 @@ class ParallelRunner:
|
||||
|
||||
def _build_command(self, run_state: RunState) -> list[str]:
|
||||
"""
|
||||
Build the subprocess command to run predix quant.
|
||||
Build the subprocess command to run nexquant quant.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -202,7 +202,7 @@ class ParallelRunner:
|
||||
"""
|
||||
cmd = [
|
||||
sys.executable, # Use same Python interpreter
|
||||
str(self.project_root / "predix.py"),
|
||||
str(self.project_root / "nexquant.py"),
|
||||
"quant",
|
||||
"--model", run_state.model,
|
||||
"--run-id", str(run_state.run_id),
|
||||
@@ -327,7 +327,7 @@ class ParallelRunner:
|
||||
|
||||
# Build summary table
|
||||
table = Table(
|
||||
title="🔀 Predix Parallel Run Dashboard",
|
||||
title="🔀 NexQuant Parallel Run Dashboard",
|
||||
show_header=True,
|
||||
header_style="bold cyan",
|
||||
expand=True,
|
||||
@@ -399,7 +399,7 @@ class ParallelRunner:
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
|
||||
console.print(f"\n[bold cyan]{'=' * 60}[/bold cyan]")
|
||||
console.print("[bold cyan]🔀 Predix Parallel Runner[/bold cyan]")
|
||||
console.print("[bold cyan]🔀 NexQuant Parallel Runner[/bold cyan]")
|
||||
console.print(f"[bold cyan]{'=' * 60}[/bold cyan]")
|
||||
console.print(f" Runs: {self.num_runs}")
|
||||
console.print(f" API Keys: {self.num_api_keys} ({len(self.api_keys)} available)")
|
||||
@@ -503,7 +503,7 @@ if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Predix Parallel Runner - Run multiple factor experiments concurrently",
|
||||
description="NexQuant Parallel Runner - Run multiple factor experiments concurrently",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--runs", "-n",
|
||||
@@ -0,0 +1,467 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Quick Daytrading Strategy Generator with CORRECT factor alignment.
|
||||
|
||||
Uses forward-fill to align daily factors to 1-min frequency,
|
||||
then runs fast backtests without LLM calls.
|
||||
|
||||
Usage:
|
||||
python nexquant_quick_daytrading.py 5
|
||||
python nexquant_quick_daytrading.py 10
|
||||
"""
|
||||
import json, time, subprocess, tempfile # nosec
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
STRATEGIES_DIR = Path('results/strategies_new')
|
||||
STRATEGIES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
FACTOR_FILES = Path('results/factors')
|
||||
VALUE_FILES = FACTOR_FILES / 'values'
|
||||
OHLCV_PATH = Path('git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
|
||||
# Best daytrading strategies (12-min horizon, optimized for FTMO)
|
||||
DAYTRADING_COMBOS = [
|
||||
{
|
||||
'name': 'MomentumDivergence12min',
|
||||
'factors': ['daily_close_return_96', 'daily_session_momentum_divergence_1d'],
|
||||
'code': '''mom = factors['daily_close_return_96']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
|
||||
w = 20
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (mom_z - div_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.3] = 1
|
||||
signal[composite < -0.3] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'LondonSessionScalp',
|
||||
'factors': ['london_mom', 'daily_session_momentum_divergence_1d'],
|
||||
'code': '''mom = factors['london_mom']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
|
||||
w = 15
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (mom_z - div_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.25] = 1
|
||||
signal[composite < -0.25] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'TrendReversionScalp',
|
||||
'factors': ['daily_ols_slope_96', 'daily_session_momentum_divergence_1d', 'DailyTrendStrength_Raw'],
|
||||
'code': '''slope = factors['daily_ols_slope_96']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
trend = factors['DailyTrendStrength_Raw']
|
||||
|
||||
w = 20
|
||||
slope_z = (slope - slope.rolling(w).mean()) / (slope.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
trend_z = (trend - trend.rolling(w).mean()) / (trend.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.5 * slope_z - 0.3 * div_z + 0.2 * trend_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.3] = 1
|
||||
signal[composite < -0.3] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'VolAdjMomentum12',
|
||||
'factors': ['daily_ret_vol_adj_1d', 'daily_session_momentum_divergence_1d', 'DCP'],
|
||||
'code': '''vol = factors['daily_ret_vol_adj_1d']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
dcp = factors['DCP']
|
||||
|
||||
w = 20
|
||||
vol_z = (vol - vol.rolling(w).mean()) / (vol.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
dcp_z = (dcp - dcp.rolling(w).mean()) / (dcp.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.5 * vol_z - 0.3 * div_z + 0.2 * dcp_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.35] = 1
|
||||
signal[composite < -0.35] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'SessionMeanReversion',
|
||||
'factors': ['session_momentum_diff', 'daily_norm_body', 'daily_c2c_return'],
|
||||
'code': '''session = factors['session_momentum_diff']
|
||||
body = factors['daily_norm_body']
|
||||
c2c = factors['daily_c2c_return']
|
||||
|
||||
w = 15
|
||||
sess_z = (session - session.rolling(w).mean()) / (session.rolling(w).std() + 1e-8)
|
||||
body_z = (body - body.rolling(w).mean()) / (body.rolling(w).std() + 1e-8)
|
||||
c2c_z = (c2c - c2c.rolling(w).mean()) / (c2c.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.5 * sess_z + 0.3 * body_z + 0.2 * c2c_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.4] = 1
|
||||
signal[composite < -0.4] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'MomentumContinuation',
|
||||
'factors': ['daily_mom', 'daily_ret_1d', 'momentum_1d'],
|
||||
'code': '''mom = factors['daily_mom']
|
||||
ret = factors['daily_ret_1d']
|
||||
mom2 = factors['momentum_1d']
|
||||
|
||||
w = 12
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
ret_z = (ret - ret.rolling(w).mean()) / (ret.rolling(w).std() + 1e-8)
|
||||
mom2_z = (mom2 - mom2.rolling(w).mean()) / (mom2.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.4 * mom_z + 0.3 * ret_z + 0.3 * mom2_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.2] = 1
|
||||
signal[composite < -0.2] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'HighFreqScalper',
|
||||
'factors': ['daily_close_return_96', 'DCP', 'london_mom'],
|
||||
'code': '''close_ret = factors['daily_close_return_96']
|
||||
dcp = factors['DCP']
|
||||
london = factors['london_mom']
|
||||
|
||||
w = 10
|
||||
cr_z = (close_ret - close_ret.rolling(w).mean()) / (close_ret.rolling(w).std() + 1e-8)
|
||||
dcp_z = (dcp - dcp.rolling(w).mean()) / (dcp.rolling(w).std() + 1e-8)
|
||||
lon_z = (london - london.rolling(w).mean()) / (london.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.4 * cr_z + 0.3 * dcp_z + 0.3 * lon_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.25] = 1
|
||||
signal[composite < -0.25] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'AdaptiveMomentumMR',
|
||||
'factors': ['daily_close_return_96', 'daily_session_momentum_divergence_1d', 'daily_ols_slope_96'],
|
||||
'code': '''mom = factors['daily_close_return_96']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
slope = factors['daily_ols_slope_96']
|
||||
|
||||
w = 20
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
slope_z = (slope - slope.rolling(w).mean()) / (slope.rolling(w).std() + 1e-8)
|
||||
|
||||
# Regime detection: high momentum = trend, low = mean reversion
|
||||
regime = (mom_z.abs() > 1.0).astype(float)
|
||||
composite = (regime * mom_z + (1 - regime) * (-div_z) + 0.3 * slope_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.4] = 1
|
||||
signal[composite < -0.4] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'TrendPullbackScalp',
|
||||
'factors': ['daily_close_return_96', 'daily_session_momentum_divergence_1d', 'daily_norm_body'],
|
||||
'code': '''mom = factors['daily_close_return_96']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
body = factors['daily_norm_body']
|
||||
|
||||
w = 15
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
body_z = (body - body.rolling(w).mean()) / (body.rolling(w).std() + 1e-8)
|
||||
|
||||
# Enter on pullbacks (divergence against trend)
|
||||
composite = (mom_z - 0.5 * div_z * mom_z.sign() + 0.2 * body_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.35] = 1
|
||||
signal[composite < -0.35] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'IntradayMomentumBlend',
|
||||
'factors': ['daily_close_return_96', 'london_mom', 'daily_session_momentum_divergence_1d', 'DCP'],
|
||||
'code': '''mom = factors['daily_close_return_96']
|
||||
lon = factors['london_mom']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
dcp = factors['DCP']
|
||||
|
||||
w = 20
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
lon_z = (lon - lon.rolling(w).mean()) / (lon.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
dcp_z = (dcp - dcp.rolling(w).mean()) / (dcp.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.3 * mom_z + 0.3 * lon_z - 0.2 * div_z + 0.2 * dcp_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.3] = 1
|
||||
signal[composite < -0.3] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
]
|
||||
|
||||
def load_factor_series(name):
|
||||
"""Load factor parquet and return as Series with correct index."""
|
||||
safe = name.replace('/','_').replace('\\','_')[:150]
|
||||
pf = VALUE_FILES / f"{safe}.parquet"
|
||||
if not pf.exists():
|
||||
return None
|
||||
|
||||
df = pd.read_parquet(str(pf))
|
||||
|
||||
# Extract EURUSD
|
||||
if df.index.names == ['datetime', 'instrument']:
|
||||
df_reset = df.reset_index()
|
||||
if 'instrument' in df_reset.columns:
|
||||
df_eur = df_reset[df_reset['instrument'] == 'EURUSD'].copy()
|
||||
df_eur = df_eur.set_index('datetime')
|
||||
series = df_eur.iloc[:, -1] # Last column is the factor value
|
||||
series.name = name
|
||||
return series
|
||||
|
||||
# If single index, just return first column
|
||||
series = df.iloc[:, 0]
|
||||
series.name = name
|
||||
return series
|
||||
|
||||
def main(n_strategies=5):
|
||||
console.print("[bold cyan]🎯 Daytrading Strategy Generator (Quick Mode)[/bold cyan]\n")
|
||||
console.print(" Style: 12-minute forward returns")
|
||||
console.print(" Target: FTMO compliant (IC>0.02, Sharpe>0.5, Trades>20, DD>-10%)\n")
|
||||
|
||||
# Load OHLCV data
|
||||
if not OHLCV_PATH.exists():
|
||||
console.print(f"[red]✗ OHLCV data not found: {OHLCV_PATH}[/red]")
|
||||
return
|
||||
|
||||
ohlcv = pd.read_hdf(str(OHLCV_PATH), key='data')
|
||||
|
||||
# Extract close prices with datetime-only index (not MultiIndex)
|
||||
if '$close' in ohlcv.columns:
|
||||
close = ohlcv['$close'].dropna()
|
||||
elif 'close' in ohlcv.columns:
|
||||
close = ohlcv['close'].dropna()
|
||||
else:
|
||||
close = ohlcv.select_dtypes(include=[np.number]).iloc[:, 0].dropna()
|
||||
|
||||
# Extract datetime from MultiIndex if present
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close_dt_idx = close.index.get_level_values('datetime')
|
||||
close_series = pd.Series(close.values, index=close_dt_idx, name='close')
|
||||
else:
|
||||
close_series = close
|
||||
|
||||
close_series = close_series.dropna()
|
||||
console.print(f"[green]✓[/green] Loaded {len(close_series):,} OHLCV bars")
|
||||
|
||||
# Load all factor series and align to close index
|
||||
all_factor_series = {}
|
||||
for combo in DAYTRADING_COMBOS:
|
||||
for factor_name in combo['factors']:
|
||||
if factor_name in all_factor_series:
|
||||
continue
|
||||
|
||||
series = load_factor_series(factor_name)
|
||||
if series is not None:
|
||||
# Forward fill to match close frequency
|
||||
series_ff = series.reindex(close_series.index).ffill()
|
||||
all_factor_series[factor_name] = series_ff
|
||||
|
||||
# Create factors DataFrame
|
||||
df_factors = pd.DataFrame(all_factor_series)
|
||||
df_factors = df_factors.dropna(how='all')
|
||||
|
||||
console.print(f"[green]✓[/green] Loaded {len(df_factors.columns)} factor series")
|
||||
console.print(f"[green]✓[/green] Aligned to {len(df_factors):,} bars\n")
|
||||
|
||||
accepted = []
|
||||
|
||||
for i, combo in enumerate(DAYTRADING_COMBOS[:n_strategies]):
|
||||
console.print(f"[{i+1}/{n_strategies}] Testing {combo['name']}...")
|
||||
|
||||
# Build factor dataframe
|
||||
valid_factors = [f for f in combo['factors'] if f in df_factors.columns]
|
||||
if len(valid_factors) < 2:
|
||||
console.print(f" ✗ Not enough valid factors")
|
||||
continue
|
||||
|
||||
strat_factors = df_factors[valid_factors].dropna()
|
||||
|
||||
if len(strat_factors) < 1000:
|
||||
console.print(f" ✗ Not enough data: {len(strat_factors)} bars")
|
||||
continue
|
||||
|
||||
# Build backtest script
|
||||
forward_bars = 12
|
||||
strategy_code = combo['code']
|
||||
|
||||
script = f"""
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import json
|
||||
|
||||
close = pd.read_pickle('close.pkl') # nosec
|
||||
factors = pd.read_pickle('factors.pkl') # nosec
|
||||
|
||||
# Execute strategy
|
||||
try:
|
||||
{chr(10).join(' ' + l for l in strategy_code.split(chr(10)))}
|
||||
except Exception as e:
|
||||
print(f"ERROR: {{e}}")
|
||||
exit(1)
|
||||
|
||||
if 'signal' not in dir():
|
||||
print("ERROR: No signal generated")
|
||||
exit(1)
|
||||
|
||||
signal = signal.fillna(0)
|
||||
|
||||
# Align
|
||||
common_idx = close.index.intersection(signal.index)
|
||||
close = close.loc[common_idx]
|
||||
signal = signal.loc[common_idx]
|
||||
|
||||
# Forward returns (12-min horizon for daytrading)
|
||||
FORWARD_BARS = {forward_bars}
|
||||
returns_fwd = close.pct_change(FORWARD_BARS).shift(-FORWARD_BARS)
|
||||
signal_aligned = signal.loc[returns_fwd.dropna().index]
|
||||
fwd_returns = returns_fwd.loc[signal_aligned.index]
|
||||
|
||||
if len(signal_aligned) < 100 or len(fwd_returns) < 100:
|
||||
print("ERROR: Not enough data")
|
||||
exit(1)
|
||||
|
||||
# Metrics
|
||||
ic = signal_aligned.corr(fwd_returns)
|
||||
strategy_returns = signal_aligned * fwd_returns
|
||||
sharpe = strategy_returns.mean() / strategy_returns.std() * np.sqrt(252 * 1440 / {forward_bars}) if strategy_returns.std() > 0 else 0
|
||||
|
||||
cum = (1 + strategy_returns).cumprod()
|
||||
running_max = cum.expanding().max()
|
||||
drawdown = (cum - running_max) / running_max.replace(0, np.nan)
|
||||
max_dd = drawdown.min() if len(drawdown) > 0 else 0
|
||||
|
||||
win_rate = (strategy_returns > 0).sum() / len(strategy_returns) if len(strategy_returns) > 0 else 0
|
||||
n_trades = int((signal_aligned != signal_aligned.shift(1)).sum())
|
||||
total_return = cum.iloc[-1] - 1
|
||||
n_bars = len(strategy_returns)
|
||||
n_months = n_bars / (252 * 1440 / {forward_bars} / 12) if n_bars > 0 else 1
|
||||
monthly_return = (1 + total_return) ** (1 / n_months) - 1 if n_months > 0 and (1 + total_return) > 0 else total_return
|
||||
|
||||
result = {{
|
||||
"status": "success",
|
||||
"sharpe": float(sharpe),
|
||||
"max_drawdown": float(max_dd) if not np.isnan(max_dd) else -0.20,
|
||||
"win_rate": float(win_rate),
|
||||
"ic": float(ic) if not np.isnan(ic) else 0,
|
||||
"n_trades": n_trades,
|
||||
"total_return": float(total_return),
|
||||
"monthly_return_pct": float(monthly_return * 100),
|
||||
"n_bars": int(n_bars),
|
||||
"n_months": float(n_months),
|
||||
"signal_long": int((signal_aligned == 1).sum()),
|
||||
"signal_short": int((signal_aligned == -1).sum()),
|
||||
"signal_neutral": int((signal_aligned == 0).sum()),
|
||||
}}
|
||||
|
||||
print(json.dumps(result))
|
||||
"""
|
||||
|
||||
# Run backtest
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
tdp = Path(td)
|
||||
strat_close = close_series.loc[strat_factors.index]
|
||||
strat_close.to_pickle(str(tdp / 'close.pkl')) # nosec
|
||||
strat_factors.to_pickle(str(tdp / 'factors.pkl')) # nosec
|
||||
|
||||
script_path = tdp / 'run.py'
|
||||
script_path.write_text(script)
|
||||
|
||||
try:
|
||||
result_proc = subprocess.run( # nosec B603
|
||||
[sys.executable, str(script_path)],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
cwd=str(tdp)
|
||||
)
|
||||
|
||||
if result_proc.returncode != 0:
|
||||
console.print(f" ✗ Failed: {result_proc.stderr[:200]}")
|
||||
continue
|
||||
|
||||
result = None
|
||||
for line in result_proc.stdout.strip().split('\n'):
|
||||
try:
|
||||
result = json.loads(line)
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not result or result.get('status') != 'success':
|
||||
console.print(f" ✗ Invalid result")
|
||||
continue
|
||||
|
||||
except subprocess.TimeoutExpired: # nosec
|
||||
console.print(f" ✗ Timeout")
|
||||
continue
|
||||
except Exception as e:
|
||||
console.print(f" ✗ Error: {e}")
|
||||
continue
|
||||
|
||||
ic = result.get('ic', 0)
|
||||
sharpe = result.get('sharpe', 0)
|
||||
trades = result.get('n_trades', 0)
|
||||
dd = result.get('max_drawdown', 0)
|
||||
|
||||
# FTMO criteria
|
||||
if abs(ic) > 0.02 and sharpe > 0.5 and trades > 20 and dd > -0.10:
|
||||
strategy = {
|
||||
'strategy_name': combo['name'],
|
||||
'factor_names': combo['factors'],
|
||||
'description': f"Daytrading strategy combining {', '.join(combo['factors'])}",
|
||||
'code': combo['code'],
|
||||
'real_backtest': result,
|
||||
'metrics': result,
|
||||
'summary': {
|
||||
'sharpe': sharpe,
|
||||
'max_drawdown': dd,
|
||||
'win_rate': result.get('win_rate', 0),
|
||||
'monthly_return_pct': result.get('monthly_return_pct', 0),
|
||||
'real_ic': ic,
|
||||
'real_n_trades': trades,
|
||||
'forward_bars': 12,
|
||||
'trading_style': 'daytrading',
|
||||
}
|
||||
}
|
||||
|
||||
fname = f"{int(time.time())}_{combo['name']}.json"
|
||||
with open(STRATEGIES_DIR / fname, 'w') as f:
|
||||
json.dump(strategy, f, indent=2, ensure_ascii=False)
|
||||
|
||||
accepted.append(strategy)
|
||||
console.print(f" ✓ [green]ACCEPT[/green]: IC={ic:.4f}, Sharpe={sharpe:.2f}, Trades={trades}, DD={dd:.1%}")
|
||||
else:
|
||||
console.print(f" ✗ [red]REJECT[/red]: IC={ic:.4f}, Sharpe={sharpe:.2f}, Trades={trades}, DD={dd:.1%}")
|
||||
|
||||
console.print(f"\n[bold green]✓ {len(accepted)}/{n_strategies} strategies accepted[/bold green]\n")
|
||||
|
||||
if accepted:
|
||||
console.print("[bold]Results:[/bold]")
|
||||
for s in accepted:
|
||||
bt = s['real_backtest']
|
||||
console.print(f" • {s['strategy_name']:30s} IC={bt['ic']:.4f} Sharpe={bt['sharpe']:.2f} "
|
||||
f"Monthly={bt['monthly_return_pct']:.2f}% Trades={bt['n_trades']}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
n = int(sys.argv[1]) if len(sys.argv) > 1 else 5
|
||||
main(n)
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python
|
||||
"""One strategy runner — standalone, called from parent script."""
|
||||
import json, sys, pandas as pd, subprocess, tempfile, numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python nexquant_rebacktest_one.py <strategy_json_path>")
|
||||
sys.exit(1)
|
||||
|
||||
strat_path = Path(sys.argv[1])
|
||||
data = json.loads(strat_path.read_text())
|
||||
|
||||
OHLCV = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors/values")
|
||||
|
||||
fmap = {p.stem: str(p) for p in FACTORS_DIR.glob("*.parquet")}
|
||||
|
||||
names = data.get("factor_names", [])
|
||||
code = data.get("code", "")
|
||||
name = data.get("strategy_name", strat_path.stem)
|
||||
|
||||
if not names or not code:
|
||||
print(json.dumps({"status": "skipped", "reason": "no factors/code"}))
|
||||
sys.exit(0)
|
||||
|
||||
# Load close
|
||||
ohlcv = pd.read_hdf(str(OHLCV), key="data")
|
||||
close = ohlcv["$close"].dropna()
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close = close.droplevel(-1)
|
||||
close = close.astype(float).sort_index()
|
||||
|
||||
# Load factors
|
||||
series = {}
|
||||
for fn in names:
|
||||
fp = fmap.get(fn) or fmap.get(fn.replace("/", "_")[:150])
|
||||
if fp:
|
||||
try:
|
||||
s = pd.read_parquet(fp).iloc[:, 0]
|
||||
series[fn] = s
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(series) < 2:
|
||||
print(json.dumps({"status": "skipped", "reason": f"only {len(series)} factors loaded"}))
|
||||
sys.exit(0)
|
||||
|
||||
df = pd.DataFrame(series).sort_index()
|
||||
if isinstance(df.index, pd.MultiIndex):
|
||||
df = df.droplevel(-1)
|
||||
|
||||
df_1m = df.reindex(close.index).ffill()
|
||||
valid = df_1m.notna().any(axis=1)
|
||||
if valid.sum() < 1000:
|
||||
print(json.dumps({"status": "skipped", "reason": f"only {valid.sum()} valid bars"}))
|
||||
sys.exit(0)
|
||||
|
||||
ca = close.loc[valid]
|
||||
fa = df_1m.loc[valid]
|
||||
|
||||
# Execute
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
tdp = Path(td)
|
||||
fa.to_parquet(str(tdp / "factors.parquet"))
|
||||
ca.to_pickle(str(tdp / "close.pkl"))
|
||||
exec_script = (
|
||||
"import sys, os\n"
|
||||
"sys.stdout = open(os.devnull, 'w')\n"
|
||||
"sys.stderr = open(os.devnull, 'w')\n"
|
||||
"import pandas as pd, numpy as np\n"
|
||||
"factors = pd.read_parquet('factors.parquet')\n"
|
||||
"close = pd.read_pickle('close.pkl')\n"
|
||||
"df = factors\n"
|
||||
+ code +
|
||||
"\nif 'signal' not in dir():\n"
|
||||
" raise SystemExit(1)\n"
|
||||
"pd.Series(signal).fillna(0).to_pickle('signal.pkl')\n"
|
||||
)
|
||||
(tdp / "run.py").write_text(exec_script)
|
||||
r = subprocess.run(
|
||||
["python", "run.py"],
|
||||
capture_output=True, text=True, timeout=60, cwd=str(tdp),
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
print(json.dumps({"status": "code_failed", "stderr": r.stderr[:500]}))
|
||||
sys.exit(1)
|
||||
sig = pd.read_pickle(tdp / "signal.pkl")
|
||||
except Exception as e:
|
||||
print(json.dumps({"status": "code_failed", "error": str(e)[:500]}))
|
||||
sys.exit(1)
|
||||
|
||||
sig = sig.reindex(ca.index).ffill().fillna(0)
|
||||
result = backtest_signal(ca, sig, txn_cost_bps=2.14)
|
||||
|
||||
# Return result as JSON
|
||||
output = {
|
||||
"status": "ok",
|
||||
"sharpe": result.get("sharpe"),
|
||||
"max_drawdown": result.get("max_drawdown"),
|
||||
"win_rate": result.get("win_rate"),
|
||||
"n_trades": result.get("n_trades"),
|
||||
"total_return": result.get("total_return"),
|
||||
"monthly_return_pct": result.get("monthly_return_pct"),
|
||||
"annualized_return": result.get("annualized_return"),
|
||||
}
|
||||
print(json.dumps(output))
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python
|
||||
"""Parent orchestrator: calls nexquant_rebacktest_one.py for each strategy."""
|
||||
import json, subprocess, sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
STRAT_DIR = Path("results/strategies_new")
|
||||
|
||||
# Build work list
|
||||
work = []
|
||||
for f in sorted(STRAT_DIR.glob("*.json")):
|
||||
if "verified_v2" in f.read_text():
|
||||
continue
|
||||
try:
|
||||
d = json.loads(f.read_text())
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("factor_names") and d.get("code"):
|
||||
work.append(f)
|
||||
|
||||
print(f"{len(work)} strategies to re-backtest", flush=True)
|
||||
|
||||
ok = skip = fail = 0
|
||||
start = datetime.now()
|
||||
|
||||
for i, f in enumerate(work):
|
||||
name = f.stem[:45]
|
||||
print(f"[{i+1}/{len(work)}] {name} ...", end=" ", flush=True)
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["timeout", "-s", "KILL", "90", "python", "scripts/nexquant_rebacktest_one.py", str(f)],
|
||||
capture_output=True, text=True, timeout=120,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
result = json.loads(r.stdout.strip() or "{}")
|
||||
except subprocess.TimeoutExpired:
|
||||
print("TIMEOUT", flush=True)
|
||||
fail += 1
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}", flush=True)
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
if result.get("status") == "ok":
|
||||
data = json.loads(f.read_text())
|
||||
data["reevaluation_status"] = "verified_v2"
|
||||
data["sharpe_ratio"] = result.get("sharpe")
|
||||
data["max_drawdown"] = result.get("max_drawdown")
|
||||
data["win_rate"] = result.get("win_rate")
|
||||
data["total_return"] = result.get("total_return")
|
||||
data["summary"] = {
|
||||
**data.get("summary", {}),
|
||||
"sharpe": result.get("sharpe"),
|
||||
"max_drawdown": result.get("max_drawdown"),
|
||||
"win_rate": result.get("win_rate"),
|
||||
"monthly_return_pct": result.get("monthly_return_pct"),
|
||||
"real_n_trades": result.get("n_trades"),
|
||||
"total_return": result.get("total_return"),
|
||||
"annualized_return": result.get("annualized_return"),
|
||||
"engine": "verified_v2",
|
||||
"txn_cost_bps": 2.14,
|
||||
}
|
||||
f.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
ok += 1
|
||||
print(f"S={result['sharpe']:.1f} DD={result['max_drawdown']:.2%} WR={result['win_rate']:.1%} T={result['n_trades']}", flush=True)
|
||||
elif result.get("status") == "skipped":
|
||||
skip += 1
|
||||
print(f"SKIP: {result.get('reason', '?')}", flush=True)
|
||||
else:
|
||||
fail += 1
|
||||
print(f"FAIL: {result.get('stderr', result.get('error', '?'))[:100]}", flush=True)
|
||||
|
||||
elapsed = (datetime.now() - start).total_seconds()
|
||||
print(f"\nDONE: ok={ok} skip={skip} fail={fail} in {elapsed:.0f}s", flush=True)
|
||||
@@ -116,8 +116,8 @@ except Exception as e:
|
||||
"n_short":int((sig==-1).sum()), "n_neutral":int((sig==0).sum())}
|
||||
|
||||
def main(count=None):
|
||||
sdir = Path('/home/nico/Predix/results/strategies')
|
||||
vdir = Path('/home/nico/Predix/results/factors/values')
|
||||
sdir = Path('/home/nico/NexQuant/results/strategies')
|
||||
vdir = Path('/home/nico/NexQuant/results/factors/values')
|
||||
|
||||
files = []
|
||||
for f in sorted(sdir.glob('*.json'), reverse=True):
|
||||
@@ -13,9 +13,9 @@ For every strategy JSON in results/strategies_new (or a user-supplied dir):
|
||||
Does NOT mutate the strategy JSON files — read-only comparison.
|
||||
|
||||
Usage:
|
||||
python scripts/predix_rebacktest_unified.py # all strategies
|
||||
python scripts/predix_rebacktest_unified.py 50 # first 50
|
||||
python scripts/predix_rebacktest_unified.py 50 --csv report.csv
|
||||
python scripts/nexquant_rebacktest_unified.py # all strategies
|
||||
python scripts/nexquant_rebacktest_unified.py 50 # first 50
|
||||
python scripts/nexquant_rebacktest_unified.py 50 --csv report.csv
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -38,9 +38,9 @@ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeEl
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo # noqa: E402
|
||||
|
||||
OHLCV_PATH = Path("/home/nico/Predix/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_VALUES_DIR = Path("/home/nico/Predix/results/factors/values")
|
||||
STRATEGIES_DIR = Path("/home/nico/Predix/results/strategies_new")
|
||||
OHLCV_PATH = Path("/home/nico/NexQuant/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_VALUES_DIR = Path("/home/nico/NexQuant/results/factors/values")
|
||||
STRATEGIES_DIR = Path("/home/nico/NexQuant/results/strategies_new")
|
||||
|
||||
# ── Logging setup: everything printed goes to log file + stdout ───────────────
|
||||
_LOG_DIR = Path(__file__).resolve().parent.parent / "git_ignore_folder" / "logs"
|
||||
@@ -1,13 +1,13 @@
|
||||
"""
|
||||
Predix Simple Factor Evaluator - Direct IC/Sharpe computation.
|
||||
NexQuant Simple Factor Evaluator - Direct IC/Sharpe computation.
|
||||
|
||||
Evaluates existing factor results by computing IC and Sharpe directly
|
||||
from factor values and forward returns, without Qlib infrastructure.
|
||||
|
||||
Usage:
|
||||
python predix_simple_eval.py --top 100 # Evaluate top 100 factors
|
||||
python predix_simple_eval.py --all # Evaluate all
|
||||
python predix_simple_eval.py --parallel 4 # 4 parallel workers
|
||||
python nexquant_simple_eval.py --top 100 # Evaluate top 100 factors
|
||||
python nexquant_simple_eval.py --all # Evaluate all
|
||||
python nexquant_simple_eval.py --parallel 4 # 4 parallel workers
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -421,7 +421,7 @@ def main(
|
||||
) -> None:
|
||||
"""Main entry point."""
|
||||
console.print(Panel(
|
||||
"[bold cyan]Predix Simple Factor Evaluator[/bold cyan]\n"
|
||||
"[bold cyan]NexQuant Simple Factor Evaluator[/bold cyan]\n"
|
||||
f"Scanning workspaces for generated factors...",
|
||||
border_style="cyan",
|
||||
))
|
||||
@@ -467,7 +467,7 @@ if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Predix Simple Factor Evaluator - Direct IC/Sharpe computation"
|
||||
description="NexQuant Simple Factor Evaluator - Direct IC/Sharpe computation"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--top", "-n",
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Strategy Performance Report Generator for Predix.
|
||||
Strategy Performance Report Generator for NexQuant.
|
||||
|
||||
Generates detailed PDF reports with charts for each accepted strategy.
|
||||
|
||||
@@ -11,8 +11,8 @@ Features:
|
||||
- Full metrics table and strategy code
|
||||
|
||||
Usage:
|
||||
python predix_strategy_report.py # All strategies
|
||||
python predix_strategy_report.py results/strategies_new/123.json # Single strategy
|
||||
python nexquant_strategy_report.py # All strategies
|
||||
python nexquant_strategy_report.py results/strategies_new/123.json # Single strategy
|
||||
"""
|
||||
import os, sys, json, warnings
|
||||
from pathlib import Path
|
||||
@@ -39,8 +39,8 @@ from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# Config
|
||||
OHLCV_PATH = Path('/home/nico/Predix/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
REPORTS_DIR = Path('/home/nico/Predix/results/strategy_reports')
|
||||
OHLCV_PATH = Path('/home/nico/NexQuant/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
REPORTS_DIR = Path('/home/nico/NexQuant/results/strategy_reports')
|
||||
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Colors
|
||||
@@ -226,7 +226,7 @@ class StrategyPerformanceReporter:
|
||||
|
||||
def _gen_pdf_report(self, pdf_path):
|
||||
doc = SimpleDocTemplate(str(pdf_path), pagesize=A4,
|
||||
title=f"Predix: {self.name}", author="Predix AI",
|
||||
title=f"NexQuant: {self.name}", author="NexQuant AI",
|
||||
leftMargin=2*cm, rightMargin=2*cm, topMargin=2*cm, bottomMargin=2*cm)
|
||||
styles = getSampleStyleSheet()
|
||||
styles.add(ParagraphStyle(name='PTitle', fontName='Helvetica-Bold', fontSize=22, leading=26, alignment=TA_CENTER, textColor=colors.HexColor('#1A237E')))
|
||||
@@ -324,7 +324,7 @@ def generate_report_for_strategy(path: str) -> dict:
|
||||
|
||||
|
||||
def generate_all_reports():
|
||||
d = Path('/home/nico/Predix/results/strategies_new')
|
||||
d = Path('/home/nico/NexQuant/results/strategies_new')
|
||||
if not d.exists(): print("No strategies."); return
|
||||
for jf in sorted(d.glob('*.json')):
|
||||
try:
|
||||
@@ -14,7 +14,7 @@ FTMO 100k rules enforced:
|
||||
Out-of-sample window: 2024-01-01 onwards (never seen during factor research).
|
||||
|
||||
Usage:
|
||||
conda activate predix
|
||||
conda activate nexquant
|
||||
python scripts/realistic_backtest_all.py
|
||||
python scripts/realistic_backtest_all.py --target-monthly 4.0 --min-trades 50
|
||||
python scripts/realistic_backtest_all.py --workers 8
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Run all Predix integration tests
|
||||
# Run all NexQuant integration tests
|
||||
# Usage:
|
||||
# ./scripts/run_all_tests.sh # Full test suite
|
||||
# ./scripts/run_all_tests.sh --quick # Skip slow tests
|
||||
@@ -12,7 +12,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
echo "========================================="
|
||||
echo "Predix Integration Test Suite"
|
||||
echo "NexQuant Integration Test Suite"
|
||||
echo "========================================="
|
||||
echo "Project: $PROJECT_ROOT"
|
||||
echo "Date: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
# Restarts automatically on crash, generates strategies continuously.
|
||||
# ============================================================================
|
||||
|
||||
SCRIPT_DIR="/home/nico/Predix"
|
||||
GENERATOR="python ${SCRIPT_DIR}/predix_smart_strategy_gen.py"
|
||||
SCRIPT_DIR="/home/nico/NexQuant"
|
||||
GENERATOR="python ${SCRIPT_DIR}/nexquant_smart_strategy_gen.py"
|
||||
TARGET_COUNT=3
|
||||
LOGFILE="${SCRIPT_DIR}/results/logs/generator_loop.log"
|
||||
PIDFILE="/tmp/predix_loop.pid"
|
||||
PIDFILE="/tmp/nexquant_loop.pid"
|
||||
|
||||
echo $$ > "$PIDFILE"
|
||||
mkdir -p "${SCRIPT_DIR}/results/logs"
|
||||
@@ -19,7 +19,7 @@ log() {
|
||||
|
||||
cleanup() {
|
||||
log "Received termination signal. Cleaning up..."
|
||||
pkill -f "predix_smart_strategy_gen.py" 2>/dev/null
|
||||
pkill -f "nexquant_smart_strategy_gen.py" 2>/dev/null
|
||||
rm -f "$PIDFILE"
|
||||
log "Cleanup complete. Exiting."
|
||||
exit 0
|
||||
@@ -53,7 +53,7 @@ while true; do
|
||||
log "📁 Existing strategies: ${STRAT_COUNT}"
|
||||
|
||||
# Kill any stale processes
|
||||
pkill -9 -f "predix_smart_strategy_gen.py" 2>/dev/null
|
||||
pkill -9 -f "nexquant_smart_strategy_gen.py" 2>/dev/null
|
||||
sleep 2
|
||||
|
||||
# Start generator
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
# Checks every 20min: is the generator running? If not, (re)start it.
|
||||
# ============================================================================
|
||||
|
||||
SCRIPT_DIR="/home/nico/Predix"
|
||||
GENERATOR="python ${SCRIPT_DIR}/predix_smart_strategy_gen.py"
|
||||
SCRIPT_DIR="/home/nico/NexQuant"
|
||||
GENERATOR="python ${SCRIPT_DIR}/nexquant_smart_strategy_gen.py"
|
||||
TARGET_COUNT=3
|
||||
LOGFILE="${SCRIPT_DIR}/results/logs/watchdog.log"
|
||||
LOCKFILE="/tmp/predix_generator.lock"
|
||||
LOCKFILE="/tmp/nexquant_generator.lock"
|
||||
MAX_ATTEMPTS=50 # Stop after this many attempts
|
||||
PIDFILE="/tmp/predix_generator_attempt.pid"
|
||||
PIDFILE="/tmp/nexquant_generator_attempt.pid"
|
||||
|
||||
mkdir -p "${SCRIPT_DIR}/results/logs"
|
||||
|
||||
@@ -51,7 +51,7 @@ check_progress() {
|
||||
|
||||
# Kill any existing generator processes
|
||||
cleanup() {
|
||||
pkill -9 -f "predix_smart_strategy_gen.py" 2>/dev/null
|
||||
pkill -9 -f "nexquant_smart_strategy_gen.py" 2>/dev/null
|
||||
rm -f "$LOCKFILE"
|
||||
log "Cleaned up old processes"
|
||||
}
|
||||
@@ -63,7 +63,7 @@ if [ "$(get_attempt_count)" -ge "$MAX_ATTEMPTS" ]; then
|
||||
fi
|
||||
|
||||
# Check if generator is running
|
||||
if pgrep -f "predix_smart_strategy_gen.py" > /dev/null 2>&1; then
|
||||
if pgrep -f "nexquant_smart_strategy_gen.py" > /dev/null 2>&1; then
|
||||
# Check if it's making progress
|
||||
if check_progress; then
|
||||
log "Generator is running and making progress. Exiting."
|
||||
|
||||
Reference in New Issue
Block a user