aponyx 0.1.18__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.
- aponyx/__init__.py +14 -0
- aponyx/backtest/__init__.py +31 -0
- aponyx/backtest/adapters.py +77 -0
- aponyx/backtest/config.py +84 -0
- aponyx/backtest/engine.py +560 -0
- aponyx/backtest/protocols.py +101 -0
- aponyx/backtest/registry.py +334 -0
- aponyx/backtest/strategy_catalog.json +50 -0
- aponyx/cli/__init__.py +5 -0
- aponyx/cli/commands/__init__.py +8 -0
- aponyx/cli/commands/clean.py +349 -0
- aponyx/cli/commands/list.py +302 -0
- aponyx/cli/commands/report.py +167 -0
- aponyx/cli/commands/run.py +377 -0
- aponyx/cli/main.py +125 -0
- aponyx/config/__init__.py +82 -0
- aponyx/data/__init__.py +99 -0
- aponyx/data/bloomberg_config.py +306 -0
- aponyx/data/bloomberg_instruments.json +26 -0
- aponyx/data/bloomberg_securities.json +42 -0
- aponyx/data/cache.py +294 -0
- aponyx/data/fetch.py +659 -0
- aponyx/data/fetch_registry.py +135 -0
- aponyx/data/loaders.py +205 -0
- aponyx/data/providers/__init__.py +13 -0
- aponyx/data/providers/bloomberg.py +383 -0
- aponyx/data/providers/file.py +111 -0
- aponyx/data/registry.py +500 -0
- aponyx/data/requirements.py +96 -0
- aponyx/data/sample_data.py +415 -0
- aponyx/data/schemas.py +60 -0
- aponyx/data/sources.py +171 -0
- aponyx/data/synthetic_params.json +46 -0
- aponyx/data/transforms.py +336 -0
- aponyx/data/validation.py +308 -0
- aponyx/docs/__init__.py +24 -0
- aponyx/docs/adding_data_providers.md +682 -0
- aponyx/docs/cdx_knowledge_base.md +455 -0
- aponyx/docs/cdx_overlay_strategy.md +135 -0
- aponyx/docs/cli_guide.md +607 -0
- aponyx/docs/governance_design.md +551 -0
- aponyx/docs/logging_design.md +251 -0
- aponyx/docs/performance_evaluation_design.md +265 -0
- aponyx/docs/python_guidelines.md +786 -0
- aponyx/docs/signal_registry_usage.md +369 -0
- aponyx/docs/signal_suitability_design.md +558 -0
- aponyx/docs/visualization_design.md +277 -0
- aponyx/evaluation/__init__.py +11 -0
- aponyx/evaluation/performance/__init__.py +24 -0
- aponyx/evaluation/performance/adapters.py +109 -0
- aponyx/evaluation/performance/analyzer.py +384 -0
- aponyx/evaluation/performance/config.py +320 -0
- aponyx/evaluation/performance/decomposition.py +304 -0
- aponyx/evaluation/performance/metrics.py +761 -0
- aponyx/evaluation/performance/registry.py +327 -0
- aponyx/evaluation/performance/report.py +541 -0
- aponyx/evaluation/suitability/__init__.py +67 -0
- aponyx/evaluation/suitability/config.py +143 -0
- aponyx/evaluation/suitability/evaluator.py +389 -0
- aponyx/evaluation/suitability/registry.py +328 -0
- aponyx/evaluation/suitability/report.py +398 -0
- aponyx/evaluation/suitability/scoring.py +367 -0
- aponyx/evaluation/suitability/tests.py +303 -0
- aponyx/examples/01_generate_synthetic_data.py +53 -0
- aponyx/examples/02_fetch_data_file.py +82 -0
- aponyx/examples/03_fetch_data_bloomberg.py +104 -0
- aponyx/examples/04_compute_signal.py +164 -0
- aponyx/examples/05_evaluate_suitability.py +224 -0
- aponyx/examples/06_run_backtest.py +242 -0
- aponyx/examples/07_analyze_performance.py +214 -0
- aponyx/examples/08_visualize_results.py +272 -0
- aponyx/main.py +7 -0
- aponyx/models/__init__.py +45 -0
- aponyx/models/config.py +83 -0
- aponyx/models/indicator_transformation.json +52 -0
- aponyx/models/indicators.py +292 -0
- aponyx/models/metadata.py +447 -0
- aponyx/models/orchestrator.py +213 -0
- aponyx/models/registry.py +860 -0
- aponyx/models/score_transformation.json +42 -0
- aponyx/models/signal_catalog.json +29 -0
- aponyx/models/signal_composer.py +513 -0
- aponyx/models/signal_transformation.json +29 -0
- aponyx/persistence/__init__.py +16 -0
- aponyx/persistence/json_io.py +132 -0
- aponyx/persistence/parquet_io.py +378 -0
- aponyx/py.typed +0 -0
- aponyx/reporting/__init__.py +10 -0
- aponyx/reporting/generator.py +517 -0
- aponyx/visualization/__init__.py +20 -0
- aponyx/visualization/app.py +37 -0
- aponyx/visualization/plots.py +309 -0
- aponyx/visualization/visualizer.py +242 -0
- aponyx/workflows/__init__.py +18 -0
- aponyx/workflows/concrete_steps.py +720 -0
- aponyx/workflows/config.py +122 -0
- aponyx/workflows/engine.py +279 -0
- aponyx/workflows/registry.py +116 -0
- aponyx/workflows/steps.py +180 -0
- aponyx-0.1.18.dist-info/METADATA +552 -0
- aponyx-0.1.18.dist-info/RECORD +104 -0
- aponyx-0.1.18.dist-info/WHEEL +4 -0
- aponyx-0.1.18.dist-info/entry_points.txt +2 -0
- aponyx-0.1.18.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
````markdown
|
|
2
|
+
# Signal Registry Pattern - Usage Guide
|
|
3
|
+
|
|
4
|
+
## Overview
|
|
5
|
+
|
|
6
|
+
The signal registry infrastructure enables scalable signal research through a **four-stage transformation pipeline**:
|
|
7
|
+
1. **Indicator Transformations** — Raw economic metrics from market data
|
|
8
|
+
2. **Score Transformations** — Normalization (z-score, volatility adjustment)
|
|
9
|
+
3. **Signal Transformations** — Trading rules (bounds, neutral zones)
|
|
10
|
+
4. **Signals** — Composed from all three transformation stages
|
|
11
|
+
|
|
12
|
+
Add new signals by editing JSON catalogs instead of modifying code. Each signal is evaluated independently to establish clear performance attribution.
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Basic Usage
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from aponyx.models import (
|
|
20
|
+
IndicatorTransformationRegistry,
|
|
21
|
+
ScoreTransformationRegistry,
|
|
22
|
+
SignalTransformationRegistry,
|
|
23
|
+
SignalRegistry,
|
|
24
|
+
)
|
|
25
|
+
from aponyx.models.signal_composer import compose_signal
|
|
26
|
+
from aponyx.config import (
|
|
27
|
+
INDICATOR_TRANSFORMATION_PATH,
|
|
28
|
+
SCORE_TRANSFORMATION_PATH,
|
|
29
|
+
SIGNAL_TRANSFORMATION_PATH,
|
|
30
|
+
SIGNAL_CATALOG_PATH,
|
|
31
|
+
)
|
|
32
|
+
from aponyx.backtest import run_backtest, BacktestConfig
|
|
33
|
+
|
|
34
|
+
# Load all four registries
|
|
35
|
+
indicator_reg = IndicatorTransformationRegistry(INDICATOR_TRANSFORMATION_PATH)
|
|
36
|
+
score_reg = ScoreTransformationRegistry(SCORE_TRANSFORMATION_PATH)
|
|
37
|
+
signal_trans_reg = SignalTransformationRegistry(SIGNAL_TRANSFORMATION_PATH)
|
|
38
|
+
signal_reg = SignalRegistry(SIGNAL_CATALOG_PATH)
|
|
39
|
+
|
|
40
|
+
# Prepare market data
|
|
41
|
+
market_data = {
|
|
42
|
+
"cdx": cdx_df, # Must have 'spread' column
|
|
43
|
+
"vix": vix_df, # Must have 'level' column
|
|
44
|
+
"etf": etf_df, # Must have 'spread' column
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Compose a signal (four-stage pipeline)
|
|
48
|
+
signal = compose_signal(
|
|
49
|
+
signal_name="cdx_etf_basis",
|
|
50
|
+
market_data=market_data,
|
|
51
|
+
indicator_registry=indicator_reg,
|
|
52
|
+
score_registry=score_reg,
|
|
53
|
+
signal_transformation_registry=signal_trans_reg,
|
|
54
|
+
signal_registry=signal_reg,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Run backtest
|
|
58
|
+
config = BacktestConfig(position_size_mm=10.0)
|
|
59
|
+
result = run_backtest(signal, cdx_df["spread"], config)
|
|
60
|
+
print(f"Sharpe Ratio: {result.metrics['sharpe_ratio']:.2f}")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Adding a New Signal
|
|
64
|
+
|
|
65
|
+
### Step 1: Create Indicator Function (if needed)
|
|
66
|
+
|
|
67
|
+
Add to `src/aponyx/models/indicators.py`:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
def compute_my_new_indicator(
|
|
71
|
+
cdx_df: pd.DataFrame,
|
|
72
|
+
other_df: pd.DataFrame,
|
|
73
|
+
) -> pd.Series:
|
|
74
|
+
"""
|
|
75
|
+
Compute my new indicator in basis points.
|
|
76
|
+
|
|
77
|
+
Outputs economically interpretable values WITHOUT normalization.
|
|
78
|
+
Score transformations are applied at signal composition layer.
|
|
79
|
+
"""
|
|
80
|
+
# Return raw values in natural units (bps, ratio, etc.)
|
|
81
|
+
return cdx_df["spread"] - other_df["spread"]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Step 2: Register Indicator Transformation
|
|
85
|
+
|
|
86
|
+
Edit `src/aponyx/models/indicator_transformation.json`:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"name": "my_new_indicator",
|
|
91
|
+
"description": "CDX-other spread differential in basis points",
|
|
92
|
+
"compute_function_name": "compute_my_new_indicator",
|
|
93
|
+
"data_requirements": {
|
|
94
|
+
"cdx": "spread",
|
|
95
|
+
"other": "spread"
|
|
96
|
+
},
|
|
97
|
+
"default_securities": {
|
|
98
|
+
"cdx": "cdx_ig_5y",
|
|
99
|
+
"other": "other_security"
|
|
100
|
+
},
|
|
101
|
+
"output_units": "basis_points",
|
|
102
|
+
"parameters": {},
|
|
103
|
+
"enabled": true
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Step 3: Define Signal (referencing all three transformations)
|
|
108
|
+
|
|
109
|
+
Edit `src/aponyx/models/signal_catalog.json`:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"name": "my_new_signal",
|
|
114
|
+
"description": "Trading signal based on my indicator",
|
|
115
|
+
"indicator_transformation": "my_new_indicator",
|
|
116
|
+
"score_transformation": "z_score_20d",
|
|
117
|
+
"signal_transformation": "passthrough",
|
|
118
|
+
"enabled": true,
|
|
119
|
+
"sign_multiplier": 1
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Step 4: Use the Signal
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# Registry automatically picks up the new signal
|
|
127
|
+
signal = compose_signal(
|
|
128
|
+
signal_name="my_new_signal",
|
|
129
|
+
market_data=market_data,
|
|
130
|
+
indicator_registry=indicator_reg,
|
|
131
|
+
score_registry=score_reg,
|
|
132
|
+
signal_transformation_registry=signal_trans_reg,
|
|
133
|
+
signal_registry=signal_reg,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Run backtest to evaluate performance
|
|
137
|
+
config = BacktestConfig(position_size_mm=10.0)
|
|
138
|
+
result = run_backtest(signal, cdx_df["spread"], config)
|
|
139
|
+
print(f"Sharpe Ratio: {result.metrics['sharpe_ratio']:.2f}")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Four-Stage Transformation Pipeline
|
|
143
|
+
|
|
144
|
+
Every signal is composed through four stages:
|
|
145
|
+
|
|
146
|
+
| Stage | Catalog | Purpose | Example |
|
|
147
|
+
|-------|---------|---------|---------|
|
|
148
|
+
| **Indicator** | `indicator_transformation.json` | Compute raw economic metric | Spread difference (bps) |
|
|
149
|
+
| **Score** | `score_transformation.json` | Normalize to common scale | Z-score over 20 days |
|
|
150
|
+
| **Signal** | `signal_transformation.json` | Apply trading rules | Bounds [-1.5, 1.5], neutral zone |
|
|
151
|
+
| **Composition** | `signal_catalog.json` | Reference all three | Links stages together |
|
|
152
|
+
|
|
153
|
+
### Transformation Catalog Files
|
|
154
|
+
|
|
155
|
+
**indicator_transformation.json** — Raw metrics:
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"name": "cdx_etf_spread_diff",
|
|
159
|
+
"compute_function_name": "compute_cdx_etf_spread_diff",
|
|
160
|
+
"data_requirements": {"cdx": "spread", "etf": "spread"},
|
|
161
|
+
"output_units": "basis_points"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**score_transformation.json** — Normalization:
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"name": "z_score_20d",
|
|
169
|
+
"transform_type": "z_score",
|
|
170
|
+
"parameters": {"window": 20, "min_periods": 10}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**signal_transformation.json** — Trading rules:
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"name": "bounded_1_5",
|
|
178
|
+
"scaling": 1.0,
|
|
179
|
+
"floor": -1.5,
|
|
180
|
+
"cap": 1.5,
|
|
181
|
+
"neutral_range": [-0.25, 0.25]
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**signal_catalog.json** — Composition:
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"name": "cdx_etf_basis",
|
|
189
|
+
"indicator_transformation": "cdx_etf_spread_diff",
|
|
190
|
+
"score_transformation": "z_score_20d",
|
|
191
|
+
"signal_transformation": "passthrough",
|
|
192
|
+
"sign_multiplier": 1
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Signal Catalog Schema
|
|
197
|
+
|
|
198
|
+
### SignalMetadata Fields
|
|
199
|
+
|
|
200
|
+
| Field | Type | Description |
|
|
201
|
+
|-------|------|-------------|
|
|
202
|
+
| `name` | string | Unique signal identifier (snake_case) |
|
|
203
|
+
| `description` | string | Human-readable signal description |
|
|
204
|
+
| `indicator_transformation` | string | Reference to indicator_transformation.json |
|
|
205
|
+
| `score_transformation` | string | Reference to score_transformation.json |
|
|
206
|
+
| `signal_transformation` | string | Reference to signal_transformation.json |
|
|
207
|
+
| `enabled` | boolean | Whether to compute this signal |
|
|
208
|
+
| `sign_multiplier` | int | Sign adjustment (1 or -1) |
|
|
209
|
+
|
|
210
|
+
## Integration with Backtesting
|
|
211
|
+
|
|
212
|
+
The backtest layer accepts any signal series. Current `BacktestConfig` parameters:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from aponyx.backtest import BacktestConfig, run_backtest
|
|
216
|
+
|
|
217
|
+
# Backtest configuration
|
|
218
|
+
config = BacktestConfig(
|
|
219
|
+
position_size_mm=10.0, # Notional in millions
|
|
220
|
+
sizing_mode="proportional", # 'binary' or 'proportional' (default)
|
|
221
|
+
stop_loss_pct=5.0, # Optional: stop at 5% loss
|
|
222
|
+
take_profit_pct=10.0, # Optional: take profit at 10%
|
|
223
|
+
max_holding_days=None, # Optional: max holding period
|
|
224
|
+
transaction_cost_bps=1.0, # Round-trip cost in bps
|
|
225
|
+
dv01_per_million=475.0, # DV01 for risk calculations ($475 per $1MM)
|
|
226
|
+
signal_lag=1, # Days to lag signal (prevent look-ahead)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
result = run_backtest(signal, cdx_df["spread"], config)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Position Sizing Modes
|
|
233
|
+
|
|
234
|
+
**Proportional (default):** Position scales with signal magnitude
|
|
235
|
+
```python
|
|
236
|
+
config = BacktestConfig(
|
|
237
|
+
position_size_mm=10.0,
|
|
238
|
+
sizing_mode="proportional", # Default: position = signal × 10MM
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Binary:** Full position for any non-zero signal
|
|
243
|
+
```python
|
|
244
|
+
config = BacktestConfig(
|
|
245
|
+
position_size_mm=10.0,
|
|
246
|
+
sizing_mode="binary", # Position = ±10MM regardless of signal magnitude
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Runtime Overrides
|
|
251
|
+
|
|
252
|
+
Override transformation stages at compose time:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
# Use 60-day z-score instead of default 20-day
|
|
256
|
+
signal = compose_signal(
|
|
257
|
+
signal_name="cdx_etf_basis",
|
|
258
|
+
market_data=market_data,
|
|
259
|
+
indicator_registry=indicator_reg,
|
|
260
|
+
score_registry=score_reg,
|
|
261
|
+
signal_transformation_registry=signal_trans_reg,
|
|
262
|
+
signal_registry=signal_reg,
|
|
263
|
+
score_transformation_override="z_score_60d",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# With intermediate stage inspection
|
|
267
|
+
result = compose_signal(
|
|
268
|
+
signal_name="cdx_etf_basis",
|
|
269
|
+
market_data=market_data,
|
|
270
|
+
indicator_registry=indicator_reg,
|
|
271
|
+
score_registry=score_reg,
|
|
272
|
+
signal_transformation_registry=signal_trans_reg,
|
|
273
|
+
signal_registry=signal_reg,
|
|
274
|
+
include_intermediates=True,
|
|
275
|
+
)
|
|
276
|
+
print(result["indicator"].tail()) # Raw indicator
|
|
277
|
+
print(result["score"].tail()) # Normalized score
|
|
278
|
+
print(result["signal"].tail()) # Final signal
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Using Strategy Registry
|
|
282
|
+
|
|
283
|
+
Load strategy configurations from catalog:
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from aponyx.backtest import StrategyRegistry
|
|
287
|
+
from aponyx.config import STRATEGY_CATALOG_PATH
|
|
288
|
+
|
|
289
|
+
# Load strategies from catalog
|
|
290
|
+
strategy_reg = StrategyRegistry(STRATEGY_CATALOG_PATH)
|
|
291
|
+
|
|
292
|
+
# Get strategy metadata and convert to config
|
|
293
|
+
metadata = strategy_reg.get_metadata("balanced")
|
|
294
|
+
config = metadata.to_config()
|
|
295
|
+
|
|
296
|
+
# Override specific parameters
|
|
297
|
+
config = metadata.to_config(
|
|
298
|
+
position_size_mm_override=20.0, # Use 20MM instead of catalog default
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
result = run_backtest(signal, cdx_df["spread"], config)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Best Practices
|
|
305
|
+
|
|
306
|
+
1. **Follow four-stage pipeline** — All signals use compose_signal() (no exceptions)
|
|
307
|
+
2. **Follow signal convention** — Positive = long credit risk (buy CDX)
|
|
308
|
+
3. **Log operations** using module-level logger with %-formatting
|
|
309
|
+
4. **Validate data requirements** are met before computing
|
|
310
|
+
5. **Include signal description** in catalog for documentation
|
|
311
|
+
6. **Test determinism** to ensure reproducible results
|
|
312
|
+
7. **Use runtime overrides** for experimentation without modifying catalogs
|
|
313
|
+
|
|
314
|
+
## Debugging Signals
|
|
315
|
+
|
|
316
|
+
### Enable Debug Logging
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
import logging
|
|
320
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
321
|
+
|
|
322
|
+
# Signal computations will log details:
|
|
323
|
+
# DEBUG - Computing indicator: cdx_etf_spread_diff
|
|
324
|
+
# DEBUG - Applying score transformation: z_score_20d
|
|
325
|
+
# DEBUG - Applying signal transformation: passthrough
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Inspect Intermediate Values
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
result = compose_signal(
|
|
332
|
+
signal_name="cdx_etf_basis",
|
|
333
|
+
market_data=market_data,
|
|
334
|
+
indicator_registry=indicator_reg,
|
|
335
|
+
score_registry=score_reg,
|
|
336
|
+
signal_transformation_registry=signal_trans_reg,
|
|
337
|
+
signal_registry=signal_reg,
|
|
338
|
+
include_intermediates=True,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
print("Indicator (raw bps):", result["indicator"].describe())
|
|
342
|
+
print("Score (normalized):", result["score"].describe())
|
|
343
|
+
print("Signal (final):", result["signal"].describe())
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Common Issues
|
|
347
|
+
|
|
348
|
+
**Signal returns all NaN values:**
|
|
349
|
+
```python
|
|
350
|
+
# Check data alignment
|
|
351
|
+
print(f"CDX: {cdx_df.index.min()} to {cdx_df.index.max()}")
|
|
352
|
+
print(f"ETF: {etf_df.index.min()} to {etf_df.index.max()}")
|
|
353
|
+
|
|
354
|
+
# Ensure indices overlap
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Signal not found in registry:**
|
|
358
|
+
```python
|
|
359
|
+
# List all enabled signals
|
|
360
|
+
enabled = signal_reg.get_enabled()
|
|
361
|
+
print(f"Enabled signals: {list(enabled.keys())}")
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
**Maintained by:** stabilefrisur
|
|
367
|
+
**Last Updated:** December 13, 2025
|
|
368
|
+
|
|
369
|
+
````
|