feat: strategie 1-10, framework analisi frattale, download dati storici BTC/ETH

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 00:55:13 +02:00
parent 49ee092c7f
commit 988739b2f5
29 changed files with 3300 additions and 0 deletions
View File
+159
View File
@@ -0,0 +1,159 @@
"""Fractal indicators: Hurst exponent, fractal dimension, self-similarity."""
from __future__ import annotations
import numpy as np
from scipy.stats import linregress
def hurst_exponent(series: np.ndarray, max_lag: int | None = None) -> float:
"""Compute Hurst exponent via R/S analysis.
H > 0.5: trending (persistent), H < 0.5: mean-reverting, H ≈ 0.5: random walk.
"""
n = len(series)
if n < 20:
return 0.5
if max_lag is None:
max_lag = min(n // 4, 100)
lags = range(10, max_lag + 1)
rs_values = []
lag_values = []
for lag in lags:
rs_list = []
for start in range(0, n - lag, lag):
chunk = series[start : start + lag]
if len(chunk) < lag:
continue
mean = np.mean(chunk)
deviations = np.cumsum(chunk - mean)
r = np.max(deviations) - np.min(deviations)
s = np.std(chunk, ddof=1)
if s > 0:
rs_list.append(r / s)
if rs_list:
rs_values.append(np.mean(rs_list))
lag_values.append(lag)
if len(lag_values) < 3:
return 0.5
log_lags = np.log(lag_values)
log_rs = np.log(rs_values)
slope, _, _, _, _ = linregress(log_lags, log_rs)
return float(np.clip(slope, 0, 1))
def rolling_hurst(close: np.ndarray, window: int = 100, step: int = 1) -> np.ndarray:
"""Compute rolling Hurst exponent."""
n = len(close)
result = np.full(n, 0.5)
returns = np.diff(np.log(np.where(close == 0, 1e-10, close)))
for i in range(window, n, step):
h = hurst_exponent(returns[i - window : i])
result[i] = h
for j in range(1, min(step, n - i)):
result[i + j] = h
return result
def fractal_dimension_higuchi(series: np.ndarray, k_max: int = 10) -> float:
"""Higuchi fractal dimension of a time series."""
n = len(series)
if n < k_max * 2:
return 1.5
lk = []
x = np.arange(1, k_max + 1)
for k in range(1, k_max + 1):
lm_list = []
for m in range(1, k + 1):
indices = np.arange(m - 1, n, k)
if len(indices) < 2:
continue
vals = series[indices]
length = np.sum(np.abs(np.diff(vals)))
norm = (n - 1) / (k * ((n - m) // k) * k)
lm_list.append(length * norm)
if lm_list:
lk.append(np.mean(lm_list))
if len(lk) < 3:
return 1.5
log_k = np.log(1.0 / x[: len(lk)])
log_lk = np.log(np.array(lk))
slope, _, _, _, _ = linregress(log_k, log_lk)
return float(np.clip(slope, 1.0, 2.0))
def self_similarity_score(close: np.ndarray, window: int, scales: list[int] | None = None) -> float:
"""Measure self-similarity across multiple time scales.
Higher score = more fractal (self-similar) structure.
"""
if scales is None:
scales = [2, 3, 4, 6]
if len(close) < window:
return 0.0
base = close[-window:]
base_returns = np.diff(np.log(np.where(base == 0, 1e-10, base)))
if np.std(base_returns) == 0:
return 0.0
similarities = []
for scale in scales:
scaled_window = window * scale
if scaled_window > len(close):
continue
scaled = close[-scaled_window:]
step = scale
downsampled = scaled[::step][:window]
if len(downsampled) != len(base):
downsampled = np.interp(
np.linspace(0, 1, window),
np.linspace(0, 1, len(downsampled)),
downsampled,
)
ds_returns = np.diff(np.log(np.where(downsampled == 0, 1e-10, downsampled)))
if len(ds_returns) != len(base_returns):
ds_returns = np.interp(
np.linspace(0, 1, len(base_returns)),
np.linspace(0, 1, len(ds_returns)),
ds_returns,
)
std_ds = np.std(ds_returns)
if std_ds == 0:
continue
corr = np.corrcoef(base_returns, ds_returns)[0, 1]
if np.isfinite(corr):
similarities.append(abs(corr))
if not similarities:
return 0.0
return float(np.mean(similarities))
def volatility_ratio(close: np.ndarray, fast: int = 12, slow: int = 48) -> float:
"""Ratio of short-term to long-term volatility."""
returns = np.diff(np.log(np.where(close == 0, 1e-10, close)))
if len(returns) < slow:
return 1.0
fast_vol = np.std(returns[-fast:])
slow_vol = np.std(returns[-slow:])
if slow_vol == 0:
return 1.0
return float(fast_vol / slow_vol)
+121
View File
@@ -0,0 +1,121 @@
"""Fractal pattern detection and encoding for candlestick sequences."""
from __future__ import annotations
from dataclasses import dataclass
from enum import IntEnum
import numpy as np
import pandas as pd
class CandleType(IntEnum):
DOWN = -1
DOJI = 0
UP = 1
@dataclass
class FractalPattern:
sequence: tuple[CandleType, ...]
start_idx: int
end_idx: int
body_ratios: tuple[float, ...]
shadow_ratios: tuple[float, ...]
@property
def length(self) -> int:
return len(self.sequence)
@property
def code(self) -> str:
m = {CandleType.DOWN: "D", CandleType.DOJI: "0", CandleType.UP: "U"}
return "".join(m[c] for c in self.sequence)
def __hash__(self) -> int:
return hash(self.sequence)
def classify_candle(open_: float, close: float, high: float, low: float, doji_threshold: float = 0.1) -> CandleType:
body = abs(close - open_)
total_range = high - low
if total_range == 0:
return CandleType.DOJI
ratio = body / total_range
if ratio < doji_threshold:
return CandleType.DOJI
return CandleType.UP if close > open_ else CandleType.DOWN
def encode_candles(df: pd.DataFrame, doji_threshold: float = 0.1) -> np.ndarray:
types = np.zeros(len(df), dtype=np.int8)
body = np.abs(df["close"].values - df["open"].values)
total = df["high"].values - df["low"].values
total = np.where(total == 0, 1e-10, total)
ratio = body / total
types[ratio < doji_threshold] = CandleType.DOJI
bullish = (ratio >= doji_threshold) & (df["close"].values > df["open"].values)
bearish = (ratio >= doji_threshold) & (df["close"].values <= df["open"].values)
types[bullish] = CandleType.UP
types[bearish] = CandleType.DOWN
return types
def extract_body_ratios(df: pd.DataFrame) -> np.ndarray:
body = np.abs(df["close"].values - df["open"].values)
total = df["high"].values - df["low"].values
total = np.where(total == 0, 1e-10, total)
return body / total
def extract_shadow_ratios(df: pd.DataFrame) -> np.ndarray:
o, c, h, l = df["open"].values, df["close"].values, df["high"].values, df["low"].values
upper_shadow = h - np.maximum(o, c)
lower_shadow = np.minimum(o, c) - l
total = h - l
total = np.where(total == 0, 1e-10, total)
return (upper_shadow - lower_shadow) / total
def find_patterns(df: pd.DataFrame, min_len: int = 3, max_len: int = 6, doji_threshold: float = 0.1) -> list[FractalPattern]:
candle_types = encode_candles(df, doji_threshold)
body_ratios = extract_body_ratios(df)
shadow_ratios = extract_shadow_ratios(df)
patterns: list[FractalPattern] = []
for length in range(min_len, max_len + 1):
for i in range(len(df) - length):
seq = tuple(CandleType(t) for t in candle_types[i : i + length])
br = tuple(body_ratios[i : i + length])
sr = tuple(shadow_ratios[i : i + length])
patterns.append(FractalPattern(
sequence=seq,
start_idx=i,
end_idx=i + length,
body_ratios=br,
shadow_ratios=sr,
))
return patterns
def pattern_frequency(patterns: list[FractalPattern]) -> pd.DataFrame:
from collections import Counter
codes = [p.code for p in patterns]
counts = Counter(codes)
total = len(codes)
rows = [
{"pattern": code, "count": cnt, "freq": cnt / total, "length": len(code)}
for code, cnt in counts.most_common()
]
return pd.DataFrame(rows)
def normalize_pattern_window(df: pd.DataFrame, start: int, end: int) -> np.ndarray:
"""Normalize OHLC window to [0,1] range for comparison."""
window = df.iloc[start:end][["open", "high", "low", "close"]].values
mn = window.min()
mx = window.max()
if mx - mn == 0:
return np.zeros_like(window)
return (window - mn) / (mx - mn)
+80
View File
@@ -0,0 +1,80 @@
"""Fractal similarity measures: DTW, Hausdorff, correlation-based."""
from __future__ import annotations
import numpy as np
from scipy.spatial.distance import directed_hausdorff
from scipy.signal import correlate
def dtw_distance(s1: np.ndarray, s2: np.ndarray) -> float:
"""Dynamic Time Warping distance between two 1D sequences."""
n, m = len(s1), len(s2)
dtw = np.full((n + 1, m + 1), np.inf)
dtw[0, 0] = 0.0
for i in range(1, n + 1):
for j in range(1, m + 1):
cost = abs(s1[i - 1] - s2[j - 1])
dtw[i, j] = cost + min(dtw[i - 1, j], dtw[i, j - 1], dtw[i - 1, j - 1])
return dtw[n, m]
def hausdorff_distance(s1: np.ndarray, s2: np.ndarray) -> float:
"""Hausdorff distance between two OHLC windows (shape Nx4)."""
if s1.ndim == 1:
s1 = s1.reshape(-1, 1)
if s2.ndim == 1:
s2 = s2.reshape(-1, 1)
d1 = directed_hausdorff(s1, s2)[0]
d2 = directed_hausdorff(s2, s1)[0]
return max(d1, d2)
def cross_correlation(s1: np.ndarray, s2: np.ndarray) -> float:
"""Max normalized cross-correlation between two 1D sequences."""
if len(s1) == 0 or len(s2) == 0:
return 0.0
s1_norm = (s1 - np.mean(s1))
s2_norm = (s2 - np.mean(s2))
std1, std2 = np.std(s1), np.std(s2)
if std1 == 0 or std2 == 0:
return 0.0
corr = correlate(s1_norm, s2_norm, mode="full")
corr /= (std1 * std2 * len(s1))
return float(np.max(np.abs(corr)))
def cosine_similarity(v1: np.ndarray, v2: np.ndarray) -> float:
"""Cosine similarity between two feature vectors."""
dot = np.dot(v1, v2)
n1, n2 = np.linalg.norm(v1), np.linalg.norm(v2)
if n1 == 0 or n2 == 0:
return 0.0
return float(dot / (n1 * n2))
def pattern_feature_vector(ohlc_window: np.ndarray) -> np.ndarray:
"""Extract compact feature vector from normalized OHLC window.
Features: body ratios, shadow ratios, close-to-close returns,
volatility, trend.
"""
o, h, l, c = ohlc_window[:, 0], ohlc_window[:, 1], ohlc_window[:, 2], ohlc_window[:, 3]
total = h - l
total = np.where(total == 0, 1e-10, total)
body = np.abs(c - o) / total
upper_shadow = (h - np.maximum(o, c)) / total
lower_shadow = (np.minimum(o, c) - l) / total
returns = np.diff(c) / np.where(c[:-1] == 0, 1e-10, c[:-1])
features = np.concatenate([
body,
upper_shadow,
lower_shadow,
returns,
[np.std(returns) if len(returns) > 0 else 0],
[c[-1] - c[0]],
])
return features