amquant 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- amquant/__init__.py +4 -0
- amquant/bar.py +28 -0
- amquant/features.py +173 -0
- amquant/http_client.py +37 -0
- amquant/loader.py +88 -0
- amquant/manual_data.py +35 -0
- amquant/universe.py +14 -0
- amquant/universe_data.py +98 -0
- amquant/yahoo_finance.py +87 -0
- amquant-0.1.0.dist-info/METADATA +42 -0
- amquant-0.1.0.dist-info/RECORD +13 -0
- amquant-0.1.0.dist-info/WHEEL +5 -0
- amquant-0.1.0.dist-info/top_level.txt +1 -0
amquant/__init__.py
ADDED
amquant/bar.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Shared data model. Mirrors include/qde/bar.hpp.
|
|
2
|
+
|
|
3
|
+
Everything downstream (csv_io, features) depends only on this module,
|
|
4
|
+
same as in the C++ version -- it's the one type both Yahoo-sourced and
|
|
5
|
+
manually-loaded (BVMT) data get normalized into.
|
|
6
|
+
"""
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Bar:
|
|
12
|
+
"""One trading day (or bar) of market data for a single instrument."""
|
|
13
|
+
timestamp_utc: int = 0 # unix seconds
|
|
14
|
+
open: float = 0.0
|
|
15
|
+
high: float = 0.0
|
|
16
|
+
low: float = 0.0
|
|
17
|
+
close: float = 0.0
|
|
18
|
+
adj_close: float = 0.0 # dividend/split adjusted; falls back to close if unknown
|
|
19
|
+
volume: float = 0.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Series:
|
|
24
|
+
"""Raw series for one instrument."""
|
|
25
|
+
symbol: str = ""
|
|
26
|
+
exchange: str = "" # e.g. "BVMT", "EURONEXT_PARIS", "XETRA"
|
|
27
|
+
country: str = "" # e.g. "TN", "FR", "DE"
|
|
28
|
+
bars: list = field(default_factory=list) # list[Bar], ascending by timestamp
|
amquant/features.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Feature engineering. Mirrors include/qde/features.hpp + src/features.cpp.
|
|
2
|
+
|
|
3
|
+
Deliberately hand-rolled (not pandas/ta-lib) so the math is a line-for-line
|
|
4
|
+
match with the C++ version -- same Wilder smoothing for RSI/ATR, same
|
|
5
|
+
rolling-sum trick for the moving average, same annualization factor.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import math
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from amquant.bar import Series
|
|
12
|
+
|
|
13
|
+
NaN = float("nan")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class FeatureSet:
|
|
18
|
+
"""All lists are the same length as series.bars. Early entries in any
|
|
19
|
+
rolling-window feature are NaN until the window fills."""
|
|
20
|
+
timestamp: list = field(default_factory=list)
|
|
21
|
+
ret_1d: list = field(default_factory=list)
|
|
22
|
+
log_ret_1d: list = field(default_factory=list)
|
|
23
|
+
sma_5: list = field(default_factory=list)
|
|
24
|
+
sma_20: list = field(default_factory=list)
|
|
25
|
+
sma_50: list = field(default_factory=list)
|
|
26
|
+
ema_12: list = field(default_factory=list)
|
|
27
|
+
ema_26: list = field(default_factory=list)
|
|
28
|
+
macd: list = field(default_factory=list)
|
|
29
|
+
macd_signal: list = field(default_factory=list)
|
|
30
|
+
rsi_14: list = field(default_factory=list)
|
|
31
|
+
vol_20: list = field(default_factory=list)
|
|
32
|
+
bb_upper_20: list = field(default_factory=list)
|
|
33
|
+
bb_lower_20: list = field(default_factory=list)
|
|
34
|
+
atr_14: list = field(default_factory=list)
|
|
35
|
+
momentum_10: list = field(default_factory=list)
|
|
36
|
+
volume_zscore_20: list = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _rolling_mean(x: list, window: int) -> list:
|
|
40
|
+
out = [NaN] * len(x)
|
|
41
|
+
s = 0.0
|
|
42
|
+
for i, v in enumerate(x):
|
|
43
|
+
s += v
|
|
44
|
+
if i >= window:
|
|
45
|
+
s -= x[i - window]
|
|
46
|
+
if i >= window - 1:
|
|
47
|
+
out[i] = s / window
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _rolling_std(x: list, window: int) -> list:
|
|
52
|
+
out = [NaN] * len(x)
|
|
53
|
+
for i in range(window - 1, len(x)):
|
|
54
|
+
window_vals = x[i - window + 1: i + 1]
|
|
55
|
+
mean = sum(window_vals) / window
|
|
56
|
+
var = sum((v - mean) ** 2 for v in window_vals) / max(window - 1, 1)
|
|
57
|
+
out[i] = math.sqrt(var)
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _ema(x: list, window: int) -> list:
|
|
62
|
+
out = [NaN] * len(x)
|
|
63
|
+
if not x:
|
|
64
|
+
return out
|
|
65
|
+
alpha = 2.0 / (window + 1.0)
|
|
66
|
+
prev = NaN
|
|
67
|
+
for i, v in enumerate(x):
|
|
68
|
+
if math.isnan(v):
|
|
69
|
+
out[i] = prev
|
|
70
|
+
continue
|
|
71
|
+
prev = v if math.isnan(prev) else alpha * v + (1 - alpha) * prev
|
|
72
|
+
out[i] = prev
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def compute_features(series: Series) -> FeatureSet:
|
|
77
|
+
bars = series.bars
|
|
78
|
+
n = len(bars)
|
|
79
|
+
f = FeatureSet()
|
|
80
|
+
f.timestamp = [b.timestamp_utc for b in bars]
|
|
81
|
+
|
|
82
|
+
close = [b.adj_close if b.adj_close != 0.0 else b.close for b in bars]
|
|
83
|
+
volume = [b.volume for b in bars]
|
|
84
|
+
high = [b.high for b in bars]
|
|
85
|
+
low = [b.low for b in bars]
|
|
86
|
+
|
|
87
|
+
f.ret_1d = [NaN] * n
|
|
88
|
+
f.log_ret_1d = [NaN] * n
|
|
89
|
+
for i in range(1, n):
|
|
90
|
+
if close[i - 1] > 0:
|
|
91
|
+
f.ret_1d[i] = close[i] / close[i - 1] - 1.0
|
|
92
|
+
f.log_ret_1d[i] = math.log(close[i] / close[i - 1])
|
|
93
|
+
|
|
94
|
+
f.sma_5 = _rolling_mean(close, 5)
|
|
95
|
+
f.sma_20 = _rolling_mean(close, 20)
|
|
96
|
+
f.sma_50 = _rolling_mean(close, 50)
|
|
97
|
+
f.ema_12 = _ema(close, 12)
|
|
98
|
+
f.ema_26 = _ema(close, 26)
|
|
99
|
+
|
|
100
|
+
f.macd = [NaN] * n
|
|
101
|
+
for i in range(n):
|
|
102
|
+
if not math.isnan(f.ema_12[i]) and not math.isnan(f.ema_26[i]):
|
|
103
|
+
f.macd[i] = f.ema_12[i] - f.ema_26[i]
|
|
104
|
+
f.macd_signal = _ema(f.macd, 9)
|
|
105
|
+
|
|
106
|
+
# RSI-14, Wilder's smoothing (not a naive SMA of gains/losses)
|
|
107
|
+
f.rsi_14 = [NaN] * n
|
|
108
|
+
window = 14
|
|
109
|
+
avg_gain = 0.0
|
|
110
|
+
avg_loss = 0.0
|
|
111
|
+
for i in range(1, n):
|
|
112
|
+
change = close[i] - close[i - 1]
|
|
113
|
+
gain = change if change > 0 else 0.0
|
|
114
|
+
loss = -change if change < 0 else 0.0
|
|
115
|
+
if i <= window:
|
|
116
|
+
avg_gain += gain / window
|
|
117
|
+
avg_loss += loss / window
|
|
118
|
+
if i == window:
|
|
119
|
+
rs = math.inf if avg_loss == 0.0 else avg_gain / avg_loss
|
|
120
|
+
f.rsi_14[i] = 100.0 - (100.0 / (1.0 + rs))
|
|
121
|
+
else:
|
|
122
|
+
avg_gain = (avg_gain * (window - 1) + gain) / window
|
|
123
|
+
avg_loss = (avg_loss * (window - 1) + loss) / window
|
|
124
|
+
rs = math.inf if avg_loss == 0.0 else avg_gain / avg_loss
|
|
125
|
+
f.rsi_14[i] = 100.0 - (100.0 / (1.0 + rs))
|
|
126
|
+
|
|
127
|
+
# Rolling volatility of log returns, annualized (sqrt(252))
|
|
128
|
+
f.vol_20 = _rolling_std(f.log_ret_1d, 20)
|
|
129
|
+
f.vol_20 = [v * math.sqrt(252.0) if not math.isnan(v) else v for v in f.vol_20]
|
|
130
|
+
|
|
131
|
+
# Bollinger bands (20, 2 std) on raw price std
|
|
132
|
+
raw_std_20 = _rolling_std(close, 20)
|
|
133
|
+
f.bb_upper_20 = [NaN] * n
|
|
134
|
+
f.bb_lower_20 = [NaN] * n
|
|
135
|
+
for i in range(n):
|
|
136
|
+
if not math.isnan(f.sma_20[i]) and not math.isnan(raw_std_20[i]):
|
|
137
|
+
f.bb_upper_20[i] = f.sma_20[i] + 2 * raw_std_20[i]
|
|
138
|
+
f.bb_lower_20[i] = f.sma_20[i] - 2 * raw_std_20[i]
|
|
139
|
+
|
|
140
|
+
# ATR-14, Wilder smoothing on true range
|
|
141
|
+
f.atr_14 = [NaN] * n
|
|
142
|
+
tr = [NaN] * n
|
|
143
|
+
for i in range(1, n):
|
|
144
|
+
a = high[i] - low[i]
|
|
145
|
+
b = abs(high[i] - close[i - 1])
|
|
146
|
+
c = abs(low[i] - close[i - 1])
|
|
147
|
+
tr[i] = max(a, b, c)
|
|
148
|
+
atr = 0.0
|
|
149
|
+
window = 14
|
|
150
|
+
for i in range(1, n):
|
|
151
|
+
if i <= window:
|
|
152
|
+
atr += tr[i] / window
|
|
153
|
+
if i == window:
|
|
154
|
+
f.atr_14[i] = atr
|
|
155
|
+
else:
|
|
156
|
+
atr = (atr * (window - 1) + tr[i]) / window
|
|
157
|
+
f.atr_14[i] = atr
|
|
158
|
+
|
|
159
|
+
# Momentum-10
|
|
160
|
+
f.momentum_10 = [NaN] * n
|
|
161
|
+
for i in range(10, n):
|
|
162
|
+
if close[i - 10] > 0:
|
|
163
|
+
f.momentum_10[i] = close[i] / close[i - 10] - 1.0
|
|
164
|
+
|
|
165
|
+
# Volume z-score over 20d window
|
|
166
|
+
vol_mean_20 = _rolling_mean(volume, 20)
|
|
167
|
+
vol_std_20 = _rolling_std(volume, 20)
|
|
168
|
+
f.volume_zscore_20 = [NaN] * n
|
|
169
|
+
for i in range(n):
|
|
170
|
+
if not math.isnan(vol_mean_20[i]) and not math.isnan(vol_std_20[i]) and vol_std_20[i] > 0:
|
|
171
|
+
f.volume_zscore_20[i] = (volume[i] - vol_mean_20[i]) / vol_std_20[i]
|
|
172
|
+
|
|
173
|
+
return f
|
amquant/http_client.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Thin HTTP GET wrapper. Mirrors include/qde/http_client.hpp + src/http_client.cpp.
|
|
2
|
+
|
|
3
|
+
Nothing finance-specific here on purpose -- same reasoning as the C++ version:
|
|
4
|
+
if you swap Yahoo for another vendor later, this file doesn't change.
|
|
5
|
+
"""
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class HttpResponse:
|
|
12
|
+
status_code: int = 0
|
|
13
|
+
body: str = ""
|
|
14
|
+
|
|
15
|
+
def ok(self) -> bool:
|
|
16
|
+
return 200 <= self.status_code < 300
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HttpClient:
|
|
20
|
+
"""Equivalent of qde::HttpClient. requests already handles connection
|
|
21
|
+
pooling / redirects / TLS verification, so this wrapper is thinner than
|
|
22
|
+
its C++ counterpart -- but kept as its own file for the same reason:
|
|
23
|
+
isolate networking from parsing logic.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, user_agent: str = "Mozilla/5.0 (X11; Linux x86_64) qde-data-engine/1.0"):
|
|
27
|
+
self.user_agent = user_agent
|
|
28
|
+
|
|
29
|
+
def get(self, url: str, headers: dict | None = None, timeout: int = 20) -> HttpResponse:
|
|
30
|
+
req_headers = {"User-Agent": self.user_agent}
|
|
31
|
+
if headers:
|
|
32
|
+
req_headers.update(headers)
|
|
33
|
+
try:
|
|
34
|
+
resp = requests.get(url, headers=req_headers, timeout=timeout)
|
|
35
|
+
except requests.RequestException as e:
|
|
36
|
+
raise RuntimeError(f"request failed: {e}") from e
|
|
37
|
+
return HttpResponse(status_code=resp.status_code, body=resp.text)
|
amquant/loader.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from amquant.universe_data import UNIVERSE
|
|
7
|
+
from amquant.manual_data import MANUAL_SERIES
|
|
8
|
+
from amquant.yahoo_finance import YahooFinanceClient
|
|
9
|
+
from amquant.features import compute_features, FeatureSet
|
|
10
|
+
from amquant.bar import Series
|
|
11
|
+
|
|
12
|
+
RAW_SERIES: dict[str, Series] = {}
|
|
13
|
+
FEATURE_SETS: dict[str, FeatureSet] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class LoadResult:
|
|
18
|
+
raw_series: dict[str, Series] = field(default_factory=dict)
|
|
19
|
+
feature_sets: dict[str, FeatureSet] = field(default_factory=dict)
|
|
20
|
+
ok: int = 0
|
|
21
|
+
failed: int = 0
|
|
22
|
+
skipped: int = 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_market_data(
|
|
26
|
+
universe=None,
|
|
27
|
+
*,
|
|
28
|
+
yahoo_client: YahooFinanceClient | None = None,
|
|
29
|
+
sleep_between_calls: float = 0.3,
|
|
30
|
+
verbose: bool = True,
|
|
31
|
+
update_globals: bool = True,
|
|
32
|
+
) -> LoadResult:
|
|
33
|
+
"""
|
|
34
|
+
Fetch/assemble bar series for every instrument in `universe`, compute features,
|
|
35
|
+
and return them as a LoadResult. By default also mirrors results into
|
|
36
|
+
amquant.loader.RAW_SERIES / FEATURE_SETS for quick terminal inspection.
|
|
37
|
+
"""
|
|
38
|
+
universe = universe if universe is not None else UNIVERSE
|
|
39
|
+
yahoo = yahoo_client if yahoo_client is not None else YahooFinanceClient()
|
|
40
|
+
|
|
41
|
+
result = LoadResult()
|
|
42
|
+
|
|
43
|
+
if verbose:
|
|
44
|
+
print(f"Loaded {len(universe)} instruments")
|
|
45
|
+
|
|
46
|
+
for inst in universe:
|
|
47
|
+
series = None
|
|
48
|
+
if inst.source == "yahoo" and inst.yahoo_symbol:
|
|
49
|
+
series = yahoo.fetch_history(inst.yahoo_symbol, "1y", "1d")
|
|
50
|
+
if sleep_between_calls:
|
|
51
|
+
time.sleep(sleep_between_calls)
|
|
52
|
+
else:
|
|
53
|
+
series = MANUAL_SERIES.get(inst.symbol)
|
|
54
|
+
if series is None and verbose:
|
|
55
|
+
print(f"[manual] {inst.symbol}: no entry in MANUAL_SERIES", file=sys.stderr)
|
|
56
|
+
|
|
57
|
+
if not series or not series.bars:
|
|
58
|
+
if verbose:
|
|
59
|
+
print(f"SKIP {inst.symbol} ({inst.exchange})", file=sys.stderr)
|
|
60
|
+
result.skipped += 1
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
series.symbol = inst.symbol
|
|
64
|
+
series.exchange = inst.exchange
|
|
65
|
+
series.country = inst.country
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
features = compute_features(series)
|
|
69
|
+
result.raw_series[inst.symbol] = series
|
|
70
|
+
result.feature_sets[inst.symbol] = features
|
|
71
|
+
if verbose:
|
|
72
|
+
print(f"OK {inst.symbol} ({inst.exchange}) -- {len(series.bars)} bars")
|
|
73
|
+
result.ok += 1
|
|
74
|
+
except Exception as e:
|
|
75
|
+
if verbose:
|
|
76
|
+
print(f"FAIL {inst.symbol}: {e}", file=sys.stderr)
|
|
77
|
+
result.failed += 1
|
|
78
|
+
|
|
79
|
+
if verbose:
|
|
80
|
+
print(f"\nDone. ok={result.ok} failed={result.failed} skipped={result.skipped}")
|
|
81
|
+
|
|
82
|
+
if update_globals:
|
|
83
|
+
RAW_SERIES.clear()
|
|
84
|
+
RAW_SERIES.update(result.raw_series)
|
|
85
|
+
FEATURE_SETS.clear()
|
|
86
|
+
FEATURE_SETS.update(result.feature_sets)
|
|
87
|
+
|
|
88
|
+
return result
|
amquant/manual_data.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from amquant.bar import Bar, Series
|
|
3
|
+
|
|
4
|
+
MANUAL_SERIES: dict[str, Series] = {
|
|
5
|
+
"SFBT": Series(symbol="SFBT", bars=[
|
|
6
|
+
# TODO: paste real SFBT bars here
|
|
7
|
+
]),
|
|
8
|
+
"BIAT": Series(symbol="BIAT", bars=[
|
|
9
|
+
# TODO: paste real BIAT bars here
|
|
10
|
+
]),
|
|
11
|
+
"ATB": Series(symbol="ATB", bars=[
|
|
12
|
+
# TODO: paste real ATB bars here
|
|
13
|
+
]),
|
|
14
|
+
"BT": Series(symbol="BT", bars=[
|
|
15
|
+
# TODO: paste real BT bars here
|
|
16
|
+
]),
|
|
17
|
+
"BH": Series(symbol="BH", bars=[
|
|
18
|
+
# TODO: paste real BH bars here
|
|
19
|
+
]),
|
|
20
|
+
"DELICE": Series(symbol="DELICE", bars=[
|
|
21
|
+
# TODO: paste real DELICE bars here
|
|
22
|
+
]),
|
|
23
|
+
"SOTUVER": Series(symbol="SOTUVER", bars=[
|
|
24
|
+
# TODO: paste real SOTUVER bars here
|
|
25
|
+
]),
|
|
26
|
+
"TLS": Series(symbol="TLS", bars=[
|
|
27
|
+
# TODO: paste real TLS bars here
|
|
28
|
+
]),
|
|
29
|
+
"ENNAKL": Series(symbol="ENNAKL", bars=[
|
|
30
|
+
# TODO: paste real ENNAKL bars here
|
|
31
|
+
]),
|
|
32
|
+
"PGH": Series(symbol="PGH", bars=[
|
|
33
|
+
# TODO: paste real PGH bars here
|
|
34
|
+
]),
|
|
35
|
+
}
|
amquant/universe.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Universe loader. Mirrors include/qde/universe.hpp + src/universe.cpp."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import csv
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Instrument:
|
|
9
|
+
symbol: str = ""
|
|
10
|
+
yahoo_symbol: str = ""
|
|
11
|
+
name: str = ""
|
|
12
|
+
country: str = ""
|
|
13
|
+
exchange: str = ""
|
|
14
|
+
source: str = "" # "yahoo" or "manual_csv"
|
amquant/universe_data.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from amquant.universe import Instrument
|
|
3
|
+
UNIVERSE: list[Instrument] = [
|
|
4
|
+
# ========================= TUNISIAN GROUP (manual) =========================
|
|
5
|
+
Instrument(symbol="SFBT", yahoo_symbol="", name="Societe Frigorifique et Brasserie de Tunis", country="TN", exchange="BVMT", source="manual_csv"),
|
|
6
|
+
Instrument(symbol="BIAT", yahoo_symbol="", name="Banque Internationale Arabe de Tunisie", country="TN", exchange="BVMT", source="manual_csv"),
|
|
7
|
+
Instrument(symbol="ATB", yahoo_symbol="", name="Arab Tunisian Bank", country="TN", exchange="BVMT", source="manual_csv"),
|
|
8
|
+
Instrument(symbol="BT", yahoo_symbol="", name="Banque de Tunisie", country="TN", exchange="BVMT", source="manual_csv"),
|
|
9
|
+
Instrument(symbol="BH", yahoo_symbol="", name="BH Bank", country="TN", exchange="BVMT", source="manual_csv"),
|
|
10
|
+
Instrument(symbol="DELICE", yahoo_symbol="", name="Delice Holding", country="TN", exchange="BVMT", source="manual_csv"),
|
|
11
|
+
Instrument(symbol="SOTUVER", yahoo_symbol="", name="Societe Tunisienne de Verreries", country="TN", exchange="BVMT", source="manual_csv"),
|
|
12
|
+
Instrument(symbol="TLS", yahoo_symbol="", name="Tunisie Leasing and Factoring", country="TN", exchange="BVMT", source="manual_csv"),
|
|
13
|
+
Instrument(symbol="ENNAKL", yahoo_symbol="", name="Ennakl Automobiles", country="TN", exchange="BVMT", source="manual_csv"),
|
|
14
|
+
Instrument(symbol="PGH", yahoo_symbol="", name="Poulina Group Holding", country="TN", exchange="BVMT", source="manual_csv"),
|
|
15
|
+
Instrument(symbol="TJARI", yahoo_symbol="", name="Banque Attijari de Tunisie", country="TN", exchange="BVMT", source="manual_csv"),
|
|
16
|
+
Instrument(symbol="ARTES", yahoo_symbol="", name="ARTES Renault", country="TN", exchange="BVMT", source="manual_csv"),
|
|
17
|
+
Instrument(symbol="SITEX", yahoo_symbol="", name="Societe Industrielle des Textiles", country="TN", exchange="BVMT", source="manual_csv"),
|
|
18
|
+
Instrument(symbol="ASSMA", yahoo_symbol="", name="Assurances Maghrebia", country="TN", exchange="BVMT", source="manual_csv"),
|
|
19
|
+
Instrument(symbol="AB", yahoo_symbol="", name="Amen Bank", country="TN", exchange="BVMT", source="manual_csv"),
|
|
20
|
+
|
|
21
|
+
# ========================= FRENCH GROUP =========================
|
|
22
|
+
Instrument(symbol="MC", yahoo_symbol="MC.PA", name="LVMH", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
23
|
+
Instrument(symbol="TTE", yahoo_symbol="TTE.PA", name="TotalEnergies", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
24
|
+
Instrument(symbol="SAN", yahoo_symbol="SAN.PA", name="Sanofi", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
25
|
+
Instrument(symbol="OR", yahoo_symbol="OR.PA", name="L'Oreal", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
26
|
+
Instrument(symbol="AIR", yahoo_symbol="AIR.PA", name="Airbus", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
27
|
+
Instrument(symbol="BNP", yahoo_symbol="BNP.PA", name="BNP Paribas", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
28
|
+
Instrument(symbol="AI", yahoo_symbol="AI.PA", name="Air Liquide", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
29
|
+
Instrument(symbol="SU", yahoo_symbol="SU.PA", name="Schneider Electric", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
30
|
+
Instrument(symbol="BN", yahoo_symbol="BN.PA", name="Danone", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
31
|
+
Instrument(symbol="ML", yahoo_symbol="ML.PA", name="Michelin", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
32
|
+
Instrument(symbol="RMS", yahoo_symbol="RMS.PA", name="Hermes International", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
33
|
+
Instrument(symbol="ACCP", yahoo_symbol="AC.PA", name="Accor", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
34
|
+
Instrument(symbol="CS", yahoo_symbol="CS.PA", name="AXA", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
35
|
+
# French Market
|
|
36
|
+
Instrument(symbol="CAC40", yahoo_symbol="^FCHI", name="CAC 40 Index", country="FR", exchange="EURONEXT_PARIS", source="yahoo"),
|
|
37
|
+
|
|
38
|
+
# ========================= GERMAN GROUP =========================
|
|
39
|
+
Instrument(symbol="SAP", yahoo_symbol="SAP.DE", name="SAP SE", country="DE", exchange="XETRA", source="yahoo"),
|
|
40
|
+
Instrument(symbol="SIE", yahoo_symbol="SIE.DE", name="Siemens", country="DE", exchange="XETRA", source="yahoo"),
|
|
41
|
+
Instrument(symbol="ALV", yahoo_symbol="ALV.DE", name="Allianz", country="DE", exchange="XETRA", source="yahoo"),
|
|
42
|
+
Instrument(symbol="VOW3", yahoo_symbol="VOW3.DE", name="Volkswagen", country="DE", exchange="XETRA", source="yahoo"),
|
|
43
|
+
Instrument(symbol="BAS", yahoo_symbol="BAS.DE", name="BASF", country="DE", exchange="XETRA", source="yahoo"),
|
|
44
|
+
Instrument(symbol="DBK", yahoo_symbol="DBK.DE", name="Deutsche Bank", country="DE", exchange="XETRA", source="yahoo"),
|
|
45
|
+
Instrument(symbol="BAYN", yahoo_symbol="BAYN.DE", name="Bayer", country="DE", exchange="XETRA", source="yahoo"),
|
|
46
|
+
Instrument(symbol="MBG", yahoo_symbol="MBG.DE", name="Mercedes-Benz Group", country="DE", exchange="XETRA", source="yahoo"),
|
|
47
|
+
Instrument(symbol="ADS", yahoo_symbol="ADS.DE", name="Adidas", country="DE", exchange="XETRA", source="yahoo"),
|
|
48
|
+
Instrument(symbol="BMW", yahoo_symbol="BMW.DE", name="BMW", country="DE", exchange="XETRA", source="yahoo"),
|
|
49
|
+
Instrument(symbol="IFX", yahoo_symbol="IFX.DE", name="Infineon Technologies", country="DE", exchange="XETRA", source="yahoo"),
|
|
50
|
+
Instrument(symbol="DTE", yahoo_symbol="DTE.DE", name="Deutsche Telekom", country="DE", exchange="XETRA", source="yahoo"),
|
|
51
|
+
# German Market
|
|
52
|
+
Instrument(symbol="DAX", yahoo_symbol="^GDAXI", name="DAX Index", country="DE", exchange="XETRA", source="yahoo"),
|
|
53
|
+
|
|
54
|
+
# ========================= ITALIAN GROUP =========================
|
|
55
|
+
Instrument(symbol="ENEL", yahoo_symbol="ENEL.MI", name="Enel", country="IT", exchange="BORSA_ITALIANA", source="yahoo"),
|
|
56
|
+
Instrument(symbol="ENI", yahoo_symbol="ENI.MI", name="Eni", country="IT", exchange="BORSA_ITALIANA", source="yahoo"),
|
|
57
|
+
Instrument(symbol="ISP", yahoo_symbol="ISP.MI", name="Intesa Sanpaolo", country="IT", exchange="BORSA_ITALIANA", source="yahoo"),
|
|
58
|
+
Instrument(symbol="UCG", yahoo_symbol="UCG.MI", name="UniCredit", country="IT", exchange="BORSA_ITALIANA", source="yahoo"),
|
|
59
|
+
Instrument(symbol="RACE", yahoo_symbol="RACE.MI", name="Ferrari", country="IT", exchange="BORSA_ITALIANA", source="yahoo"),
|
|
60
|
+
Instrument(symbol="STLAM", yahoo_symbol="STLAM.MI", name="Stellantis", country="IT", exchange="BORSA_ITALIANA", source="yahoo"),
|
|
61
|
+
Instrument(symbol="G", yahoo_symbol="G.MI", name="Generali", country="IT", exchange="BORSA_ITALIANA", source="yahoo"),
|
|
62
|
+
Instrument(symbol="STM", yahoo_symbol="STMMI.MI", name="STMicroelectronics", country="IT", exchange="BORSA_ITALIANA", source="yahoo"),
|
|
63
|
+
Instrument(symbol="PRY", yahoo_symbol="PRY.MI", name="Prysmian", country="IT", exchange="BORSA_ITALIANA", source="yahoo"),
|
|
64
|
+
# Italian Market (Corrected)
|
|
65
|
+
Instrument(symbol="FTSEMIB", yahoo_symbol="FTSEMIB.MI", name="FTSE MIB Index", country="IT", exchange="BORSA_ITALIANA", source="yahoo"),
|
|
66
|
+
|
|
67
|
+
# ========================= SPANISH GROUP =========================
|
|
68
|
+
Instrument(symbol="IBE", yahoo_symbol="IBE.MC", name="Iberdrola", country="ES", exchange="BME", source="yahoo"),
|
|
69
|
+
Instrument(symbol="SAN_ES", yahoo_symbol="SAN.MC", name="Banco Santander", country="ES", exchange="BME", source="yahoo"),
|
|
70
|
+
Instrument(symbol="ITX", yahoo_symbol="ITX.MC", name="Inditex", country="ES", exchange="BME", source="yahoo"),
|
|
71
|
+
Instrument(symbol="BBVA", yahoo_symbol="BBVA.MC", name="BBVA", country="ES", exchange="BME", source="yahoo"),
|
|
72
|
+
Instrument(symbol="REP", yahoo_symbol="REP.MC", name="Repsol", country="ES", exchange="BME", source="yahoo"),
|
|
73
|
+
Instrument(symbol="TEF", yahoo_symbol="TEF.MC", name="Telefonica", country="ES", exchange="BME", source="yahoo"),
|
|
74
|
+
Instrument(symbol="AMS", yahoo_symbol="AMS.MC", name="Amadeus IT Group", country="ES", exchange="BME", source="yahoo"),
|
|
75
|
+
Instrument(symbol="CABK", yahoo_symbol="CABK.MC", name="CaixaBank", country="ES", exchange="BME", source="yahoo"),
|
|
76
|
+
# Spanish Market
|
|
77
|
+
Instrument(symbol="IBEX35", yahoo_symbol="^IBEX", name="IBEX 35 Index", country="ES", exchange="BME", source="yahoo"),
|
|
78
|
+
|
|
79
|
+
# ========================= DUTCH GROUP =========================
|
|
80
|
+
Instrument(symbol="ASML", yahoo_symbol="ASML.AS", name="ASML Holding", country="NL", exchange="EURONEXT_AMSTERDAM", source="yahoo"),
|
|
81
|
+
Instrument(symbol="HEIA", yahoo_symbol="HEIA.AS", name="Heineken", country="NL", exchange="EURONEXT_AMSTERDAM", source="yahoo"),
|
|
82
|
+
Instrument(symbol="AD", yahoo_symbol="AD.AS", name="Ahold Delhaize", country="NL", exchange="EURONEXT_AMSTERDAM", source="yahoo"),
|
|
83
|
+
Instrument(symbol="PHIA", yahoo_symbol="PHIA.AS", name="Philips", country="NL", exchange="EURONEXT_AMSTERDAM", source="yahoo"),
|
|
84
|
+
Instrument(symbol="INGA", yahoo_symbol="INGA.AS", name="ING Groep", country="NL", exchange="EURONEXT_AMSTERDAM", source="yahoo"),
|
|
85
|
+
Instrument(symbol="PRX", yahoo_symbol="PRX.AS", name="Prosus", country="NL", exchange="EURONEXT_AMSTERDAM", source="yahoo"),
|
|
86
|
+
# Dutch Market
|
|
87
|
+
Instrument(symbol="AEX", yahoo_symbol="^AEX", name="AEX Index", country="NL", exchange="EURONEXT_AMSTERDAM", source="yahoo"),
|
|
88
|
+
|
|
89
|
+
# ========================= BELGIAN GROUP =========================
|
|
90
|
+
Instrument(symbol="ABI", yahoo_symbol="ABI.BR", name="Anheuser-Busch InBev", country="BE", exchange="EURONEXT_BRUSSELS", source="yahoo"),
|
|
91
|
+
Instrument(symbol="KBC", yahoo_symbol="KBC.BR", name="KBC Group", country="BE", exchange="EURONEXT_BRUSSELS", source="yahoo"),
|
|
92
|
+
Instrument(symbol="UCB", yahoo_symbol="UCB.BR", name="UCB SA", country="BE", exchange="EURONEXT_BRUSSELS", source="yahoo"),
|
|
93
|
+
Instrument(symbol="AGS", yahoo_symbol="AGS.BR", name="Ageas", country="BE", exchange="EURONEXT_BRUSSELS", source="yahoo"),
|
|
94
|
+
Instrument(symbol="ARGX", yahoo_symbol="ARGX.BR", name="argenx", country="BE", exchange="EURONEXT_BRUSSELS", source="yahoo"),
|
|
95
|
+
Instrument(symbol="ACKB", yahoo_symbol="ACKB.BR", name="Ackermans & van Haaren", country="BE", exchange="EURONEXT_BRUSSELS", source="yahoo"),
|
|
96
|
+
# Belgian Market
|
|
97
|
+
Instrument(symbol="BEL20", yahoo_symbol="^BFX", name="BEL 20 Index", country="BE", exchange="EURONEXT_BRUSSELS", source="yahoo"),
|
|
98
|
+
]
|
amquant/yahoo_finance.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Yahoo Finance ingestion. Mirrors include/qde/yahoo_finance.hpp + src/yahoo_finance.cpp.
|
|
2
|
+
|
|
3
|
+
Uses the same unofficial chart endpoint:
|
|
4
|
+
https://query1.finance.yahoo.com/v8/finance/chart/<symbol>
|
|
5
|
+
|
|
6
|
+
NOTE: Tunis Stock Exchange (BVMT) tickers are NOT covered by Yahoo Finance --
|
|
7
|
+
same caveat as the C++ version. Use csv_io.read_series_csv for those instead.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from amquant.bar import Bar, Series
|
|
14
|
+
from amquant.http_client import HttpClient
|
|
15
|
+
|
|
16
|
+
_RANGE_MAP = {
|
|
17
|
+
"5d": "5d", "1mo": "1mo", "3mo": "3mo", "6mo": "6mo",
|
|
18
|
+
"1y": "1y", "2y": "2y", "5y": "5y", "10y": "10y", "max": "max",
|
|
19
|
+
}
|
|
20
|
+
_INTERVAL_MAP = {"1d": "1d", "1wk": "1wk", "1mo": "1mo"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class YahooFinanceClient:
|
|
24
|
+
"""Equivalent of qde::YahooFinanceClient."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, http_client: HttpClient | None = None):
|
|
27
|
+
self.http = http_client or HttpClient()
|
|
28
|
+
|
|
29
|
+
def fetch_history(self, symbol: str, range_: str = "2y", interval: str = "1d") -> Optional[Series]:
|
|
30
|
+
"""symbol must be the Yahoo ticker, e.g. 'AAPL', 'MC.PA', 'SAP.DE'.
|
|
31
|
+
Returns None on failure (network or bad HTTP status), same fail-soft
|
|
32
|
+
behaviour as the C++ version so one bad symbol doesn't kill a batch run.
|
|
33
|
+
"""
|
|
34
|
+
url = (
|
|
35
|
+
f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
|
|
36
|
+
f"?range={_RANGE_MAP.get(range_, '2y')}"
|
|
37
|
+
f"&interval={_INTERVAL_MAP.get(interval, '1d')}"
|
|
38
|
+
f"&events=div,splits"
|
|
39
|
+
)
|
|
40
|
+
try:
|
|
41
|
+
resp = self.http.get(url, headers={"Accept": "application/json"})
|
|
42
|
+
except RuntimeError as e:
|
|
43
|
+
print(f"[yahoo] request failed for {symbol}: {e}", file=sys.stderr)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
if not resp.ok():
|
|
47
|
+
print(f"[yahoo] HTTP {resp.status_code} for {symbol}", file=sys.stderr)
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
return self._parse_chart_json(symbol, resp.body)
|
|
51
|
+
|
|
52
|
+
def _parse_chart_json(self, symbol: str, json_text: str) -> Optional[Series]:
|
|
53
|
+
import json as jsonlib
|
|
54
|
+
try:
|
|
55
|
+
root = jsonlib.loads(json_text)
|
|
56
|
+
result = root["chart"]["result"]
|
|
57
|
+
if not result:
|
|
58
|
+
print(f"[yahoo] empty result for {symbol}", file=sys.stderr)
|
|
59
|
+
return None
|
|
60
|
+
r0 = result[0]
|
|
61
|
+
timestamps = r0["timestamp"]
|
|
62
|
+
quote = r0["indicators"]["quote"][0]
|
|
63
|
+
opens, highs, lows, closes, volumes = (
|
|
64
|
+
quote["open"], quote["high"], quote["low"], quote["close"], quote["volume"],
|
|
65
|
+
)
|
|
66
|
+
adjclose = None
|
|
67
|
+
if "adjclose" in r0["indicators"]:
|
|
68
|
+
adjclose = r0["indicators"]["adjclose"][0]["adjclose"]
|
|
69
|
+
|
|
70
|
+
series = Series(symbol=symbol)
|
|
71
|
+
for i, ts in enumerate(timestamps):
|
|
72
|
+
if ts is None or closes[i] is None:
|
|
73
|
+
continue # skip holes, same as the C++ version
|
|
74
|
+
bar = Bar(
|
|
75
|
+
timestamp_utc=int(ts),
|
|
76
|
+
open=opens[i] or 0.0,
|
|
77
|
+
high=highs[i] or 0.0,
|
|
78
|
+
low=lows[i] or 0.0,
|
|
79
|
+
close=closes[i],
|
|
80
|
+
volume=volumes[i] or 0.0,
|
|
81
|
+
adj_close=(adjclose[i] if adjclose and adjclose[i] is not None else closes[i]),
|
|
82
|
+
)
|
|
83
|
+
series.bars.append(bar)
|
|
84
|
+
return series
|
|
85
|
+
except (KeyError, IndexError, TypeError, ValueError, jsonlib.JSONDecodeError) as e:
|
|
86
|
+
print(f"[yahoo] JSON parse error for {symbol}: {e}", file=sys.stderr)
|
|
87
|
+
return None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: amquant
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight library for loading market data and computing engineered features.
|
|
5
|
+
Author-email: Your Name <you@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: quant,finance,market-data,features,trading
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: requests>=2.31
|
|
11
|
+
Requires-Dist: pandas>=2.0
|
|
12
|
+
|
|
13
|
+
**activate the virtual env with** : source venv/bin/activate
|
|
14
|
+
|
|
15
|
+
**interactive program** :python3 -i main.py
|
|
16
|
+
|
|
17
|
+
**DATA ACCESS:**
|
|
18
|
+
|
|
19
|
+
After `main()` executes in the same Python session:
|
|
20
|
+
|
|
21
|
+
- `RAW_SERIES: dict[str, Series]` — populated with fully enriched `Series` objects (OHLCV + metadata).
|
|
22
|
+
- `FEATURE_SETS: dict[str, FeatureSet]` — contains computed technical features per symbol.
|
|
23
|
+
|
|
24
|
+
Both dicts are module-level globals, accessible immediately post-run via `RAW_SERIES['BIAT']` or `FEATURE_SETS['MC']`.
|
|
25
|
+
|
|
26
|
+
Data lives in memory for the duration of the Python process / REPL session. Not persisted to disk unless explicitly saved.
|
|
27
|
+
|
|
28
|
+
For library use: expose `RAW_SERIES` and `FEATURE_SETS` via a `DataManager` singleton or return them from `load_universe()` for clean dependency injection.
|
|
29
|
+
|
|
30
|
+
**demo**
|
|
31
|
+
|
|
32
|
+
list(RAW_SERIES.keys())
|
|
33
|
+
RAW_SERIES["BNP"].bars[0]
|
|
34
|
+
FEATURE_SETS["BNP"].ret_1d[:5]
|
|
35
|
+
FEATURE_SETS["BNP"].sma_20[:5]
|
|
36
|
+
FEATURE_SETS["BNP"].__dict__.keys()
|
|
37
|
+
len(RAW_SERIES["BNP"].bars)
|
|
38
|
+
len(RAW_SERIES)
|
|
39
|
+
len(FEATURE_SETS)
|
|
40
|
+
|
|
41
|
+
# AmQuant
|
|
42
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
amquant/__init__.py,sha256=uJ-E1jM39IUGvw6YF8s5NdH1agERbNYl0FotzY3wDGs,179
|
|
2
|
+
amquant/bar.py,sha256=BlAKPAg5m9EA6KxdVZXQ5BbOl7BMLTL49q4uz3a4H6c,916
|
|
3
|
+
amquant/features.py,sha256=gNhBXei0Q-upcISOso9cBYSJNqu-LsNwrPIDgdmkNzU,5829
|
|
4
|
+
amquant/http_client.py,sha256=Av9EKuNwUtOZ08CQ7It4WUDR0-IKpWgbRchVrumbmD4,1346
|
|
5
|
+
amquant/loader.py,sha256=imkZDxllWfwBoTqBgWnrrotPyz5MyFmDHfahGXE7I5o,2878
|
|
6
|
+
amquant/manual_data.py,sha256=Dfy3O_KGNf_5BvBsEXwLYb3ebSwsGPJyyN-hLmkMH0w,1020
|
|
7
|
+
amquant/universe.py,sha256=IvDHRLlqDl_KbylHCNZXObOoPPAL4l2pK4SM-8uRh2U,348
|
|
8
|
+
amquant/universe_data.py,sha256=h0IkYZsGtIitc2Ot0NvExlF8l0v-TDc2R4NBFCgSDJw,10311
|
|
9
|
+
amquant/yahoo_finance.py,sha256=5llFkJ0TwCGcKJ_BoylZ0vc_nNMmNJinbhaAl9G4X74,3563
|
|
10
|
+
amquant-0.1.0.dist-info/METADATA,sha256=vUqIT7Q6perrqbu6YUaeI74VPf1nSi_cvvt9bzO3yBI,1385
|
|
11
|
+
amquant-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
12
|
+
amquant-0.1.0.dist-info/top_level.txt,sha256=Oe3cmGEGH8HTW3Bvfpyw8uIyT4MmE-_UuBprzEvaeVM,8
|
|
13
|
+
amquant-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
amquant
|