Spaces:
Sleeping
Sleeping
Delete pipeline
Browse files- pipeline/__init__.py +0 -0
- pipeline/exporter.py +0 -202
- pipeline/extractor.py +0 -153
- pipeline/julia_bridge.py +0 -212
- pipeline/pdf_processor.py +0 -88
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|