quantred 2.4.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.
- quantred/__init__.py +24 -0
- quantred/alphas/__init__.py +53 -0
- quantred/alphas/base.py +221 -0
- quantred/alphas/combine.py +260 -0
- quantred/alphas/horizon.py +153 -0
- quantred/alphas/refine.py +300 -0
- quantred/alphas/scorers.py +87 -0
- quantred/analytics/attribution.py +354 -0
- quantred/analytics/bootstrap.py +353 -0
- quantred/analytics/charts.py +393 -0
- quantred/analytics/excursion.py +185 -0
- quantred/analytics/execution_quality.py +180 -0
- quantred/analytics/exposure.py +167 -0
- quantred/analytics/htmlreport.py +1208 -0
- quantred/analytics/information.py +197 -0
- quantred/analytics/metrics.py +502 -0
- quantred/analytics/performance.py +639 -0
- quantred/analytics/reporting.py +1672 -0
- quantred/analytics/screening.py +190 -0
- quantred/analytics/series_comparison.py +303 -0
- quantred/analytics/trade_analytics.py +353 -0
- quantred/brokers/alpaca/broker.py +415 -0
- quantred/brokers/alpaca/factory.py +33 -0
- quantred/brokers/alpaca/market_data.py +166 -0
- quantred/brokers/base.py +220 -0
- quantred/brokers/dryrun.py +164 -0
- quantred/brokers/errors.py +70 -0
- quantred/cli.py +8619 -0
- quantred/costs/__init__.py +11 -0
- quantred/costs/base.py +126 -0
- quantred/costs/parametric.py +131 -0
- quantred/data/__init__.py +29 -0
- quantred/data/compute.py +332 -0
- quantred/data/edges.py +81 -0
- quantred/data/features.py +120 -0
- quantred/data/panel.py +61 -0
- quantred/data/scan.py +85 -0
- quantred/data/store.py +269 -0
- quantred/demo/__init__.py +22 -0
- quantred/demo/scanners.py +104 -0
- quantred/demo/strategies.py +125 -0
- quantred/engine/backtest.py +1206 -0
- quantred/engine/barcheck.py +228 -0
- quantred/engine/live.py +539 -0
- quantred/execution/decision.py +192 -0
- quantred/execution/flatten.py +302 -0
- quantred/execution/halt.py +165 -0
- quantred/execution/ledger.py +758 -0
- quantred/execution/live_trader.py +587 -0
- quantred/execution/order_id.py +83 -0
- quantred/execution/sizing.py +114 -0
- quantred/indicators/indicators.py +158 -0
- quantred/marketdata/base.py +62 -0
- quantred/marketdata/client.py +59 -0
- quantred/marketdata/session.py +112 -0
- quantred/marketdata/synthetic.py +74 -0
- quantred/marketdata/timeframe.py +125 -0
- quantred/mcp/__init__.py +6 -0
- quantred/mcp/selftest.py +139 -0
- quantred/mcp/server.py +2419 -0
- quantred/optimization/causality.py +607 -0
- quantred/optimization/config_store.py +300 -0
- quantred/optimization/optimizer.py +431 -0
- quantred/optimization/parallel.py +345 -0
- quantred/optimization/param_space.py +309 -0
- quantred/optimization/walk_forward.py +1254 -0
- quantred/portfolio/allocator.py +135 -0
- quantred/portfolio/benchmark.py +109 -0
- quantred/portfolio/optimizer.py +786 -0
- quantred/portfolio/policy.py +335 -0
- quantred/portfolio/posterior.py +208 -0
- quantred/research/__init__.py +30 -0
- quantred/research/agent.py +868 -0
- quantred/research/demo_proposals.py +222 -0
- quantred/research/llm.py +311 -0
- quantred/research/proposer.py +234 -0
- quantred/research/provider_errors.py +242 -0
- quantred/research/sandbox.py +340 -0
- quantred/risk/__init__.py +60 -0
- quantred/risk/base.py +218 -0
- quantred/risk/conditional.py +474 -0
- quantred/risk/exposures.py +88 -0
- quantred/risk/factor.py +119 -0
- quantred/risk/sample.py +89 -0
- quantred/risk/streaming.py +214 -0
- quantred/scanners/base.py +141 -0
- quantred/scanners/symbol_scanner.py +148 -0
- quantred/services/__init__.py +51 -0
- quantred/services/analysis.py +6082 -0
- quantred/services/audit.py +574 -0
- quantred/services/campaign.py +381 -0
- quantred/services/configs.py +282 -0
- quantred/services/data.py +172 -0
- quantred/services/dryrun.py +206 -0
- quantred/services/glossary.py +130 -0
- quantred/services/maintenance.py +314 -0
- quantred/services/registry.py +351 -0
- quantred/services/setup.py +1023 -0
- quantred/services/sizing.py +150 -0
- quantred/services/smallreal.py +450 -0
- quantred/settings.py +571 -0
- quantred/store/__init__.py +0 -0
- quantred/store/bars.py +660 -0
- quantred/store/trials.py +1804 -0
- quantred/strategies/base.py +643 -0
- quantred/strategies/signals.py +28 -0
- quantred/utils/errors.py +48 -0
- quantred/utils/extras.py +330 -0
- quantred/utils/logging_config.py +33 -0
- quantred/utils/numeric.py +46 -0
- quantred/utils/streaming.py +474 -0
- quantred/utils/timeutils.py +148 -0
- quantred-2.4.0.dist-info/METADATA +632 -0
- quantred-2.4.0.dist-info/RECORD +117 -0
- quantred-2.4.0.dist-info/WHEEL +4 -0
- quantred-2.4.0.dist-info/entry_points.txt +8 -0
- quantred-2.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""The example scanner: volume and price-move confirmation.
|
|
2
|
+
|
|
3
|
+
Flags symbols whose latest bar shows unusually high volume *and* a meaningful price
|
|
4
|
+
move - a simple liquidity/attention filter for building a trading universe. Pure
|
|
5
|
+
pandas/numpy; no TA-Lib.
|
|
6
|
+
|
|
7
|
+
**A demonstration, not a screen to trade on.** It reaches the engine through the
|
|
8
|
+
``quantred.scanners`` entry-point group in this project's ``pyproject.toml``, which is
|
|
9
|
+
the same mechanism a private pack uses. Yours belongs in your own package -
|
|
10
|
+
``quantred init --example-pack ./my-signals`` copies one you own.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any, ClassVar, Dict
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
from quantred.scanners.base import SCANNER_BUY, SCANNER_HOLD, SCANNER_SELL, ScannerStrategy
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DemoVolumeScanner(ScannerStrategy):
|
|
21
|
+
"""Select symbols on a volume spike confirmed by price movement."""
|
|
22
|
+
|
|
23
|
+
#: A shipped demonstration, not a screen anyone should trade on. Carried on
|
|
24
|
+
#: the class so it survives being registered by entry point.
|
|
25
|
+
DEMO = True
|
|
26
|
+
|
|
27
|
+
TIMEFRAME = "1Day"
|
|
28
|
+
|
|
29
|
+
PARAM_RANGES: ClassVar[Dict[str, Dict[str, Any]]] = {
|
|
30
|
+
"min_volume": {
|
|
31
|
+
"type": "float",
|
|
32
|
+
"min": 50_000,
|
|
33
|
+
"max": 1_000_000,
|
|
34
|
+
"step": 50_000,
|
|
35
|
+
"default": 500_000,
|
|
36
|
+
"description": "Minimum bar volume to consider a symbol liquid",
|
|
37
|
+
},
|
|
38
|
+
"volume_ma_period": {
|
|
39
|
+
"type": "int",
|
|
40
|
+
"min": 5,
|
|
41
|
+
"max": 50,
|
|
42
|
+
"step": 5,
|
|
43
|
+
"default": 10,
|
|
44
|
+
"description": "Lookback for the volume moving-average baseline",
|
|
45
|
+
},
|
|
46
|
+
"volume_threshold": {
|
|
47
|
+
"type": "float",
|
|
48
|
+
"min": 1.0,
|
|
49
|
+
"max": 5.0,
|
|
50
|
+
"step": 0.25,
|
|
51
|
+
"default": 1.75,
|
|
52
|
+
"description": "Volume / volume-MA ratio that flags unusual activity",
|
|
53
|
+
},
|
|
54
|
+
"price_change_threshold": {
|
|
55
|
+
"type": "float",
|
|
56
|
+
"min": 0.5,
|
|
57
|
+
"max": 5.0,
|
|
58
|
+
"step": 0.25,
|
|
59
|
+
"default": 0.5,
|
|
60
|
+
"description": "Minimum |intrabar price change| (%) to confirm",
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def initialize(self) -> None: # nothing to set up
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def required_data_points(self) -> int:
|
|
68
|
+
return self.config["volume_ma_period"] + 1
|
|
69
|
+
|
|
70
|
+
def process_data(self, data: pd.DataFrame) -> pd.DataFrame:
|
|
71
|
+
if data.empty:
|
|
72
|
+
return pd.DataFrame()
|
|
73
|
+
|
|
74
|
+
enriched = data.copy()
|
|
75
|
+
volume_ma = data["volume"].rolling(window=self.config["volume_ma_period"]).mean()
|
|
76
|
+
enriched["volume_ratio"] = data["volume"] / volume_ma
|
|
77
|
+
enriched["price_change"] = ((data["close"] - data["open"]) / data["open"] * 100).abs()
|
|
78
|
+
return enriched
|
|
79
|
+
|
|
80
|
+
def generate_signals_df(self, data: pd.DataFrame) -> pd.DataFrame:
|
|
81
|
+
if data.empty:
|
|
82
|
+
return pd.DataFrame()
|
|
83
|
+
|
|
84
|
+
out = pd.DataFrame(index=data.index)
|
|
85
|
+
out["signal"] = SCANNER_HOLD
|
|
86
|
+
out["signal_strength"] = 0.0
|
|
87
|
+
|
|
88
|
+
qualifies = (
|
|
89
|
+
(data["volume_ratio"] > self.config["volume_threshold"])
|
|
90
|
+
& (data["price_change"] > self.config["price_change_threshold"])
|
|
91
|
+
& (data["volume"] > self.config["min_volume"])
|
|
92
|
+
)
|
|
93
|
+
bullish = qualifies & (data["close"] > data["open"])
|
|
94
|
+
bearish = qualifies & (data["close"] <= data["open"])
|
|
95
|
+
|
|
96
|
+
out.loc[bullish, "signal"] = SCANNER_BUY
|
|
97
|
+
out.loc[bearish, "signal"] = SCANNER_SELL
|
|
98
|
+
out.loc[qualifies, "signal_strength"] = (
|
|
99
|
+
data.loc[qualifies, "volume_ratio"] * data.loc[qualifies, "price_change"]
|
|
100
|
+
)
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
def is_hit(self, returns: float, price_change: float) -> bool:
|
|
104
|
+
return price_change > self.config["price_change_threshold"]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""The example strategy: a moving-average crossover, the "hello world" of trend following.
|
|
2
|
+
|
|
3
|
+
**This ships so the demo has something to run and so there is a smallest complete
|
|
4
|
+
strategy to read. It is not an edge, and it is not where your work goes** — that belongs
|
|
5
|
+
in your own package. ``quantred init --example-pack ./my-signals`` copies a complete one
|
|
6
|
+
you own, with two strategies, a scanner and configs already wired up.
|
|
7
|
+
|
|
8
|
+
It reaches the engine through the ``quantred.strategies`` entry-point group declared in
|
|
9
|
+
this project's ``pyproject.toml`` — the same mechanism your pack will use, so the path
|
|
10
|
+
is exercised by every install rather than only in tests.
|
|
11
|
+
|
|
12
|
+
Long-only. Its conviction score is the **normalized EMA gap**
|
|
13
|
+
``(fast - slow) / slow``: positive (and rising) when the fast line leads, negative
|
|
14
|
+
when it lags. The base class derives the trade signal from the score's sign, so a
|
|
15
|
+
golden cross (score crossing above 0) becomes a ``BUY`` and a death cross (crossing
|
|
16
|
+
below 0) a ``CLOSE_BUY`` - the discrete behavior is a consequence of the score, not
|
|
17
|
+
a second code path. Deliberately simple and only five parameters, so it's an honest
|
|
18
|
+
baseline - and a clean example of how little it takes to add a strategy (one file,
|
|
19
|
+
a score, the indicators you already have, register the name).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import Any, ClassVar, Dict, Tuple
|
|
23
|
+
|
|
24
|
+
import pandas as pd
|
|
25
|
+
|
|
26
|
+
from quantred.indicators import indicators
|
|
27
|
+
from quantred.strategies.base import Strategy
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DemoTrendStrategy(Strategy):
|
|
31
|
+
"""Long-only EMA trend follower: buy the golden cross, exit the death cross."""
|
|
32
|
+
|
|
33
|
+
#: Marks this as a shipped demonstration rather than something to trade. Carried on
|
|
34
|
+
#: the class rather than inferred from where it was registered, because how a
|
|
35
|
+
#: strategy was discovered says nothing about what it is for - and after this moved
|
|
36
|
+
#: to an entry point, registry membership stopped being able to tell.
|
|
37
|
+
DEMO = True
|
|
38
|
+
|
|
39
|
+
#: Bars this strategy is designed to trade.
|
|
40
|
+
TIMEFRAME = "1Day"
|
|
41
|
+
|
|
42
|
+
PARAM_RANGES: ClassVar[Dict[str, Dict[str, Any]]] = {
|
|
43
|
+
"fast_ema_period": {
|
|
44
|
+
"type": "int",
|
|
45
|
+
"min": 5,
|
|
46
|
+
"max": 20,
|
|
47
|
+
"step": 1,
|
|
48
|
+
"default": 10,
|
|
49
|
+
"description": "Fast EMA period (the responsive line)",
|
|
50
|
+
},
|
|
51
|
+
"slow_ema_period": {
|
|
52
|
+
"type": "int",
|
|
53
|
+
"min": 21,
|
|
54
|
+
"max": 60,
|
|
55
|
+
"step": 1,
|
|
56
|
+
"default": 30,
|
|
57
|
+
"description": "Slow EMA period (the trend baseline)",
|
|
58
|
+
},
|
|
59
|
+
"risk_per_trade": {
|
|
60
|
+
"type": "float",
|
|
61
|
+
"min": 0.01,
|
|
62
|
+
"max": 0.05,
|
|
63
|
+
"step": 0.01,
|
|
64
|
+
"default": 0.02,
|
|
65
|
+
"description": "Capital fraction risked per trade",
|
|
66
|
+
},
|
|
67
|
+
"stop_loss": {
|
|
68
|
+
"type": "float",
|
|
69
|
+
"min": 0.01,
|
|
70
|
+
"max": 0.08,
|
|
71
|
+
"step": 0.01,
|
|
72
|
+
"default": 0.03,
|
|
73
|
+
"description": "Stop-loss distance from entry (fraction)",
|
|
74
|
+
},
|
|
75
|
+
"take_profit": {
|
|
76
|
+
"type": "float",
|
|
77
|
+
"min": 0.02,
|
|
78
|
+
"max": 0.15,
|
|
79
|
+
"step": 0.01,
|
|
80
|
+
"default": 0.06,
|
|
81
|
+
"description": "Take-profit distance from entry (fraction)",
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#: The fast line has to be faster than the slow one. Declared rather than checked
|
|
86
|
+
#: in ``initialize`` so a sampler can read it: today's ranges (5-20 against 21-60)
|
|
87
|
+
#: cannot produce a violation, but narrowed ranges can, and a combination that
|
|
88
|
+
#: gets evaluated is a journaled trial that raises the deflation bar for every
|
|
89
|
+
#: future candidate in this family.
|
|
90
|
+
PARAM_CONSTRAINTS: ClassVar[Tuple[Tuple[str, str, Any], ...]] = (
|
|
91
|
+
("fast_ema_period", "<", "slow_ema_period"),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def __init__(self, config: Dict[str, Any]):
|
|
95
|
+
config["timeframe"] = self.TIMEFRAME
|
|
96
|
+
config.setdefault(
|
|
97
|
+
"position_limits",
|
|
98
|
+
{"max_positions": 1, "max_position_size": 100_000.0, "max_total_risk": 0.05},
|
|
99
|
+
)
|
|
100
|
+
super().__init__(config)
|
|
101
|
+
|
|
102
|
+
def calculate_required_lookback(self) -> int:
|
|
103
|
+
return self.config["slow_ema_period"] + 1
|
|
104
|
+
|
|
105
|
+
def initialize(self) -> None:
|
|
106
|
+
# The fast/slow ordering is declared in PARAM_CONSTRAINTS and enforced at
|
|
107
|
+
# construction, so there is nothing left to restate here: a second copy of the
|
|
108
|
+
# comparison would be the half a sampler cannot read.
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def process_data(self, data: pd.DataFrame) -> pd.DataFrame:
|
|
112
|
+
if data.empty:
|
|
113
|
+
return pd.DataFrame()
|
|
114
|
+
|
|
115
|
+
enriched = data.copy()
|
|
116
|
+
enriched["fast_ema"] = indicators.calculate_ema(data["close"], self.config["fast_ema_period"])
|
|
117
|
+
enriched["slow_ema"] = indicators.calculate_ema(data["close"], self.config["slow_ema_period"])
|
|
118
|
+
return enriched
|
|
119
|
+
|
|
120
|
+
def calculate_scores(self, data: pd.DataFrame) -> pd.Series:
|
|
121
|
+
if data.empty:
|
|
122
|
+
return pd.Series(dtype=float)
|
|
123
|
+
# Normalized EMA gap: signed trend strength. Sign crossings are the golden /
|
|
124
|
+
# death crosses; magnitude is how decisively the fast line leads.
|
|
125
|
+
return (data["fast_ema"] - data["slow_ema"]) / data["slow_ema"]
|