cyberkyne commited on
Commit
8028a17
·
verified ·
1 Parent(s): ef7f800

Delete pipeline

Browse files
pipeline/__init__.py DELETED
File without changes
pipeline/exporter.py DELETED
@@ -1,202 +0,0 @@
1
- """pipeline/exporter.py — Generate markdown, MT5 .set, Julia config strings."""
2
- import re, json
3
- from datetime import datetime
4
-
5
-
6
- def slugify(t):
7
- t = t.lower().strip()
8
- t = re.sub(r"[^\w\s-]","",t)
9
- t = re.sub(r"[\s_-]+","-",t)
10
- return t[:50]
11
-
12
-
13
- def strategy_md(rec):
14
- name = rec.get("name","Unknown")
15
- sources = rec.get("sources",[])
16
- layers = rec.get("layers",[])
17
- lines = [f"# {name}", "",
18
- f"> **Category:** {rec.get('category','')} ",
19
- f"> **Sources:** {', '.join(sources)[:80]} ",
20
- f"> **ID:** `{rec.get('canonical_id','')}`",
21
- "", "---", "", "## Description", "", rec.get("description",""), ""]
22
- for label, key in [("## Entry Rules","entry_rules"),("## Exit Rules","exit_rules"),("## Filters","filters")]:
23
- items = rec.get(key,[])
24
- if items:
25
- lines += [label, ""]
26
- for i,r in enumerate(items,1): lines.append(f"{i}. {r}")
27
- lines.append("")
28
- if rec.get("mathematical_basis"):
29
- lines += ["## Mathematical Basis", "", rec["mathematical_basis"], ""]
30
- if layers:
31
- lines += [f"## Variants ({len(layers)} sources)", ""]
32
- for i,l in enumerate(layers,1):
33
- d = l.get("data",{}); lines.append(f"### Variant {i} — {l.get('source_file','')}")
34
- if d.get("description"): lines.append(d["description"])
35
- lines.append("")
36
- lines += ["---","","*Generated by Quant Knowledge Extractor — Julia Engine*"]
37
- return "\n".join(lines)
38
-
39
-
40
- def formula_md(rec):
41
- lines = [f"# {rec.get('name','Unknown')}", "",
42
- f"> **Category:** {rec.get('category','')} ",
43
- f"> **Sources:** {', '.join(rec.get('sources',[]))[:80]}",
44
- "", "---", ""]
45
- if rec.get("latex"):
46
- lines += ["## Formula (LaTeX)", "", "$$", rec["latex"], "$$", ""]
47
- if rec.get("plain_text"):
48
- lines += ["## Plain Text", "", "```", rec["plain_text"], "```", ""]
49
- if rec.get("purpose"):
50
- lines += ["## Purpose", "", rec["purpose"], ""]
51
- if rec.get("variables"):
52
- lines += ["## Variables", "", "| Symbol | Description |", "|--------|-------------|"]
53
- for s,d in rec["variables"].items(): lines.append(f"| `{s}` | {d} |")
54
- lines.append("")
55
- lines += ["---","","*Generated by Quant Knowledge Extractor — Julia Engine*"]
56
- return "\n".join(lines)
57
-
58
-
59
- def backtest_report_md(result: dict, record: dict) -> str:
60
- name = result.get("strategy","Unknown")
61
- sym = result.get("symbol","")
62
- tf = result.get("timeframe","")
63
- viable = result.get("is_viable", False)
64
- status = "✅ VIABLE" if viable else "❌ NOT VIABLE"
65
- now = datetime.now().strftime("%Y-%m-%d %H:%M UTC")
66
- lines = [
67
- f"# {name}", f"## Backtest Report — {sym} {tf}", "",
68
- f"> **Status:** {status} ",
69
- f"> **Robustness:** {result.get('robustness',0):.0f}/100 ",
70
- f"> **Engine:** Julia 1.10 ",
71
- f"> **Generated:** {now}", "", "---", "", "## Viability", "",
72
- ]
73
- for r in result.get("reasons",[]): lines.append(f"- {r}")
74
- lines += [""]
75
- if result.get("optimal_params"):
76
- lines += ["## Optimal Parameters (Julia Walk-Forward)", "",
77
- "| Parameter | Value |", "|-----------|-------|"]
78
- for k,v in result["optimal_params"].items():
79
- lines.append(f"| `{k}` | `{v:.4g}` |")
80
- lines += [""]
81
- lines += [
82
- "## OOS Performance (Walk-Forward Aggregate)", "",
83
- "| Metric | Value |", "|--------|-------|",
84
- f"| Sharpe (mean±std) | `{result.get('oos_sharpe_mean',0):.3f} ± {result.get('oos_sharpe_std',0):.3f}` |",
85
- f"| Win Rate | `{result.get('oos_win_rate',0):.1f}%` |",
86
- f"| Max Drawdown (mean) | `{result.get('oos_max_dd',0):.2f}%` |",
87
- f"| Profit Factor | `{result.get('oos_pf_mean',0):.3f}` |",
88
- f"| Total OOS Trades | `{result.get('oos_trades',0)}` |",
89
- f"| WF Efficiency Ratio | `{result.get('wf_efficiency',0):.3f}` |",
90
- f"| Robustness Score | `{result.get('robustness',0):.0f}/100` |",
91
- ]
92
- if record.get("description"):
93
- lines += ["", "## Strategy Description", "", record["description"]]
94
- lines += ["", "---", "", "*Backtested using Julia 1.10 — BacktestEngine.jl + Optimizer.jl*"]
95
- return "\n".join(lines)
96
-
97
-
98
- def optimal_json(result: dict, record: dict) -> dict:
99
- return {
100
- "metadata": {
101
- "strategy": result.get("strategy"), "symbol": result.get("symbol"),
102
- "timeframe": result.get("timeframe"),
103
- "generated": datetime.now().isoformat(),
104
- "engine": "Julia 1.10",
105
- "is_viable": result.get("is_viable"), "robustness": result.get("robustness"),
106
- },
107
- "optimal_parameters": result.get("optimal_params",{}),
108
- "performance": {
109
- "oos_sharpe_mean": round(result.get("oos_sharpe_mean",0),4),
110
- "oos_sharpe_std": round(result.get("oos_sharpe_std",0),4),
111
- "oos_win_rate": round(result.get("oos_win_rate",0),2),
112
- "oos_max_dd_pct": round(result.get("oos_max_dd",0),2),
113
- "oos_pf_mean": round(result.get("oos_pf_mean",0),4),
114
- "oos_total_trades":result.get("oos_trades",0),
115
- "wf_efficiency": round(result.get("wf_efficiency",0),4),
116
- },
117
- "viability": {"is_viable": result.get("is_viable"), "reasons": result.get("reasons",[])},
118
- }
119
-
120
-
121
- def mt5_set(result: dict, record: dict) -> str:
122
- name = result.get("strategy","Unknown")
123
- sym = result.get("symbol",""); tf = result.get("timeframe","")
124
- params = result.get("optimal_params",{})
125
- lines = [
126
- f"; MT5 Strategy Tester Set File — Generated by Julia Engine",
127
- f"; Strategy: {name}", f"; Symbol: {sym} Timeframe: {tf}",
128
- f"; OOS Sharpe: {result.get('oos_sharpe_mean',0):.3f} Score: {result.get('robustness',0):.0f}/100",
129
- f"; Viable: {result.get('is_viable', False)}",
130
- f";", f"; 1. Open MT5 → View → Strategy Tester",
131
- f"; 2. Select Expert Advisor", f"; 3. Inputs → Open → select this file", "",
132
- ]
133
- for k, v in params.items():
134
- mt5k = "".join(w.capitalize() for w in k.replace("-","_").split("_"))
135
- try:
136
- fv = float(v); step = max(1.0, abs(fv)*0.1)
137
- lines.append(f"{mt5k}={fv:.4g}||{max(0,fv-step*3):.4g}||{step:.4g}||{fv+step*3:.4g}||1")
138
- except: lines.append(f"{mt5k}={v}")
139
- lines += ["","RiskPercent=1.0||0.5||0.5||3.0||1","LotSize=0.1||0.01||0.01||1.0||1"]
140
- return "\n".join(lines)
141
-
142
-
143
- def julia_config(result: dict) -> str:
144
- name = result.get("strategy","Unknown")
145
- sym = result.get("symbol",""); tf = result.get("timeframe","")
146
- struct = "".join(w.capitalize() for w in name.split()[:4])
147
- params = result.get("optimal_params",{})
148
- plines = "\n ".join(
149
- f'{k.lower().replace("-","_")} = {float(v):.6g}'
150
- for k,v in params.items()
151
- ) or "# no parameters"
152
- return f'''# Optimal Parameters — {name}
153
- # Engine: Julia 1.10 BacktestEngine.jl + Optimizer.jl
154
- # Symbol: {sym} Timeframe: {tf}
155
- # OOS Sharpe: {result.get("oos_sharpe_mean",0):.3f} Robustness: {result.get("robustness",0):.0f}/100
156
- # Viable: {result.get("is_viable",False)}
157
- # Generated: {datetime.now().strftime("%Y-%m-%d")}
158
-
159
- function optimal_params()::{struct}Params
160
- return {struct}Params(
161
- {plines}
162
- )
163
- end
164
-
165
- const BACKTEST_METADATA = (
166
- strategy = "{name}",
167
- symbol = "{sym}",
168
- timeframe = "{tf}",
169
- engine = "Julia 1.10",
170
- oos_sharpe = {result.get("oos_sharpe_mean",0):.4f},
171
- max_dd_pct = {result.get("oos_max_dd",0):.2f},
172
- win_rate = {result.get("oos_win_rate",0):.1f},
173
- wf_eff = {result.get("wf_efficiency",0):.4f},
174
- viable = {str(result.get("is_viable",False)).lower()},
175
- )
176
- '''
177
-
178
-
179
- def index_md(all_results: list) -> str:
180
- viable = sorted([r for r in all_results if r.get("is_viable")],
181
- key=lambda r: r.get("oos_sharpe_mean",0), reverse=True)
182
- not_v = [r for r in all_results if not r.get("is_viable")]
183
- now = datetime.now().strftime("%Y-%m-%d %H:%M UTC")
184
- lines = [
185
- "# Backtest Results Index — Julia Engine", "",
186
- f"> Generated: {now} Engine: Julia 1.10 ",
187
- f"> Total: {len(all_results)} Viable: {len(viable)}", "", "---", "",
188
- "## ✅ Viable Strategies (by OOS Sharpe)", "",
189
- "| # | Strategy | Symbol | TF | Sharpe | DD% | Win% | PF | Score |",
190
- "|---|----------|--------|----|--------|-----|------|----|-------|",
191
- ]
192
- for i,r in enumerate(viable,1):
193
- lines.append(
194
- f"| {i} | {r.get('strategy','')[:35]} | {r.get('symbol','')} | {r.get('timeframe','')} | "
195
- f"{r.get('oos_sharpe_mean',0):.2f} | {r.get('oos_max_dd',0):.1f} | "
196
- f"{r.get('oos_win_rate',0):.1f} | {r.get('oos_pf_mean',0):.2f} | {r.get('robustness',0):.0f} |")
197
- lines += ["", "## ❌ Not Viable", "", "| Strategy | Symbol | TF | Reason |", "|----------|--------|----|--------|"]
198
- for r in not_v[:30]:
199
- reasons = r.get("reasons",[])
200
- reason = reasons[0][:60] if reasons else ""
201
- lines.append(f"| {r.get('strategy','')[:35]} | {r.get('symbol','')} | {r.get('timeframe','')} | {reason} |")
202
- return "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pipeline/extractor.py DELETED
@@ -1,153 +0,0 @@
1
- """pipeline/extractor.py — Claude API extraction + 3-layer deduplication."""
2
- import json, time, hashlib
3
- from typing import Optional
4
- import numpy as np
5
- from sklearn.feature_extraction.text import TfidfVectorizer
6
- from sklearn.metrics.pairwise import cosine_similarity
7
- import anthropic
8
- from loguru import logger
9
- import utils.config as cfg
10
-
11
-
12
- class AIExtractor:
13
- MODEL = "claude-sonnet-4-20250514"
14
- def __init__(self):
15
- self.client = anthropic.Anthropic(api_key=cfg.ANTHROPIC_API_KEY)
16
- self.tokens_used = 0
17
-
18
- def extract(self, chunk) -> dict:
19
- if chunk.word_count < 20:
20
- return {"strategies":[],"formulas":[],"systems":[]}
21
- prompt = cfg.EXTRACTION_PROMPT.format(
22
- source_file=chunk.source_file, page_start=chunk.page_start,
23
- page_end=chunk.page_end, text=chunk.text)
24
- raw = self._call(prompt)
25
- if not raw: return {"strategies":[],"formulas":[],"systems":[]}
26
- return self._parse(raw, chunk)
27
-
28
- def _call(self, prompt, retries=3):
29
- delay = 2.0
30
- for attempt in range(retries):
31
- try:
32
- resp = self.client.messages.create(
33
- model=self.MODEL, max_tokens=4096,
34
- messages=[{"role":"user","content":prompt}])
35
- self.tokens_used += resp.usage.input_tokens + resp.usage.output_tokens
36
- return resp.content[0].text if resp.content else ""
37
- except anthropic.RateLimitError:
38
- logger.warning(f"Rate limit — {delay}s")
39
- time.sleep(delay); delay *= 2
40
- except Exception as e:
41
- logger.error(f"API: {e}")
42
- if attempt == retries-1: return ""
43
- time.sleep(delay); delay *= 2
44
- return ""
45
-
46
- def _parse(self, raw, chunk):
47
- raw = raw.strip()
48
- if raw.startswith("```"): raw = "\n".join(raw.split("\n")[1:]).rstrip("`").strip()
49
- try: data = json.loads(raw)
50
- except:
51
- try:
52
- s=raw.find("{"); e=raw.rfind("}")
53
- data=json.loads(raw[s:e+1]) if s!=-1 else {}
54
- except: return {"strategies":[],"formulas":[],"systems":[]}
55
- result = {}
56
- for kind in ("strategies","formulas","systems"):
57
- result[kind] = []
58
- for item in data.get(kind,[]):
59
- if isinstance(item,dict) and item.get("name"):
60
- item.update({"source_file":chunk.source_file,
61
- "source_pages":f"{chunk.page_start}-{chunk.page_end}"})
62
- item["content_hash"] = _hash(
63
- item.get("description","") + item.get("plain_text","") +
64
- item.get("entry_system","") + item.get("name",""))
65
- result[kind].append(item)
66
- return result
67
-
68
- def compile_strategy_code(self, record: dict) -> str:
69
- """Ask Claude to generate Julia signal code for this strategy."""
70
- compact = {k: record.get(k) for k in
71
- ("name","category","description","entry_rules","exit_rules",
72
- "filters","parameters","mathematical_basis")}
73
- prompt = cfg.COMPILER_PROMPT.format(
74
- strategy_json=json.dumps(compact, indent=2))
75
- code = self._call(prompt)
76
- if not code: return ""
77
- if "```" in code:
78
- lines = code.split("\n")
79
- in_block = False; out = []
80
- for line in lines:
81
- if line.strip().startswith("```"): in_block = not in_block; continue
82
- if in_block: out.append(line)
83
- code = "\n".join(out)
84
- return code.strip()
85
-
86
-
87
- class Deduplicator:
88
- def __init__(self, threshold=None):
89
- self.threshold = threshold or cfg.SIMILARITY_THRESHOLD
90
- self._vec = TfidfVectorizer(ngram_range=(1,2), max_features=5000, stop_words="english")
91
-
92
- def process(self, extracted, kb):
93
- stats = {k:{"added":0,"merged":0,"skipped":0} for k in ("strategies","formulas","systems")}
94
- for kind in ("strategies","formulas","systems"):
95
- for item in extracted.get(kind,[]):
96
- stats[kind][self._process_one(item, kb[kind], kind)] += 1
97
- return stats
98
-
99
- def _process_one(self, item, store, kind):
100
- h = item.get("content_hash","")
101
- for e in store.values():
102
- if e.get("content_hash") == h:
103
- self._add_src(item, e); return "skipped"
104
- sid = self._similar(item, store, kind)
105
- if sid: self._merge(item, store[sid]); return "merged"
106
- cid = _cid(item["name"], h, kind)
107
- item["canonical_id"] = cid
108
- item["sources"] = [item.get("source_file","")]
109
- item["layers"] = []
110
- store[cid] = item
111
- return "added"
112
-
113
- def _similar(self, item, store, kind):
114
- if not store: return None
115
- texts = [_text(v,kind) for v in store.values()] + [_text(item,kind)]
116
- try:
117
- mat = self._vec.fit_transform(texts)
118
- sims = cosine_similarity(mat[-1], mat[:-1])[0]
119
- idx = int(np.argmax(sims))
120
- if sims[idx] >= self.threshold:
121
- return list(store.keys())[idx]
122
- except: pass
123
- return None
124
-
125
- @staticmethod
126
- def _add_src(item, existing):
127
- s = item.get("source_file","")
128
- if s and s not in existing.get("sources",[]):
129
- existing.setdefault("sources",[]).append(s)
130
-
131
- @staticmethod
132
- def _merge(item, existing):
133
- Deduplicator._add_src(item, existing)
134
- layers = existing.setdefault("layers",[])
135
- if item.get("content_hash") not in {l.get("content_hash") for l in layers}:
136
- layers.append({"source_file":item.get("source_file"),
137
- "content_hash":item.get("content_hash"),
138
- "data":{k:v for k,v in item.items()
139
- if k not in ("sources","layers","canonical_id")}})
140
-
141
-
142
- def _hash(text):
143
- return hashlib.sha256(" ".join(text.lower().split()).encode()).hexdigest()[:16]
144
-
145
- def _cid(name, h, kind):
146
- return hashlib.md5(f"{kind}_{name}_{h}".encode()).hexdigest()[:12]
147
-
148
- def _text(item, kind):
149
- if kind=="strategies":
150
- return f"{item.get('name','')} {item.get('description','')} {' '.join(item.get('entry_rules',[]))}"
151
- if kind=="formulas":
152
- return f"{item.get('name','')} {item.get('plain_text','')} {item.get('purpose','')}"
153
- return f"{item.get('name','')} {item.get('entry_system','')} {item.get('exit_system','')}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pipeline/julia_bridge.py DELETED
@@ -1,212 +0,0 @@
1
- """
2
- pipeline/julia_bridge.py
3
- ─────────────────────────────────────────────────────
4
- Python bridge to Julia via juliacall.
5
-
6
- Julia is loaded ONCE on first use (lazy init) to avoid
7
- slowing down Space startup. Subsequent calls are instant.
8
-
9
- juliacall converts:
10
- Python list/np.ndarray → Julia Vector{Float64}
11
- Python dict → Julia Dict
12
- Julia Dict{String,Any} → Python dict
13
- Julia Vector → Python list
14
-
15
- All heavy computation — indicators, backtest, optimizer —
16
- runs in Julia. Python only calls this bridge.
17
- """
18
-
19
- from __future__ import annotations
20
- import os
21
- from pathlib import Path
22
- from typing import Optional
23
- import numpy as np
24
- from loguru import logger
25
-
26
- # ── Julia runtime (loaded once) ───────────────────────
27
- _jl = None
28
- _QE = None # QuantEngine module
29
-
30
- JULIA_SRC = Path(__file__).parent.parent / "src"
31
-
32
-
33
- def _init_julia():
34
- """Lazy-load Julia + QuantEngine on first call."""
35
- global _jl, _QE
36
- if _jl is not None:
37
- return
38
-
39
- logger.info("Initializing Julia runtime…")
40
- try:
41
- from juliacall import Main as jl
42
- _jl = jl
43
-
44
- # Load the QuantEngine module
45
- _jl.seval(f'push!(LOAD_PATH, "{JULIA_SRC}")')
46
- _jl.seval(f'include("{JULIA_SRC / "QuantEngine.jl"}")')
47
- _jl.seval("using .QuantEngine")
48
- _QE = _jl.QuantEngine
49
- logger.info("Julia QuantEngine loaded ✓")
50
- except Exception as e:
51
- logger.error(f"Julia init failed: {e}")
52
- raise RuntimeError(f"Julia init failed: {e}") from e
53
-
54
-
55
- def _arr(x) -> "jl.Vector":
56
- """Convert Python list/ndarray to Julia Vector{Float64}."""
57
- _init_julia()
58
- import numpy as np
59
- arr = np.asarray(x, dtype=np.float64)
60
- return _jl.convert(_jl.Vector[_jl.Float64], arr.tolist())
61
-
62
-
63
- def _signals(x) -> "jl.Vector":
64
- """Convert signal array to Julia Vector{Int}."""
65
- _init_julia()
66
- arr = [int(v) for v in x]
67
- return _jl.convert(_jl.Vector[_jl.Int], arr)
68
-
69
-
70
- def _pydict(jl_dict) -> dict:
71
- """Recursively convert Julia Dict to Python dict."""
72
- out = {}
73
- for k in jl_dict.keys():
74
- v = jl_dict[k]
75
- k_py = str(k)
76
- if hasattr(v, "keys"):
77
- out[k_py] = _pydict(v)
78
- elif hasattr(v, "__iter__") and not isinstance(v, str):
79
- out[k_py] = list(v)
80
- elif isinstance(v, bool):
81
- out[k_py] = bool(v)
82
- elif hasattr(v, "__float__"):
83
- out[k_py] = float(v)
84
- elif hasattr(v, "__int__"):
85
- out[k_py] = int(v)
86
- else:
87
- out[k_py] = v
88
- return out
89
-
90
-
91
- # ── Public API ────────────────────────────────────────
92
-
93
- def full_backtest_pipeline(
94
- strategy_code: str,
95
- strategy_name: str,
96
- open_p: list | np.ndarray,
97
- high: list | np.ndarray,
98
- low: list | np.ndarray,
99
- close: list | np.ndarray,
100
- volume: list | np.ndarray,
101
- timeframe: str,
102
- symbol: str,
103
- n_windows: int = 5,
104
- is_ratio: float = 0.70,
105
- min_trades: int = 30,
106
- min_sharpe: float = 0.5,
107
- max_combos: int = 300,
108
- initial_equity: float = 10_000.0,
109
- commission_pct: float = 0.0002,
110
- risk_per_trade: float = 0.01,
111
- ) -> dict:
112
- """
113
- Run full Julia backtest pipeline.
114
- Compile strategy code → walk-forward optimize → return results dict.
115
- """
116
- _init_julia()
117
- try:
118
- result = _QE.full_backtest_pipeline(
119
- strategy_code, strategy_name,
120
- _arr(open_p), _arr(high), _arr(low),
121
- _arr(close), _arr(volume),
122
- timeframe, symbol,
123
- n_windows = n_windows,
124
- is_ratio = is_ratio,
125
- min_trades = min_trades,
126
- min_sharpe = min_sharpe,
127
- max_combos = max_combos,
128
- initial_equity = initial_equity,
129
- commission_pct = commission_pct,
130
- risk_per_trade = risk_per_trade,
131
- )
132
- return _pydict(result)
133
- except Exception as e:
134
- logger.error(f"Julia pipeline error [{strategy_name}]: {e}")
135
- return {
136
- "is_valid": False,
137
- "error": str(e),
138
- "strategy": strategy_name,
139
- "symbol": symbol,
140
- "timeframe": timeframe,
141
- }
142
-
143
-
144
- def run_backtest_only(
145
- open_p: np.ndarray, high: np.ndarray, low: np.ndarray,
146
- close: np.ndarray, volume: np.ndarray,
147
- signals: list | np.ndarray,
148
- timeframe: str = "1h",
149
- initial_equity: float = 10_000.0,
150
- commission_pct: float = 0.0002,
151
- risk_per_trade: float = 0.01,
152
- ) -> dict:
153
- """Run a single backtest with pre-computed signals."""
154
- _init_julia()
155
- try:
156
- cfg = _QE.BacktestConfig(
157
- initial_equity=initial_equity,
158
- commission_pct=commission_pct,
159
- risk_per_trade=risk_per_trade,
160
- )
161
- r = _QE.run_backtest(
162
- _arr(open_p), _arr(high), _arr(low),
163
- _arr(close), _arr(volume),
164
- _signals(signals), timeframe, cfg,
165
- )
166
- return {
167
- "total_return": float(r.total_return),
168
- "cagr": float(r.cagr),
169
- "sharpe": float(r.sharpe),
170
- "sortino": float(r.sortino),
171
- "calmar": float(r.calmar),
172
- "max_dd": float(r.max_dd),
173
- "n_trades": int(r.n_trades),
174
- "win_rate": float(r.win_rate),
175
- "profit_factor": float(r.profit_factor),
176
- "final_equity": float(r.final_equity),
177
- "equity_curve": list(r.equity_curve),
178
- "is_valid": bool(r.is_valid),
179
- "error": str(r.error_msg),
180
- }
181
- except Exception as e:
182
- return {"is_valid": False, "error": str(e)}
183
-
184
-
185
- def indicator(name: str, *args, **kwargs):
186
- """
187
- Call any indicator by name from Python.
188
- Returns numpy array.
189
-
190
- Example:
191
- rsi_values = indicator("rsi", close_array, 14)
192
- upper, mid, lower = indicator("bbands", close_array, 20, 2.0)
193
- """
194
- _init_julia()
195
- fn = getattr(_QE, name, None)
196
- if fn is None:
197
- raise ValueError(f"Unknown indicator: {name}")
198
- jl_args = [_arr(a) if isinstance(a, (list, np.ndarray)) else a
199
- for a in args]
200
- result = fn(*jl_args, **kwargs)
201
- if isinstance(result, tuple):
202
- return tuple(np.array(list(r)) for r in result)
203
- return np.array(list(result))
204
-
205
-
206
- def julia_available() -> bool:
207
- """Check if Julia runtime is available."""
208
- try:
209
- _init_julia()
210
- return _QE is not None
211
- except Exception:
212
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pipeline/pdf_processor.py DELETED
@@ -1,88 +0,0 @@
1
- """pipeline/pdf_processor.py — PDF text extraction + OCR fallback."""
2
- import re
3
- from pathlib import Path
4
- from typing import Generator
5
- from dataclasses import dataclass
6
- import fitz, pdfplumber, pytesseract
7
- from pdf2image import convert_from_path
8
- from loguru import logger
9
- import utils.config as cfg
10
-
11
-
12
- @dataclass
13
- class Chunk:
14
- source_file: str
15
- page_start: int
16
- page_end: int
17
- text: str
18
- was_ocr: bool = False
19
-
20
- @property
21
- def word_count(self): return len(self.text.split())
22
-
23
-
24
- class PDFProcessor:
25
- MIN_CHARS = 80
26
- CHUNK_WORDS = cfg.MAX_TOKENS_PER_CHUNK // 2
27
-
28
- def process(self, pdf_path: Path) -> Generator[Chunk, None, None]:
29
- pages = self._extract_pages(pdf_path)
30
- yield from self._chunk(pages, pdf_path.name)
31
-
32
- def _extract_pages(self, path):
33
- mu = {}
34
- try:
35
- doc = fitz.open(str(path))
36
- for i, pg in enumerate(doc): mu[i+1] = self._clean(pg.get_text("text"))
37
- doc.close()
38
- except: pass
39
- pl = {}
40
- try:
41
- with pdfplumber.open(str(path)) as pdf:
42
- for i, pg in enumerate(pdf.pages):
43
- try: pl[i+1] = self._clean(pg.extract_text() or "")
44
- except: pl[i+1] = ""
45
- except: pass
46
- total = max(len(mu), len(pl), 1)
47
- results = []; ocr_needed = []
48
- for pnum in range(1, total+1):
49
- best = mu.get(pnum,"") if len(mu.get(pnum,"")) > len(pl.get(pnum,"")) else pl.get(pnum,"")
50
- if len(best) >= self.MIN_CHARS: results.append((pnum, best, False))
51
- else: results.append((pnum, best, False)); ocr_needed.append(pnum)
52
- if ocr_needed:
53
- ocr = self._ocr(path, ocr_needed)
54
- for i,(pnum,_,_) in enumerate(results):
55
- if pnum in ocr: results[i] = (pnum, ocr[pnum], True)
56
- return results
57
-
58
- def _ocr(self, path, pages):
59
- out = {}
60
- try:
61
- imgs = convert_from_path(str(path), dpi=cfg.OCR_DPI,
62
- first_page=min(pages), last_page=max(pages))
63
- for i, pnum in enumerate(range(min(pages), max(pages)+1)):
64
- if pnum in pages and i < len(imgs):
65
- out[pnum] = self._clean(pytesseract.image_to_string(imgs[i],lang="eng",config="--psm 6"))
66
- except Exception as e: logger.warning(f"OCR: {e}")
67
- return out
68
-
69
- def _chunk(self, pages, source):
70
- buf, words, p_start, any_ocr = [], 0, 1, False
71
- for pnum, text, ocr in pages:
72
- if not text: continue
73
- buf.append(text); words += len(text.split())
74
- if ocr: any_ocr = True
75
- if words >= self.CHUNK_WORDS:
76
- yield Chunk(source, p_start, pnum, "\n\n".join(buf), any_ocr)
77
- buf, words, p_start, any_ocr = [text], len(text.split()), pnum, ocr
78
- if buf:
79
- last = pages[-1][0] if pages else p_start
80
- yield Chunk(source, p_start, last, "\n\n".join(buf), any_ocr)
81
-
82
- @staticmethod
83
- def _clean(text):
84
- if not text: return ""
85
- text = re.sub(r"(\w)-\n(\w)", r"\1\2", text)
86
- text = re.sub(r"\n{3,}", "\n\n", text)
87
- text = re.sub(r"[ \t]+", " ", text)
88
- return text.strip()