diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..929f81b
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+layout uv
diff --git a/aapl_yfinance.md b/aapl_yfinance.md
new file mode 100644
index 0000000..9e3974f
--- /dev/null
+++ b/aapl_yfinance.md
@@ -0,0 +1,119 @@
+# AAPL - All yfinance Data
+
+```python
+import yfinance as yf
+import marimo as mo
+
+ticker = yf.Ticker("AAPL")
+```
+
+## Price History
+
+```python
+history = ticker.history(period="1y", interval="1d")
+mo.ui.table(history.tail(30))
+```
+
+## Dividends
+
+```python
+mo.ui.table(ticker.dividends.tail(20))
+```
+
+## Splits
+
+```python
+mo.ui.table(ticker.splits)
+```
+
+## Income Statement
+
+```python
+mo.ui.table(ticker.get_income_stmt())
+```
+
+## Balance Sheet
+
+```python
+mo.ui.table(ticker.get_balance_sheet())
+```
+
+## Cash Flow
+
+```python
+mo.ui.table(ticker.get_cashflow())
+```
+
+## Analyst Recommendations
+
+```python
+mo.ui.table(ticker.recommendations.tail(20))
+```
+
+## Analyst Target Prices
+
+```python
+mo.ui.table([ticker.target_prices])
+```
+
+## Earnings Dates
+
+```python
+mo.ui.table(ticker.earnings_dates)
+```
+
+## Options Expirations
+
+```python
+expirations = ticker.get_options()
+f"Available: {expirations}"
+```
+
+```python
+if expirations:
+ chain = ticker.option_chain(expirations[0])
+ mo.ui.table(chain.calls)
+ mo.ui.table(chain.puts)
+```
+
+## Major Holders
+
+```python
+mo.ui.table(ticker.major_holders)
+```
+
+## Institutional Holders
+
+```python
+mo.ui.table(ticker.institutional_holders)
+```
+
+## Sustainability / ESG
+
+```python
+mo.ui.table([ticker.sustainability])
+```
+
+## Calendar / Events
+
+```python
+mo.ui.table(ticker.calendar)
+```
+
+## Company Info Summary
+
+```python
+info = ticker.info
+{
+ "name": info.get("longName"),
+ "sector": info.get("sector"),
+ "industry": info.get("industry"),
+ "marketCap": info.get("marketCap"),
+ "peRatio": info.get("trailingPE"),
+ "fwdDividend": info.get("dividendYield"),
+ "52wkHigh": info.get("fiftyTwoWeekHigh"),
+ "52wkLow": info.get("fiftyTwoWeekLow"),
+ "avgTarget": info.get("targetMeanPrice"),
+ "recommendation": info.get("recommendationKey"),
+}
+```
diff --git a/aapl_yfinance.py b/aapl_yfinance.py
new file mode 100644
index 0000000..1f54839
--- /dev/null
+++ b/aapl_yfinance.py
@@ -0,0 +1,279 @@
+import marimo
+
+__generated_with = "0.23.2"
+app = marimo.App(width="full")
+
+
+@app.cell
+def _():
+ import yfinance as yf
+ import marimo as mo
+ ticker = yf.Ticker("AAPL")
+ return mo, ticker
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ # AAPL — All Data via yfinance
+ """)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Price History
+ """)
+ return
+
+
+@app.cell
+def _(ticker):
+ history = ticker.history(period="max", interval="1d")
+ history
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Info
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ info = ticker.info
+ mo.ui.table([info])
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Dividends
+ """)
+ return
+
+
+@app.cell
+def _(ticker):
+ dividends = ticker.dividends
+ dividends
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Splits
+ """)
+ return
+
+
+@app.cell
+def _(ticker):
+ splits = ticker.splits
+ splits
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Income Statement
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ inc = ticker.get_income_stmt()
+ mo.ui.table(inc)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Balance Sheet
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ bal = ticker.get_balance_sheet()
+ mo.ui.table(bal)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Cash Flow
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ cf = ticker.get_cashflow()
+ mo.ui.table(cf)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Recommendations
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ recs = ticker.recommendations
+ mo.ui.table(recs)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Analyst Target Prices
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ target = ticker.target_prices
+ mo.ui.table([target] if isinstance(target, dict) else target)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Earnings Dates
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ earn_dates = ticker.earnings_dates
+ mo.ui.table(earn_dates)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Options
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ expirations = ticker.options
+ mo.md(f"Available expirations: {expirations}")
+ return (expirations,)
+
+
+@app.cell
+def _(expirations, mo, ticker):
+ sections = []
+ for exp in expirations:
+ chain = ticker.option_chain(exp)
+ sections += [
+ mo.md(f"### {exp} — Calls"),
+ mo.ui.table(chain.calls),
+ mo.md(f"### {exp} — Puts"),
+ mo.ui.table(chain.puts),
+ ]
+ mo.vstack(sections) if sections else None
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Major Holders
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ major = ticker.major_holders
+ mo.ui.table(major)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Institutional Holders
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ inst = ticker.institutional_holders
+ mo.ui.table(inst)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Mutual Fund Holders
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ mf = ticker.mutualfund_holders
+ mo.ui.table(mf)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Sustainability / ESG
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ sus = ticker.sustainability
+ mo.ui.table([sus] if sus is not None else [{"data": "No ESG data available"}])
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md("""
+ ## Calendar / Events
+ """)
+ return
+
+
+@app.cell
+def _(mo, ticker):
+ cal = ticker.calendar
+ mo.ui.table([cal] if isinstance(cal, dict) else cal)
+ return
+
+
+if __name__ == "__main__":
+ app.run()
diff --git a/defeatbeta_tutorial.ipynb b/defeatbeta_tutorial.ipynb
index 0415d39..d991c2a 100644
--- a/defeatbeta_tutorial.ipynb
+++ b/defeatbeta_tutorial.ipynb
@@ -44,6 +44,7 @@
"|___/ \\___|_| \\___|\\__,_|\\__| \\____/ \\___|\\__\\__,_|\u001b[0m\n",
"\u001b[1;38;5;10m📈:: Data Update Time ::\u001b[0m\t2026-04-17 \u001b[1;38;5;10m::\u001b[0m\n",
"\u001b[1;38;5;10m📈:: Software Version ::\u001b[0m\t0.0.45 \u001b[1;38;5;10m::\u001b[0m\n",
+ "[persistent_cache] cache → /home/df/.cache/defeatbeta\n",
"✅ yfinance is installed\n",
"✅ DefeatBeta-API imported successfully\n",
"Python version: 3.12.12\n"
@@ -59,6 +60,8 @@
"\n",
"# DefeatBeta-API\n",
"from defeatbeta_api.data.ticker import Ticker\n",
+ "from persistent_cache import enable_persistent_cache\n",
+ "enable_persistent_cache()\n",
"\n",
"# Yahoo Finance (for comparison)\n",
"try:\n",
@@ -84,7 +87,7 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": 5,
"metadata": {},
"outputs": [
{
@@ -94,12 +97,11 @@
"============================================================\n",
"PERFORMANCE COMPARISON: Fetching Price Data\n",
"============================================================\n",
- "2026-04-25 17:43:11 INFO DuckDBClient MainThread - Cache is up-to-date. Update time: 2026-04-17\n",
"\n",
- "✅ DefeatBeta: 0.617s\n",
+ "✅ DefeatBeta: 0.019s\n",
" Data shape: (7897, 7)\n",
"\n",
- "✅ Yahoo Finance: 0.493s\n",
+ "✅ Yahoo Finance: 0.276s\n",
" Data shape: (11433, 7)\n"
]
}
@@ -141,7 +143,7 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 6,
"metadata": {},
"outputs": [
{
@@ -531,7 +533,7 @@
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 7,
"metadata": {},
"outputs": [
{
@@ -841,7 +843,7 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 8,
"metadata": {},
"outputs": [
{
@@ -887,7 +889,7 @@
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": 9,
"metadata": {},
"outputs": [
{
@@ -956,7 +958,7 @@
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": 10,
"metadata": {},
"outputs": [
{
@@ -3416,6 +3418,1142 @@
"dir(yft)"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import vectorbt as vbt\n",
+ "data = Ticker(\"AAPL\")\n",
+ "price = data.price().close"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fast_ma = vbt.MA.run(price, 10)\n",
+ "slow_ma = vbt.MA.run(price, 50)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "entries = fast_ma.ma_crossed_above(slow_ma)\n",
+ "exits = slow_ma.ma_crossed_above(fast_ma)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "pf = vbt.Portfolio.from_signals(price, entries, exits, init_cash=100)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "np.float64(24134.667890761524)"
+ ]
+ },
+ "execution_count": 30,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "pf.total_profit()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 37,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df = data.price()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 38,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Move the 'report_date' column into the Index position\n",
+ "df['report_date'] = pd.to_datetime(df['report_date'])\n",
+ "df = df.set_index('report_date')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 54,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/home/df/scratch/trading/learn-trading/.venv/lib/python3.12/site-packages/vectorbt/data/base.py:535: UserWarning: Symbols have mismatching index. Dropping missing data points.\n",
+ " data = cls.align_index(data, missing=missing_index)\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.plotly.v1+json": {
+ "config": {
+ "plotlyServerURL": "https://plot.ly"
+ },
+ "data": [
+ {
+ "mode": "markers",
+ "name": "BTC-USD",
+ "showlegend": true,
+ "type": "scatter",
+ "uid": "67aa9626-ac1b-42c0-a099-a328b49dec1c",
+ "x": {
+ "bdata": "CgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZA==",
+ "dtype": "i1"
+ },
+ "y": {
+ "bdata": "mK+UtOtqP0CuriPoT8szQIPwAEludVRA0xWeEGcPR0B7ZgSGdPw8QC6BJHCzoz5ANwxYwoLmOUCsGGPJO0Q8QNValYvRsSZAU19+kQcFKkAzPoxaHCYuQEinlOf0CSlAjbz+GDw5O0BK7rBdneM4QIHFzYytHT9A+w7yM2w4M0B+OVaaQRwgQIS+XEoBFidAe6qXi4NZBEAyQnWGlsA4QKBGRSUNyh9An3CYHWZKJUC8sFrwKB8VQAXSlAMHdShAdiQSlDDxIUDF2eoSmZcnQJnJg7UhsiFAl9wziuibKkDPoAU6Q/oUQEm0ZSOE8CNAy0HQuZ47KUA38jMaMjYgQKbzz73VjyBAT7ndADplHkBeC1GBgvMkQGgEOYUWJBJAy4X1+AmpBUDVLkwee0EKQAbEb4ovHBFAbNSj/R1dEkAHk0TJ2hsXQNAmwCtLbBNAXUV6z2ruG0D4q1QZxQAVQDj53fEekSNATxiMWbMzDkBL9m2Z4uYfQETRdEADOyBAt9NxUoFsGUDHBMVOsgYWQDEG466+QghAF8bkh6+FI0AoIcHtJP0XQBwjLe8nWiZAGWUkVwPpG0DNWALOhU8LQCJq4VT4kRFAcvK7f4qzD0CJo2FHcVQwQDkzpILCVxJA2wJW/+n7IkCVVjlTvDYjQNMN3ShU0gtAxrkHlw2ZIUCo9aZU7tUQQI3AimKP5xRAVpySzUt2CUB1KxSOX6QjQOxSkltS+gRAMHDw5jgbHUBhugiL8IYTQF+M/wYV6RVAi3I+XeMyA0CCVDJB2csSQMvj3gsVaRdA4XU5epM3A0D5beo8/BccQPXXbHs9pSVAr0MFgzSsGkA6tPc38MkZQNeWAqmwvQdA483JLopcBUB2nojO+S0CQOFGF2UKsBJA+e6G3NQkDUCM7oGijaQWQPr1I5x3NhJAHNBghB8JJUCGWEGmGaEOQIOisDRH+wZAVSGYKt1f/z8=",
+ "dtype": "f8"
+ }
+ },
+ {
+ "mode": "markers",
+ "name": "ETH-USD",
+ "showlegend": true,
+ "type": "scatter",
+ "uid": "7748b161-109f-4585-95c5-260ab9ff8b64",
+ "x": {
+ "bdata": "CgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZA==",
+ "dtype": "i1"
+ },
+ "y": {
+ "bdata": "6iVfN8g8M0BCy/opnOZNQMSfb5TRlThAGWkY3uj2VEC042UXuo5HQGPDYY010/m/t+TUlO2/SkDRDD1Kh14SQBEiVhtL2URAkbm/lI0/CUC1MPC1d18jQD2HRQgqCypAp12SkVInIkAWIztTc245QKp/xnTX8U1AHSSJJV7rPkCBLpn23DsgQKdGI6LddTFA3DKwD9tk8j9S8wGzM6cwQEuuJcjfxD1A0mjJQmqTLEA6WWWAge4fQJRIlmfGZfU/Avbcy0WKIUAu0c7vfw8jQP14+wJQriZA18AKYXYOQ0DASrWG3fYbQBBhBRRgpfc/Z4qw3g/GI0AM93GHPcYRQCUsgMzydhJAfX4LSG7NJ0BeYs0EDqsxQIAuJDbW3C1A8/3U698vFEAMAosDAUg1QAuAbLXH2SdAh8/znL5wE0D/sYa5o9QGQKlhGfoKCCNAS3jaJsCkEkDQ00OcmbkZQKHxMlaRIxdAszYGx4IqPUAd5+tZCBogQKw/DcbC9xVA1H8GTF/JAUAe5clNIY4UQGGsxRyEzRBAphJvh8nJF0CXA/6ZGJgPQJnACzqdjB9AUmfugSOHBkA88O4HSTvjP4TJuQspoRZAcX2qUYD5QEBboZSl8GklQCC0nA8ASzhACDtmHTO2E0DVriQW7ocnQMc6UioPxh5A+4+oINKOJECKDQ+0I9cxQPc/M/zISjBAWpO8SRHAF0AzypxrpckUQIenNJ+usAFAGnfiuOn7HECALvrCi74LQGMwEGiD7gdATyDCmm9+IEDxW24DXC8gQNujJ8m1/RVAlQRk1eMc4T/sZpSOl58JQKUb4ria4wlAUgblBtzvC0AQ0oG8fKIHQCLPC9E3lRBAFHXhUbemG0Dt1qDrPi0HQOChYlK0HhFAcI8bE35YBEDsiy0swLH+P8v+Qv+QABFAVZtlZ/1VG0CxT3ljTE0aQJ/OaOajJPg/Cb5sKRH4HUA=",
+ "dtype": "f8"
+ }
+ }
+ ],
+ "layout": {
+ "height": 350,
+ "legend": {
+ "orientation": "h",
+ "traceorder": "normal",
+ "x": 1,
+ "xanchor": "right",
+ "y": 1.02,
+ "yanchor": "bottom"
+ },
+ "margin": {
+ "b": 30,
+ "l": 30,
+ "r": 30,
+ "t": 30
+ },
+ "template": {
+ "data": {
+ "bar": [
+ {
+ "error_x": {
+ "color": "#2a3f5f"
+ },
+ "error_y": {
+ "color": "#2a3f5f"
+ },
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ }
+ },
+ "type": "bar"
+ }
+ ],
+ "barpolar": [
+ {
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ }
+ },
+ "type": "barpolar"
+ }
+ ],
+ "carpet": [
+ {
+ "aaxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "baxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "type": "carpet"
+ }
+ ],
+ "choropleth": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "choropleth"
+ }
+ ],
+ "contour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "contour"
+ }
+ ],
+ "contourcarpet": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "contourcarpet"
+ }
+ ],
+ "heatmap": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "heatmap"
+ }
+ ],
+ "histogram": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "histogram"
+ }
+ ],
+ "histogram2d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2d"
+ }
+ ],
+ "histogram2dcontour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2dcontour"
+ }
+ ],
+ "mesh3d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "mesh3d"
+ }
+ ],
+ "parcoords": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "parcoords"
+ }
+ ],
+ "pie": [
+ {
+ "automargin": true,
+ "type": "pie"
+ }
+ ],
+ "scatter": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatter"
+ }
+ ],
+ "scatter3d": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatter3d"
+ }
+ ],
+ "scattercarpet": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattercarpet"
+ }
+ ],
+ "scattergeo": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergeo"
+ }
+ ],
+ "scattergl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergl"
+ }
+ ],
+ "scattermapbox": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermapbox"
+ }
+ ],
+ "scatterpolar": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolar"
+ }
+ ],
+ "scatterpolargl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolargl"
+ }
+ ],
+ "scatterternary": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterternary"
+ }
+ ],
+ "surface": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "surface"
+ }
+ ],
+ "table": [
+ {
+ "cells": {
+ "fill": {
+ "color": "#EBF0F8"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "header": {
+ "fill": {
+ "color": "#C8D4E3"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "type": "table"
+ }
+ ]
+ },
+ "layout": {
+ "annotationdefaults": {
+ "arrowcolor": "#2a3f5f",
+ "arrowhead": 0,
+ "arrowwidth": 1
+ },
+ "coloraxis": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "colorscale": {
+ "diverging": [
+ [
+ 0,
+ "#8e0152"
+ ],
+ [
+ 0.1,
+ "#c51b7d"
+ ],
+ [
+ 0.2,
+ "#de77ae"
+ ],
+ [
+ 0.3,
+ "#f1b6da"
+ ],
+ [
+ 0.4,
+ "#fde0ef"
+ ],
+ [
+ 0.5,
+ "#f7f7f7"
+ ],
+ [
+ 0.6,
+ "#e6f5d0"
+ ],
+ [
+ 0.7,
+ "#b8e186"
+ ],
+ [
+ 0.8,
+ "#7fbc41"
+ ],
+ [
+ 0.9,
+ "#4d9221"
+ ],
+ [
+ 1,
+ "#276419"
+ ]
+ ],
+ "sequential": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "sequentialminus": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ]
+ },
+ "colorway": [
+ "#1f77b4",
+ "#ff7f0e",
+ "#2ca02c",
+ "#dc3912",
+ "#9467bd",
+ "#8c564b",
+ "#e377c2",
+ "#7f7f7f",
+ "#bcbd22",
+ "#17becf"
+ ],
+ "font": {
+ "color": "#2a3f5f"
+ },
+ "geo": {
+ "bgcolor": "white",
+ "lakecolor": "white",
+ "landcolor": "#E5ECF6",
+ "showlakes": true,
+ "showland": true,
+ "subunitcolor": "white"
+ },
+ "hoverlabel": {
+ "align": "left"
+ },
+ "hovermode": "closest",
+ "mapbox": {
+ "style": "light"
+ },
+ "paper_bgcolor": "white",
+ "plot_bgcolor": "#E5ECF6",
+ "polar": {
+ "angularaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "radialaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "scene": {
+ "xaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "yaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "zaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ }
+ },
+ "shapedefaults": {
+ "line": {
+ "color": "#2a3f5f"
+ }
+ },
+ "ternary": {
+ "aaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "baxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "caxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "title": {
+ "x": 0.05
+ },
+ "xaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ },
+ "yaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ }
+ }
+ },
+ "width": 700,
+ "xaxis": {
+ "title": {
+ "text": "randnx_n"
+ }
+ },
+ "yaxis": {
+ "title": {
+ "text": "mean_expectancy"
+ }
+ }
+ }
+ },
+ "image/png": "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"
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "\n",
+ "symbols = [\"BTC-USD\", \"ETH-USD\"]\n",
+ "data = vbt.YFData.download(symbols, missing_index=\"drop\")\n",
+ "price = data.get(\"Close\")\n",
+ "\n",
+ "n = np.random.randint(10, 101, size=1000).tolist()\n",
+ "pf = vbt.Portfolio.from_random_signals(price, n=n, init_cash=100, seed=42)\n",
+ "\n",
+ "mean_expectancy = pf.trades.expectancy().groupby([\"randnx_n\", \"symbol\"]).mean()\n",
+ "fig = mean_expectancy.unstack().vbt.scatterplot(xaxis_title=\"randnx_n\", yaxis_title=\"mean_expectancy\")\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 63,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " idx | \n",
+ " symbol | \n",
+ " cik | \n",
+ " name | \n",
+ " financial_currency | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " 0 | \n",
+ " NVDA | \n",
+ " 1045810.0 | \n",
+ " NVIDIA CORP | \n",
+ " USD | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " 1 | \n",
+ " GOOGL | \n",
+ " 1652044.0 | \n",
+ " Alphabet Inc. | \n",
+ " USD | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " 2 | \n",
+ " AAPL | \n",
+ " 320193.0 | \n",
+ " Apple Inc. | \n",
+ " USD | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " 3 | \n",
+ " MSFT | \n",
+ " 789019.0 | \n",
+ " MICROSOFT CORP | \n",
+ " USD | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " 4 | \n",
+ " AMZN | \n",
+ " 1018724.0 | \n",
+ " AMAZON COM INC | \n",
+ " USD | \n",
+ "
\n",
+ " \n",
+ " | ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ "
\n",
+ " \n",
+ " | 10396 | \n",
+ " extra_5 | \n",
+ " USMV | \n",
+ " NaN | \n",
+ " iShares MSCI USA Min Vol Factor ETF | \n",
+ " USD | \n",
+ "
\n",
+ " \n",
+ " | 10397 | \n",
+ " extra_6 | \n",
+ " IWM | \n",
+ " NaN | \n",
+ " iShares Russell 2000 ETF | \n",
+ " USD | \n",
+ "
\n",
+ " \n",
+ " | 10398 | \n",
+ " extra_7 | \n",
+ " VTV | \n",
+ " NaN | \n",
+ " Vanguard Value ETF | \n",
+ " USD | \n",
+ "
\n",
+ " \n",
+ " | 10399 | \n",
+ " extra_8 | \n",
+ " TLT | \n",
+ " NaN | \n",
+ " iShares 20+ Year Treasury Bond ETF | \n",
+ " USD | \n",
+ "
\n",
+ " \n",
+ " | 10400 | \n",
+ " extra_9 | \n",
+ " JNK | \n",
+ " NaN | \n",
+ " SPDR Bloomberg High Yield Bond ETF | \n",
+ " USD | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
10401 rows × 5 columns
\n",
+ "
"
+ ],
+ "text/plain": [
+ " idx symbol cik name \\\n",
+ "0 0 NVDA 1045810.0 NVIDIA CORP \n",
+ "1 1 GOOGL 1652044.0 Alphabet Inc. \n",
+ "2 2 AAPL 320193.0 Apple Inc. \n",
+ "3 3 MSFT 789019.0 MICROSOFT CORP \n",
+ "4 4 AMZN 1018724.0 AMAZON COM INC \n",
+ "... ... ... ... ... \n",
+ "10396 extra_5 USMV NaN iShares MSCI USA Min Vol Factor ETF \n",
+ "10397 extra_6 IWM NaN iShares Russell 2000 ETF \n",
+ "10398 extra_7 VTV NaN Vanguard Value ETF \n",
+ "10399 extra_8 TLT NaN iShares 20+ Year Treasury Bond ETF \n",
+ "10400 extra_9 JNK NaN SPDR Bloomberg High Yield Bond ETF \n",
+ "\n",
+ " financial_currency \n",
+ "0 USD \n",
+ "1 USD \n",
+ "2 USD \n",
+ "3 USD \n",
+ "4 USD \n",
+ "... ... \n",
+ "10396 USD \n",
+ "10397 USD \n",
+ "10398 USD \n",
+ "10399 USD \n",
+ "10400 USD \n",
+ "\n",
+ "[10401 rows x 5 columns]"
+ ]
+ },
+ "execution_count": 63,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from defeatbeta_api.data.company_meta import CompanyMeta\n",
+ "\n",
+ "meta = CompanyMeta()\n",
+ "pd.DataFrame(meta.get_all_companies_info())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 64,
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "SyntaxError",
+ "evalue": "invalid syntax (3999457017.py, line 1)",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;36m Cell \u001b[0;32mIn[64], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m dbt.\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n"
+ ]
+ }
+ ],
+ "source": [
+ "dbt."
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
diff --git a/docs/defeatbeta-api.org b/docs/defeatbeta-api.org
new file mode 100644
index 0000000..82de03d
--- /dev/null
+++ b/docs/defeatbeta-api.org
@@ -0,0 +1,203 @@
+#+TITLE: defeatbeta-api Reference
+#+AUTHOR: Wong Ding Feng
+#+DATE: 2026-04-25
+
+* How Data Retrieval Works
+
+** NOT a full download
+
+Uses *DuckDB + ~cache_httpfs~ extension* querying *remote Parquet files on HuggingFace*
+(~defeatbeta/yahoo-finance-data~). Every query runs SQL directly against remote files:
+
+#+begin_src sql
+SELECT * FROM 'https://huggingface.co/.../stock_prices.parquet' WHERE symbol = 'AAPL'
+#+end_src
+
+Parquet's columnar format + DuckDB *predicate pushdown* = only the row-groups matching
+your ticker are fetched over HTTP range requests. Not the full 3-4 GB file.
+
+** On-disk cache
+
+- Default 1 GB cache at ~~/.defeatbeta/cache/~
+- Stores fetched blocks so repeated queries are fast
+- On startup: checks ~spec.json~ on HuggingFace, clears stale cache if dataset was updated
+
+* Getting All Available Tickers
+
+#+begin_src python
+from defeatbeta_api.data.company_meta import CompanyMeta
+
+meta = CompanyMeta()
+all_tickers = meta.get_all_tickers() # List[str]
+all_companies = meta.get_all_companies_info() # List[dict]: symbol, name, cik, currency
+#+end_src
+
+Reads ~company_tickers.json~ from HuggingFace — a small JSON, not the big Parquet files.
+
+* Single Ticker API — ~Ticker("AAPL")~
+
+#+begin_src python
+from defeatbeta_api.data.ticker import Ticker
+t = Ticker("AAPL")
+#+end_src
+
+** Company Info
+
+| Method | Returns | What it gives |
+|----------------------------+-------------+---------------------------------------------------------|
+| ~info()~ | DataFrame | Profile: name, sector, industry, description, headcount |
+| ~officers()~ | DataFrame | Executive officers |
+| ~sec_filing()~ | DataFrame | SEC filings list |
+| ~news()~ | ~News~ object | Latest news articles |
+| ~earning_call_transcripts()~ | ~Transcripts~ | Earnings call transcripts |
+| ~calendar()~ | DataFrame | Upcoming earnings dates |
+
+** Prices & Basic Finance
+
+| Method | Returns | What it gives |
+|------------------------------------+-----------+------------------------------|
+| ~price()~ | DataFrame | Historical OHLCV prices |
+| ~splits()~ | DataFrame | Stock split events |
+| ~dividends()~ | DataFrame | Dividend payment history |
+| ~shares()~ | DataFrame | Shares outstanding over time |
+| ~beta(period="5y", benchmark="SPY")~ | DataFrame | Calculated beta vs benchmark |
+| ~currency(symbol)~ | DataFrame | Exchange rate history |
+| ~ttm_eps()~ | DataFrame | Trailing 12-month EPS |
+
+** Financial Statements
+
+| Method | Returns | What it gives |
+|------------------------------+-----------+-------------------------|
+| ~quarterly_income_statement()~ | ~Statement~ | Quarterly P&L |
+| ~annual_income_statement()~ | ~Statement~ | Annual P&L |
+| ~quarterly_balance_sheet()~ | ~Statement~ | Quarterly balance sheet |
+| ~annual_balance_sheet()~ | ~Statement~ | Annual balance sheet |
+| ~quarterly_cash_flow()~ | ~Statement~ | Quarterly cash flow |
+| ~annual_cash_flow()~ | ~Statement~ | Annual cash flow |
+
+** TTM Aggregates
+
+| Method | Returns | What it gives |
+|----------------------------------------+-----------+----------------------------------|
+| ~ttm_revenue()~ | DataFrame | Trailing 12-month revenue |
+| ~ttm_fcf()~ | DataFrame | Trailing 12-month free cash flow |
+| ~ttm_ebitda()~ | DataFrame | Trailing 12-month EBITDA |
+| ~ttm_net_income_common_stockholders()~ | DataFrame | Trailing 12-month net income |
+| ~ttm_pe()~ | DataFrame | Trailing P/E (price / ttm_eps) |
+
+** Revenue Breakdown
+
+| Method | Returns | What it gives |
+|------------------------+-----------+-----------------------------|
+| ~revenue_by_segment()~ | DataFrame | Revenue by business segment |
+| ~revenue_by_geography()~ | DataFrame | Revenue by region |
+| ~revenue_by_product()~ | DataFrame | Revenue by product line |
+
+** Valuation Multiples
+
+| Method | Returns | What it gives |
+|-------------------------+-----------+------------------------------|
+| ~market_capitalization()~ | DataFrame | Historical market cap |
+| ~ps_ratio()~ | DataFrame | Price/Sales ratio |
+| ~pb_ratio()~ | DataFrame | Price/Book ratio |
+| ~peg_ratio()~ | DataFrame | PEG ratio |
+| ~enterprise_value()~ | DataFrame | Enterprise value |
+| ~enterprise_to_revenue()~ | DataFrame | EV/Revenue |
+| ~enterprise_to_ebitda()~ | DataFrame | EV/EBITDA |
+| ~debt_to_equity()~ | DataFrame | D/E ratio |
+| ~net_debt_ttm()~ | DataFrame | Net debt (TTM) |
+| ~wacc()~ | DataFrame | Weighted avg cost of capital |
+
+** Profitability Returns
+
+| Method | Returns | What it gives |
+|---------------------+-----------+------------------------------------|
+| ~roe()~ | DataFrame | Return on equity |
+| ~roa()~ | DataFrame | Return on assets |
+| ~roic()~ | DataFrame | Return on invested capital |
+| ~roce()~ | DataFrame | Return on capital employed |
+| ~equity_multiplier()~ | DataFrame | Financial leverage (assets/equity) |
+| ~asset_turnover()~ | DataFrame | Revenue/assets efficiency |
+
+** Margins
+
+| Method | Returns | What it gives |
+|------------------------------+-----------+--------------------|
+| ~quarterly_gross_margin()~ | DataFrame | Gross margin % |
+| ~annual_gross_margin()~ | DataFrame | Gross margin % |
+| ~quarterly_operating_margin()~ | DataFrame | Operating margin % |
+| ~annual_operating_margin()~ | DataFrame | Operating margin % |
+| ~quarterly_net_margin()~ | DataFrame | Net margin % |
+| ~annual_net_margin()~ | DataFrame | Net margin % |
+| ~quarterly_ebitda_margin()~ | DataFrame | EBITDA margin % |
+| ~annual_ebitda_margin()~ | DataFrame | EBITDA margin % |
+| ~quarterly_fcf_margin()~ | DataFrame | FCF margin % |
+| ~annual_fcf_margin()~ | DataFrame | FCF margin % |
+
+** YoY Growth
+
+| Method | Returns | What it gives |
+|-----------------------------------------+-----------+---------------------|
+| ~quarterly_revenue_yoy_growth()~ | DataFrame | Revenue growth % |
+| ~annual_revenue_yoy_growth()~ | DataFrame | Revenue growth % |
+| ~quarterly_operating_income_yoy_growth()~ | DataFrame | Op. income growth % |
+| ~annual_operating_income_yoy_growth()~ | DataFrame | Op. income growth % |
+| ~quarterly_ebitda_yoy_growth()~ | DataFrame | EBITDA growth % |
+| ~annual_ebitda_yoy_growth()~ | DataFrame | EBITDA growth % |
+| ~quarterly_net_income_yoy_growth()~ | DataFrame | Net income growth % |
+| ~annual_net_income_yoy_growth()~ | DataFrame | Net income growth % |
+| ~quarterly_fcf_yoy_growth()~ | DataFrame | FCF growth % |
+| ~annual_fcf_yoy_growth()~ | DataFrame | FCF growth % |
+| ~quarterly_eps_yoy_growth()~ | DataFrame | EPS growth % |
+| ~quarterly_ttm_eps_yoy_growth()~ | DataFrame | TTM EPS growth % |
+
+** Industry Comparisons
+
+Uses the ticker's own industry to benchmark against peers.
+
+| Method | Returns | What it gives |
+|------------------------------------+-----------+--------------------------|
+| ~industry_ttm_pe()~ | DataFrame | Avg P/E across industry |
+| ~industry_ps_ratio()~ | DataFrame | Industry P/S |
+| ~industry_pb_ratio()~ | DataFrame | Industry P/B |
+| ~industry_roe()~ | DataFrame | Industry ROE |
+| ~industry_roa()~ | DataFrame | Industry ROA |
+| ~industry_roic()~ | DataFrame | Industry ROIC |
+| ~industry_equity_multiplier()~ | DataFrame | Industry leverage |
+| ~industry_asset_turnover()~ | DataFrame | Industry efficiency |
+| ~industry_quarterly_gross_margin()~ | DataFrame | Industry gross margin % |
+| ~industry_quarterly_ebitda_margin()~ | DataFrame | Industry EBITDA margin % |
+| ~industry_quarterly_net_margin()~ | DataFrame | Industry net margin % |
+
+** DCF / Advanced
+
+| Method | Returns | What it gives |
+|-----------------------------+---------+----------------------------------------|
+| ~dcf_data()~ | dict | All raw inputs for a DCF model |
+| ~dcf()~ | dict | Full DCF valuation + exports ~.xlsx~ |
+| ~download_data_performance()~ | str | Timing summary of data fetch durations |
+
+* Multi-Ticker API — ~Tickers(["AAPL", "NVDA"])~
+
+#+begin_src python
+from defeatbeta_api.data.tickers import Tickers
+t = Tickers(["AAPL", "NVDA"])
+t = Tickers(["AAPL", "NVDA"], max_workers=2) # limit parallelism
+#+end_src
+
+Wraps all ~Ticker~ methods, running them in *parallel threads*.
+
+- Methods returning simple data → *combined DataFrame* (all tickers in one table)
+- Methods returning complex objects (statements, news, transcripts) → ~{symbol: result}~ dict
+
+Same method names as ~Ticker~, plus industry comparison methods operate per unique
+industry represented across the list.
+
+#+begin_src python
+t.info() # → DataFrame (combined)
+t.price() # → DataFrame (combined)
+t.annual_income_statement() # → {'AAPL': Statement(...), 'NVDA': Statement(...)}
+t.news() # → {'AAPL': News(...), 'NVDA': News(...)}
+t.earning_call_transcripts() # → {'AAPL': Transcripts(...), 'NVDA': Transcripts(...)}
+t.industry_roe() # → DataFrame (one row per unique industry)
+#+end_src
diff --git a/docs/defeatbeta_mapping.org b/docs/defeatbeta_mapping.org
index 261b4d6..3883ee2 100644
--- a/docs/defeatbeta_mapping.org
+++ b/docs/defeatbeta_mapping.org
@@ -7,20 +7,20 @@
# Overview
-| Category | Yahoo Finance | DefeatBeta-API | Notes |
-|----------|--------------|----------------|-------|
-| **Data Source** | Yahoo Finance API | HuggingFace + DuckDB | No rate limits |
-| **Query Engine** | Direct API | DuckDB OLAP | Sub-second queries |
-| **Update Frequency** | Real-time (15min delayed) | Daily batch | DefeatBeta is historical only |
-| **Historical Depth** | Full history | Full history | Comparable coverage |
-| **Special Features** | Limited | Earnings transcripts, DCF, AI | DefeatBeta has unique capabilities |
+| Category | Yahoo Finance | DefeatBeta-API | Notes |
+|--------------------+---------------------------+-------------------------------+------------------------------------|
+| **Data Source** | Yahoo Finance API | HuggingFace + DuckDB | No rate limits |
+| **Query Engine** | Direct API | DuckDB OLAP | Sub-second queries |
+| **Update Frequency** | Real-time (15min delayed) | Daily batch | DefeatBeta is historical only |
+| **Historical Depth** | Full history | Full history | Comparable coverage |
+| **Special Features** | Limited | Earnings transcripts, DCF, AI | DefeatBeta has unique capabilities |
* Price & Volume Data
-| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
-|--------------|----------------|-------------|-------|
-| ~ticker.history(period='max')~ | ~ticker.price()~ | pandas.DataFrame | OHLCV data |
-| ~ticker.history(period='1d')~ | N/A | - | Real-time not available |
+| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
+|----------------------------------------+-------------------------+------------------+--------------------------|
+| ~ticker.history(period='max')~ | ~ticker.price()~ | pandas.DataFrame | OHLCV data |
+| ~ticker.history(period='1d')~ | N/A | - | Real-time not available |
| ~ticker.history(start='...', end='...')~ | ~ticker.price()~ (filter) | pandas.DataFrame | Date filtering available |
* DefeatBeta Price Data Structure
@@ -33,14 +33,14 @@
* Financial Statements
-| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
-|--------------|----------------|-------------|-------|
-| ~ticker.quarterly_financials~ | ~ticker.quarterly_income_statement()~ | Statement object | Different format |
-| ~ticker.financials~ | ~ticker.annual_income_statement()~ | Statement object | Annual version |
-| ~ticker.quarterly_balance_sheet~ | ~ticker.quarterly_balance_sheet()~ | Statement object | Same structure |
-| ~ticker.balance_sheet~ | ~ticker.annual_balance_sheet()~ | Statement object | Annual version |
-| ~ticker.quarterly_cashflow~ | ~ticker.quarterly_cash_flow()~ | Statement object | Note: 'flow' vs 'flow' |
-| ~ticker.cashflow~ | ~ticker.annual_cash_flow()~ | Statement object | Annual version |
+| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
+|--------------------------------+-------------------------------------+------------------+------------------------|
+| ~ticker.quarterly_income_stmt~ | ~ticker.quarterly_income_statement()~ | Statement object | Different format |
+| ~ticker.income_stmt~ | ~ticker.annual_income_statement()~ | Statement object | Annual version |
+| ~ticker.quarterly_balance_sheet~ | ~ticker.quarterly_balance_sheet()~ | Statement object | Same structure |
+| ~ticker.balance_sheet~ | ~ticker.annual_balance_sheet()~ | Statement object | Annual version |
+| ~ticker.quarterly_cashflow~ | ~ticker.quarterly_cash_flow()~ | Statement object | 'cashflow' vs 'cash_flow' |
+| ~ticker.cashflow~ | ~ticker.annual_cash_flow()~ | Statement object | Annual version |
** Statement Object Methods
#+BEGIN_SRC python
@@ -53,16 +53,16 @@ income_stmt.print_pretty_table() # Formatted output
* Valuation Metrics
-| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
-|--------------|----------------|-------------|-------|
-| ~ticker.info['trailingPE']~ | ~ticker.ttm_pe()~ | pandas.DataFrame | **Historical** time series! |
-| ~ticker.info['forwardPE']~ | N/A | - | Not available |
-| ~ticker.info['trailingEps']~ | ~ticker.ttm_eps()~ | pandas.DataFrame | **Historical** time series! |
-| ~ticker.info['forwardEps']~ | N/A | - | Not available |
-| ~ticker.info['marketCap']~ | ~ticker.market_capitalization()~ | pandas.DataFrame | **Historical** time series! |
-| ~ticker.info['priceToBook']~ | ~ticker.pb_ratio()~ | pandas.DataFrame | Price/Book ratio |
-| ~ticker.info['priceToSalesTrailing12Months']~ | ~ticker.ps_ratio()~ | pandas.DataFrame | Price/Sales ratio |
-| N/A | ~ticker.peg_ratio()~ | pandas.DataFrame | PEG ratio (unique) |
+| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
+|---------------------------------------------+--------------------------------+------------------+---------------------------|
+| ~ticker.info['trailingPE']~ | ~ticker.ttm_pe()~ | pandas.DataFrame | **Historical** time series! |
+| ~ticker.info['forwardPE']~ | N/A | - | Not available |
+| ~ticker.info['trailingEps']~ | ~ticker.ttm_eps()~ | pandas.DataFrame | **Historical** time series! |
+| ~ticker.info['forwardEps']~ | N/A | - | Not available |
+| ~ticker.info['marketCap']~ | ~ticker.market_capitalization()~ | pandas.DataFrame | **Historical** time series! |
+| ~ticker.info['priceToBook']~ | ~ticker.pb_ratio()~ | pandas.DataFrame | Price/Book ratio |
+| ~ticker.info['priceToSalesTrailing12Months']~ | ~ticker.ps_ratio()~ | pandas.DataFrame | Price/Sales ratio |
+| N/A | ~ticker.peg_ratio()~ | pandas.DataFrame | PEG ratio (unique) |
** Key Advantage: Historical Valuation Data
DefeatBeta provides **full historical time series** for:
@@ -75,13 +75,13 @@ Yahoo Finance only provides **current values** in ~.info~
* Financial Ratios
-| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
-|--------------|----------------|-------------|-------|
-| ~ticker.info['returnOnEquity']~ | ~ticker.roe()~ | pandas.DataFrame | **Historical** time series! |
-| ~ticker.info['returnOnAssets']~ | ~ticker.roa()~ | pandas.DataFrame | **Historical** time series! |
-| N/A | ~ticker.roic()~ | pandas.DataFrame | Return on Invested Capital |
-| N/A | ~ticker.wacc()~ | pandas.DataFrame | Weighted Avg Cost of Capital |
-| ~ticker.info['beta']~ | ~ticker.beta()~ | pandas.DataFrame | 5Y monthly beta |
+| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
+|-------------------------------+----------------+------------------+------------------------------|
+| ~ticker.info['returnOnEquity']~ | ~ticker.roe()~ | pandas.DataFrame | **Historical** time series! |
+| ~ticker.info['returnOnAssets']~ | ~ticker.roa()~ | pandas.DataFrame | **Historical** time series! |
+| N/A | ~ticker.roic()~ | pandas.DataFrame | Return on Invested Capital |
+| N/A | ~ticker.wacc()~ | pandas.DataFrame | Weighted Avg Cost of Capital |
+| ~ticker.info['beta']~ | ~ticker.beta()~ | pandas.DataFrame | 5Y monthly beta |
** WACC Components Available in DefeatBeta
#+BEGIN_SRC python
@@ -93,66 +93,67 @@ wacc = ticker.wacc()
* Growth Metrics
-| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
-|--------------|----------------|-------------|-------|
-| ~ticker.info['revenueGrowth']~ | ~ticker.quarterly_revenue_yoy_growth()~ | pandas.DataFrame | YoY growth |
-| ~ticker.info['earningsGrowth']~ | ~ticker.quarterly_eps_yoy_growth()~ | pandas.DataFrame | EPS YoY growth |
-| N/A | ~ticker.quarterly_net_income_yoy_growth()~ | pandas.DataFrame | Net income growth |
-| N/A | ~ticker.quarterly_operating_income_yoy_growth()~ | pandas.DataFrame | Operating income growth |
-| N/A | ~ticker.quarterly_ebitda_yoy_growth()~ | pandas.DataFrame | EBITDA growth |
-| N/A | ~ticker.quarterly_fcf_yoy_growth()~ | pandas.DataFrame | Free cash flow growth |
-| N/A | ~ticker.annual_revenue_yoy_growth()~ | pandas.DataFrame | Annual revenue growth |
+| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
+|-------------------------------+------------------------------------------------+------------------+-------------------------|
+| ~ticker.info['revenueGrowth']~ | ~ticker.quarterly_revenue_yoy_growth()~ | pandas.DataFrame | YoY growth |
+| ~ticker.info['earningsGrowth']~ | ~ticker.quarterly_eps_yoy_growth()~ | pandas.DataFrame | EPS YoY growth |
+| N/A | ~ticker.quarterly_net_income_yoy_growth()~ | pandas.DataFrame | Net income growth |
+| N/A | ~ticker.quarterly_operating_income_yoy_growth()~ | pandas.DataFrame | Operating income growth |
+| N/A | ~ticker.quarterly_ebitda_yoy_growth()~ | pandas.DataFrame | EBITDA growth |
+| N/A | ~ticker.quarterly_fcf_yoy_growth()~ | pandas.DataFrame | Free cash flow growth |
+| N/A | ~ticker.annual_revenue_yoy_growth()~ | pandas.DataFrame | Annual revenue growth |
* Margin Metrics
-| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
-|--------------|----------------|-------------|-------|
-| ~ticker.info['profitMargins']~ | ~ticker.quarterly_net_margin()~ | pandas.DataFrame | **Historical** time series! |
-| ~ticker.info['grossMargins']~ | ~ticker.quarterly_gross_margin()~ | pandas.DataFrame | **Historical** time series! |
-| ~ticker.info['operatingMargins']~ | ~ticker.quarterly_operating_margin()~ | pandas.DataFrame | **Historical** time series! |
-| N/A | ~ticker.quarterly_ebitda_margin()~ | pandas.DataFrame | EBITDA margin |
-| N/A | ~ticker.quarterly_fcf_margin()~ | pandas.DataFrame | Free cash flow margin |
-| N/A | ~ticker.industry_quarterly_gross_margin()~ | pandas.DataFrame | Industry comparison |
+| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
+|---------------------------------+------------------------------------------+------------------+---------------------------|
+| ~ticker.info['profitMargins']~ | ~ticker.quarterly_net_margin()~ | pandas.DataFrame | **Historical** time series! |
+| ~ticker.info['grossMargins']~ | ~ticker.quarterly_gross_margin()~ | pandas.DataFrame | **Historical** time series! |
+| ~ticker.info['operatingMargins']~ | ~ticker.quarterly_operating_margin()~ | pandas.DataFrame | **Historical** time series! |
+| N/A | ~ticker.quarterly_ebitda_margin()~ | pandas.DataFrame | EBITDA margin |
+| N/A | ~ticker.quarterly_fcf_margin()~ | pandas.DataFrame | Free cash flow margin |
+| N/A | ~ticker.industry_quarterly_gross_margin()~ | pandas.DataFrame | Industry comparison |
* Dividends & Stock Splits
-| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
-|--------------|----------------|-------------|-------|
-| ~ticker.dividends~ | ~ticker.dividends()~ | pandas.DataFrame | Dividend history |
-| ~ticker.splits~ | ~ticker.splits()~ | pandas.DataFrame | Stock split history |
-| ~ticker.info['dividendYield']~ | N/A | - | Not in separate field |
-| ~ticker.info['trailingAnnualDividendYield']~ | N/A | - | Not available |
+| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
+|--------------------------------------------+--------------------+------------------+-----------------------|
+| ~ticker.dividends~ | ~ticker.dividends()~ | pandas.DataFrame | Dividend history |
+| ~ticker.splits~ | ~ticker.splits()~ | pandas.DataFrame | Stock split history |
+| ~ticker.info['dividendYield']~ | N/A | - | Not in separate field |
+| ~ticker.info['trailingAnnualDividendYield']~ | N/A | - | Not available |
* Company Info & Metadata
-| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
-|--------------|----------------|-------------|-------|
-| ~ticker.info~ | ~ticker.info()~ | pandas.DataFrame | One row, many columns |
-| ~ticker.info['longName']~ | ~ticker.info()['long_business_summary']~ | string | Business summary |
-| ~ticker.info['sector']~ | ~ticker.info()['sector']~ | string | Sector classification |
-| ~ticker.info['industry']~ | ~ticker.info()['industry']~ | string | Industry classification |
-| ~ticker.info['website']~ | ~ticker.info()['web_site']~ | string | Company website |
-| ~ticker.info['fullTimeEmployees']~ | ~ticker.info()['full_time_employees']~ | int | Employee count |
-| N/A | ~ticker.officers()~ | pandas.DataFrame | Company officers |
-| N/A | ~ticker.calendar()~ | pandas.DataFrame | Earnings calendar |
+| Yahoo Finance | DefeatBeta-API | Return Type | Notes |
+|----------------------------------+----------------------------------------+------------------+-------------------------|
+| ~ticker.info~ | ~ticker.info()~ | pandas.DataFrame | One row, many columns |
+| ~ticker.info['longName']~ | N/A (check name/short_name column) | string | Company trading name |
+| ~ticker.info['longBusinessSummary']~ | ~ticker.info()['long_business_summary']~ | string | Business description |
+| ~ticker.info['sector']~ | ~ticker.info()['sector']~ | string | Sector classification |
+| ~ticker.info['industry']~ | ~ticker.info()['industry']~ | string | Industry classification |
+| ~ticker.info['website']~ | ~ticker.info()['web_site']~ | string | Company website |
+| ~ticker.info['fullTimeEmployees']~ | ~ticker.info()['full_time_employees']~ | int | Employee count |
+| N/A | ~ticker.officers()~ | pandas.DataFrame | Company officers |
+| N/A | ~ticker.calendar()~ | pandas.DataFrame | Earnings calendar |
* Unique DefeatBeta Features (Not in Yahoo Finance)
-| Feature | Method | Description |
-|---------|--------|-------------|
-| **Earnings Transcripts** | ~ticker.earning_call_transcripts()~ | Full earnings call transcripts |
-| | ~transcripts.get_transcripts_list()~ | List all available transcripts |
-| | ~transcripts.get_transcript(year, quarter)~ | Get specific quarter's transcript |
-| | ~transcripts.summarize_key_financial_data_with_ai()~ | AI-powered summary |
-| **Revenue Breakdown** | ~ticker.revenue_by_segment()~ | Revenue by product segment |
-| | ~ticker.revenue_by_product()~ | Detailed product breakdown |
-| | ~ticker.revenue_by_geography()~ | Revenue by geographic region |
-| **Automated DCF** | ~ticker.dcf()~ | Generates Excel DCF valuation |
-| **AI Analysis** | ~transcripts.analyze_financial_metrics_change...~ | LLM analysis of quarter changes |
-| | ~transcripts.analyze_financial_metrics_forecast...~ | LLM forecast analysis |
-| **Industry Metrics** | ~ticker.industry_ttm_pe()~ | Industry P/E comparison |
-| | ~ticker.industry_roe()~ | Industry ROE comparison |
-| | ~ticker.industry_quarterly_gross_margin()~ | Industry margin comparison |
+| Feature | Method | Description |
+|------------------------+----------------------------------------------------+-----------------------------------|
+| **Earnings Transcripts** | ~ticker.earning_call_transcripts()~ | Full earnings call transcripts |
+| | ~transcripts.get_transcripts_list()~ | List all available transcripts |
+| | ~transcripts.get_transcript(year, quarter)~ | Get specific quarter's transcript |
+| | ~transcripts.summarize_key_financial_data_with_ai()~ | AI-powered summary |
+| **Revenue Breakdown** | ~ticker.revenue_by_segment()~ | Revenue by product segment |
+| | ~ticker.revenue_by_product()~ | Detailed product breakdown |
+| | ~ticker.revenue_by_geography()~ | Revenue by geographic region |
+| **Automated DCF** | ~ticker.dcf()~ | Generates Excel DCF valuation |
+| **AI Analysis** | ~transcripts.analyze_financial_metrics_change...~ | LLM analysis of quarter changes |
+| | ~transcripts.analyze_financial_metrics_forecast...~ | LLM forecast analysis |
+| **Industry Metrics** | ~ticker.industry_ttm_pe()~ | Industry P/E comparison |
+| | ~ticker.industry_roe()~ | Industry ROE comparison |
+| | ~ticker.industry_quarterly_gross_margin()~ | Industry margin comparison |
* Example: yfinance → DefeatBeta Migration
@@ -209,26 +210,26 @@ current_roe = ticker.roe().iloc[-1]['roe']
* Quick Reference: Common Operations
-| Operation | Yahoo Finance | DefeatBeta-API |
-|-----------|--------------|----------------|
-| Get current price | ~ticker.info['currentPrice']~ | ~ticker.price().iloc[-1]['close']~ |
-| Get current P/E | ~ticker.info['trailingPE']~ | ~ticker.ttm_pe().iloc[-1]['ttm_pe']~ |
-| Get current EPS | ~ticker.info['trailingEps']~ | ~ticker.ttm_eps().iloc[-1]['tailing_eps']~ |
-| Get market cap | ~ticker.info['marketCap']~ | ~ticker.market_capitalization().iloc[-1]['market_capitalization']~ |
-| Get ROE | ~ticker.info['returnOnEquity']~ | ~ticker.roe().iloc[-1]['roe']~ |
-| Get revenue (TTM) | ~ticker.info['totalRevenue']~ | ~ticker.quarterly_income_statement().df().iloc[0]['TTM']~ |
-| Get 5Y beta | ~ticker.info['beta']~ | ~ticker.beta().iloc[-1]['beta']~ |
+| Operation | Yahoo Finance | DefeatBeta-API |
+|-------------------+-------------------------------+------------------------------------------------------------------|
+| Get current price | ~ticker.info['currentPrice']~ | ~ticker.price().iloc[-1]['close']~ |
+| Get current P/E | ~ticker.info['trailingPE']~ | ~ticker.ttm_pe().iloc[-1]['ttm_pe']~ |
+| Get current EPS | ~ticker.info['trailingEps']~ | ~ticker.ttm_eps().iloc[-1]['tailing_eps']~ |
+| Get market cap | ~ticker.info['marketCap']~ | ~ticker.market_capitalization().iloc[-1]['market_capitalization']~ |
+| Get ROE | ~ticker.info['returnOnEquity']~ | ~ticker.roe().iloc[-1]['roe']~ |
+| Get revenue (TTM) | ~ticker.info['totalRevenue']~ | ~ticker.quarterly_income_statement().df().iloc[0]['TTM']~ |
+| Get 5Y beta | ~ticker.info['beta']~ | ~ticker.beta().iloc[-1]['beta']~ |
* Data Type Differences
-| Aspect | Yahoo Finance | DefeatBeta-API |
-|--------|--------------|----------------|
-| Dates in DataFrame | DatetimeIndex | 'report_date' column |
-| Column naming | Title case (Open, Close) | Snake case (open, close) |
-| Dividends/Splits | Separate columns in history | Separate DataFrames |
-| Quarterly data | Transposed (dates as columns) | Wide format (TTM + quarters as columns) |
-| Missing data | NaN | NaN |
-| Numeric types | float | Decimal (convert with ~float()~) |
+| Aspect | Yahoo Finance | DefeatBeta-API |
+|--------------------+-------------------------------+-----------------------------------------|
+| Dates in DataFrame | DatetimeIndex | 'report_date' column |
+| Column naming | Title case (Open, Close) | Snake case (open, close) |
+| Dividends/Splits | Separate columns in history | Separate DataFrames |
+| Quarterly data | Transposed (dates as columns) | Wide format (TTM + quarters as columns) |
+| Missing data | NaN | NaN |
+| Numeric types | float | Decimal (convert with ~float()~) |
* Converting Decimal to Float
#+BEGIN_SRC python
@@ -244,37 +245,94 @@ market_cap_billions = float(market_cap.iloc[-1]['market_capitalization']) / 1e9
* When to Use Each
-| Use Case | Recommendation | Reason |
-|----------|---------------|--------|
-| Backtesting trading strategies | **DefeatBeta** | No rate limits, consistent historical data |
-| DCF Valuation modeling | **DefeatBeta** | Automated Excel output |
-| Revenue segment analysis | **DefeatBeta** | Unique revenue breakdown |
-| Earnings call research | **DefeatBeta** | Full transcripts available |
-| Real-time price alerts | **Yahoo Finance** | 15min delayed but real-time |
-| Analyst recommendations | **Yahoo Finance** | Price targets, ratings |
-| Institutional ownership | **Yahoo Finance** | Major holders data |
-| Options/derivatives data | **Yahoo Finance** | Not available in DefeatBeta |
-| Quick stock lookup | **Either** | Both work well |
-| Multi-year historical analysis | **DefeatBeta** | Faster queries, no rate limits |
+| Use Case | Recommendation | Reason |
+|--------------------------------+-----------------+--------------------------------------------|
+| Backtesting trading strategies | **DefeatBeta** | No rate limits, consistent historical data |
+| DCF Valuation modeling | **DefeatBeta** | Automated Excel output |
+| Revenue segment analysis | **DefeatBeta** | Unique revenue breakdown |
+| Earnings call research | **DefeatBeta** | Full transcripts available |
+| Real-time price alerts | **Yahoo Finance** | 15min delayed but real-time |
+| Analyst recommendations | **Yahoo Finance** | Price targets, ratings |
+| Institutional ownership | **Yahoo Finance** | Major holders data |
+| Options/derivatives data | **Yahoo Finance** | Not available in DefeatBeta |
+| Quick stock lookup | **Either** | Both work well |
+| Multi-year historical analysis | **DefeatBeta** | Faster queries, no rate limits |
* Environment Setup
-| Task | Command |
-|------|---------|
-| Install DefeatBeta | ~uv add defeatbeta-api~ |
-| Install yfinance | ~uv add yfinance~ |
-| Run notebook | ~uv run jupyter notebook~ |
-| Check version | ~ticker.price()~ shows data update date |
+| Task | Command |
+|--------------------+---------------------------------------|
+| Install DefeatBeta | ~uv add defeatbeta-api~ |
+| Install yfinance | ~uv add yfinance~ |
+| Run notebook | ~uv run jupyter notebook~ |
+| Check version | ~ticker.price()~ shows data update date |
+
+* Mapping Limitations & Verification Notes
+ /Verified 2026-04-26 via test_mapping.py on AAPL. defeatbeta-api 0.0.45, yfinance 1.3.0, DuckDB 1.4.3./
+
+** What was confirmed correct (live data matched)
+- ~dividends()~: both APIs return $0.26 for Nov-2025 and Feb-2026 payments — values match exactly
+- ~splits()~: both return the same ratios (7:1 in 2014, 4:1 in 2020)
+
+** Incorrect property names for yfinance (deprecated in v1.3)
+
+| Used in mapping | Correct name in yfinance 1.3+ |
+|----------------------------+-------------------------------|
+| ~ticker.quarterly_financials~ | ~ticker.quarterly_income_stmt~ |
+| ~ticker.financials~ | ~ticker.income_stmt~ |
+
+The old names were removed. The mapping should use the new names.
+
+** Conceptual mismatch: longName vs long_business_summary (Company Info)
+
+The mapping equates ~ticker.info['longName']~ with ~ticker.info()['long_business_summary']~,
+labelling it "Business summary". This is wrong:
+
+- ~longName~ = company trading name, e.g. "Apple Inc."
+- ~long_business_summary~ = multi-sentence business description paragraph
+- The yfinance equivalent of the description is ~ticker.info['longBusinessSummary']~
+
+** Typo in Financial Statements note (cashflow row)
+
+The "Notes" column reads ~Note: 'flow' vs 'flow'~. Should read: ~'cashflow' vs 'cash_flow'~
+
+** Data format differences found in verified pairs
+
+| Field | yfinance | defeatbeta |
+|---------------+--------------------------------------+-------------------------------|
+| dividends | Series, DatetimeIndex, float amount | DataFrame, report_date col |
+| split_factor | float (e.g. ~7.0~) | string ratio (e.g. ~"7:1"~) |
+
+** Historical coverage gap
+
+- Dividends: yfinance returns 90 records, defeatbeta returns 61 (truncated history)
+- Splits: yfinance returns 5 events, defeatbeta returns 4 (one historical split missing)
+
+** DuckDB compatibility issue (defeatbeta-api 0.0.45 + DuckDB 1.4.3)
+
+All Parquet-backed queries except ~dividends()~ and ~splits()~ failed during verification
+with either ~"don't know what type:"~ or ~"TProtocolException: Invalid data"~. Affected:
+~price()~, all financial statements, all valuation/ratio/margin/growth metrics, ~info()~, ~beta()~.
+
+This is an incompatibility between DuckDB 1.4.3 and the httpfs extension / remote Parquet
+format used by defeatbeta-api. Downgrading DuckDB or waiting for a defeatbeta-api update
+may resolve it. The logical mapping is still valid — the API shape is correct, only the
+DuckDB query layer is broken.
+
+** Data freshness
+
+DefeatBeta dataset last updated 2026-04-17, 9 days behind current date. yfinance has
+15-minute delayed real-time data. This gap will always exist for defeatbeta.
* Common Issues & Solutions
-| Issue | Solution |
-|-------|----------|
-| ~TypeError: unsupported operand type(s) for /: 'Decimal' and 'float'~ | Wrap values in ~float()~ |
-| ~Rate limit exceeded~ | Switch to DefeatBeta |
-| ~Real-time data needed~ | Use Yahoo Finance |
-| ~Missing revenue breakdown~ | Use DefeatBeta ~revenue_by_segment()~ |
-| ~Slow queries~ | Use DefeatBeta (DuckDB engine) |
+| Issue | Solution |
+|---------------------------------------------------------------------+-------------------------------------|
+| ~TypeError: unsupported operand type(s) for /: 'Decimal' and 'float'~ | Wrap values in ~float()~ |
+| ~Rate limit exceeded~ | Switch to DefeatBeta |
+| ~Real-time data needed~ | Use Yahoo Finance |
+| ~Missing revenue breakdown~ | Use DefeatBeta ~revenue_by_segment()~ |
+| ~Slow queries~ | Use DefeatBeta (DuckDB engine) |
* Additional Resources
@@ -285,7 +343,7 @@ market_cap_billions = float(market_cap.iloc[-1]['market_capitalization']) / 1e9
* Footer
#+BEGIN_COMMENT
-Last updated: 2026-04-25
+Last updated: 2026-04-26 (verified via test_mapping.py on AAPL)
Author: Documentation
Version: 1.0
-#+END_COMMENT
\ No newline at end of file
+#+END_COMMENT
diff --git a/docs/test_mapping.py b/docs/test_mapping.py
new file mode 100644
index 0000000..9330cd5
--- /dev/null
+++ b/docs/test_mapping.py
@@ -0,0 +1,228 @@
+"""
+Mapping verification: yfinance vs defeatbeta-api for AAPL.
+Prints type + 2 representative rows/values for each mapped pair.
+Methods that fail are reported inline (not crashed).
+"""
+import warnings
+warnings.filterwarnings("ignore")
+
+import yfinance as yf
+from defeatbeta_api.data.ticker import Ticker
+import pandas as pd
+
+SYMBOL = "AAPL"
+
+yf_t = yf.Ticker(SYMBOL)
+db_t = Ticker(SYMBOL)
+
+PASS = "✓"
+FAIL = "✗"
+
+
+def try_db(fn, *args, **kwargs):
+ """Call a defeatbeta method, return (result, None) or (None, error_str)."""
+ try:
+ return fn(*args, **kwargs), None
+ except Exception as e:
+ return None, str(e)[:80]
+
+
+def show_df(label, df, n=2):
+ if df is None:
+ return
+ if isinstance(df, pd.DataFrame):
+ print(f" {label} ({df.shape[0]}r×{df.shape[1]}c), last {n} rows:")
+ cols = list(df.columns)[:6]
+ print(df[cols].tail(n).to_string(index=False))
+ elif isinstance(df, pd.Series):
+ print(f" {label} (Series len={len(df)}), last {n}:")
+ print(df.tail(n).to_string())
+
+
+def row(label, yf_val, yf_type_label, db_result, db_err):
+ ok = PASS if db_result is not None else FAIL
+ print(f"\n{ok} {label}")
+ if db_err:
+ print(f" yfinance → {yf_type_label}: {yf_val!r}")
+ print(f" defeatbeta→ ERROR: {db_err}")
+ else:
+ print(f" yfinance type={yf_type_label} value={yf_val!r}")
+ print(f" defeatbeta type={type(db_result).__name__}", end="")
+ if isinstance(db_result, pd.DataFrame):
+ print(f" cols={list(db_result.columns)}")
+ show_df("defeatbeta", db_result)
+ else:
+ print(f" value={db_result!r}")
+
+
+def section(title):
+ print(f"\n{'='*68}\n {title}\n{'='*68}")
+
+
+# ── 1. PRICE DATA ─────────────────────────────────────────────────────────────
+section("1. PRICE DATA — ticker.history() vs ticker.price()")
+
+yf_price = yf_t.history(period="5d")[["Open", "Close", "High", "Low", "Volume"]]
+db_price, db_price_err = try_db(db_t.price)
+
+print(f"\n{PASS if not db_price_err else FAIL} ticker.history() vs ticker.price()")
+print(f" yfinance type=DataFrame cols={list(yf_price.columns)}")
+print(yf_price.tail(2).to_string())
+if db_price_err:
+ print(f" defeatbeta→ ERROR: {db_price_err}")
+else:
+ print(f" defeatbeta type=DataFrame cols={list(db_price.columns)}")
+ print(db_price.tail(2).to_string(index=False))
+
+# ── 2. FINANCIAL STATEMENTS ───────────────────────────────────────────────────
+section("2. FINANCIAL STATEMENTS")
+
+# yfinance uses .quarterly_income_stmt (v1.3) — old .quarterly_financials is deprecated
+yf_inc_q = yf_t.quarterly_income_stmt
+db_inc_q, db_inc_q_err = try_db(db_t.quarterly_income_statement)
+db_inc_q_df = db_inc_q.df() if db_inc_q else None
+
+print(f"\n{PASS if not db_inc_q_err else FAIL} quarterly_income_stmt vs quarterly_income_statement()")
+print(f" yfinance type={type(yf_inc_q).__name__} shape={yf_inc_q.shape} (rows=line items, cols=quarters)")
+print(yf_inc_q.iloc[:2, :2].to_string())
+if db_inc_q_err:
+ print(f" defeatbeta→ ERROR: {db_inc_q_err}")
+else:
+ print(f" defeatbeta type={type(db_inc_q).__name__} → .df() shape={db_inc_q_df.shape}")
+ print(db_inc_q_df.iloc[:2, :5].to_string(index=False))
+
+yf_bs = yf_t.balance_sheet
+db_bs, db_bs_err = try_db(db_t.annual_balance_sheet)
+db_bs_df = db_bs.df() if db_bs else None
+
+print(f"\n{PASS if not db_bs_err else FAIL} balance_sheet vs annual_balance_sheet()")
+print(f" yfinance type={type(yf_bs).__name__} shape={yf_bs.shape}")
+print(yf_bs.iloc[:2, :2].to_string())
+if db_bs_err:
+ print(f" defeatbeta→ ERROR: {db_bs_err}")
+else:
+ print(f" defeatbeta type={type(db_bs).__name__} → .df() shape={db_bs_df.shape}")
+ print(db_bs_df.iloc[:2, :5].to_string(index=False))
+
+yf_cf = yf_t.cashflow
+db_cf, db_cf_err = try_db(db_t.annual_cash_flow)
+db_cf_df = db_cf.df() if db_cf else None
+
+print(f"\n{PASS if not db_cf_err else FAIL} cashflow vs annual_cash_flow()")
+print(f" yfinance type={type(yf_cf).__name__} shape={yf_cf.shape}")
+print(yf_cf.iloc[:2, :2].to_string())
+if db_cf_err:
+ print(f" defeatbeta→ ERROR: {db_cf_err}")
+else:
+ print(f" defeatbeta → .df() shape={db_cf_df.shape}")
+ print(db_cf_df.iloc[:2, :5].to_string(index=False))
+
+# ── 3. VALUATION METRICS ──────────────────────────────────────────────────────
+section("3. VALUATION METRICS")
+
+db_pe, db_pe_err = try_db(db_t.ttm_pe)
+db_eps, db_eps_err = try_db(db_t.ttm_eps)
+db_mc, db_mc_err = try_db(db_t.market_capitalization)
+db_pb, db_pb_err = try_db(db_t.pb_ratio)
+db_ps, db_ps_err = try_db(db_t.ps_ratio)
+
+row("trailingPE → ttm_pe()", yf_t.info.get("trailingPE"), "float", db_pe, db_pe_err)
+row("trailingEps → ttm_eps()", yf_t.info.get("trailingEps"), "float", db_eps, db_eps_err)
+row("marketCap → market_cap()", yf_t.info.get("marketCap"), "int", db_mc, db_mc_err)
+row("priceToBook → pb_ratio()", yf_t.info.get("priceToBook"), "float", db_pb, db_pb_err)
+row("priceToSales → ps_ratio()", yf_t.info.get("priceToSalesTrailing12Months"), "float", db_ps, db_ps_err)
+
+# ── 4. FINANCIAL RATIOS ───────────────────────────────────────────────────────
+section("4. FINANCIAL RATIOS")
+
+db_roe, db_roe_err = try_db(db_t.roe)
+db_roa, db_roa_err = try_db(db_t.roa)
+db_beta, db_beta_err = try_db(db_t.beta)
+db_wacc, db_wacc_err = try_db(db_t.wacc)
+
+row("returnOnEquity → roe()", yf_t.info.get("returnOnEquity"), "float", db_roe, db_roe_err)
+row("returnOnAssets → roa()", yf_t.info.get("returnOnAssets"), "float", db_roa, db_roa_err)
+row("beta → beta()", yf_t.info.get("beta"), "float", db_beta, db_beta_err)
+row("N/A → wacc()", None, "N/A", db_wacc, db_wacc_err)
+
+# ── 5. GROWTH METRICS ─────────────────────────────────────────────────────────
+section("5. GROWTH METRICS")
+
+db_rg, db_rg_err = try_db(db_t.quarterly_revenue_yoy_growth)
+db_eg, db_eg_err = try_db(db_t.quarterly_eps_yoy_growth)
+db_nig, db_nig_err = try_db(db_t.quarterly_net_income_yoy_growth)
+
+row("revenueGrowth → quarterly_revenue_yoy_growth()", yf_t.info.get("revenueGrowth"), "float", db_rg, db_rg_err)
+row("earningsGrowth → quarterly_eps_yoy_growth()", yf_t.info.get("earningsGrowth"), "float", db_eg, db_eg_err)
+row("N/A → quarterly_net_income_yoy_growth()", None, "N/A", db_nig, db_nig_err)
+
+# ── 6. MARGIN METRICS ─────────────────────────────────────────────────────────
+section("6. MARGIN METRICS")
+
+db_nm, db_nm_err = try_db(db_t.quarterly_net_margin)
+db_gm, db_gm_err = try_db(db_t.quarterly_gross_margin)
+db_om, db_om_err = try_db(db_t.quarterly_operating_margin)
+
+row("profitMargins → quarterly_net_margin()", yf_t.info.get("profitMargins"), "float", db_nm, db_nm_err)
+row("grossMargins → quarterly_gross_margin()", yf_t.info.get("grossMargins"), "float", db_gm, db_gm_err)
+row("operatingMargins → quarterly_op_margin()", yf_t.info.get("operatingMargins"), "float", db_om, db_om_err)
+
+# ── 7. DIVIDENDS & SPLITS ─────────────────────────────────────────────────────
+section("7. DIVIDENDS & SPLITS")
+
+yf_div = yf_t.dividends
+db_div, db_div_err = try_db(db_t.dividends)
+
+print(f"\n{PASS if not db_div_err else FAIL} .dividends vs .dividends()")
+print(f" yfinance type={type(yf_div).__name__} len={len(yf_div)}")
+print(yf_div.tail(2).to_string())
+if db_div_err:
+ print(f" defeatbeta→ ERROR: {db_div_err}")
+else:
+ print(f" defeatbeta type={type(db_div).__name__} shape={db_div.shape}")
+ print(db_div.tail(2).to_string(index=False))
+
+yf_sp = yf_t.splits
+db_sp, db_sp_err = try_db(db_t.splits)
+
+print(f"\n{PASS if not db_sp_err else FAIL} .splits vs .splits()")
+print(f" yfinance type={type(yf_sp).__name__} len={len(yf_sp)}")
+print(yf_sp.tail(2).to_string())
+if db_sp_err:
+ print(f" defeatbeta→ ERROR: {db_sp_err}")
+else:
+ print(f" defeatbeta type={type(db_sp).__name__} shape={db_sp.shape}")
+ print(db_sp.tail(2).to_string(index=False))
+
+# ── 8. COMPANY INFO ───────────────────────────────────────────────────────────
+section("8. COMPANY INFO — .info vs .info()")
+
+yf_info = yf_t.info
+db_info, db_info_err = try_db(db_t.info)
+
+if db_info_err:
+ print(f" defeatbeta→ ERROR: {db_info_err}")
+else:
+ fields = [
+ ("sector", "sector", yf_info.get("sector")),
+ ("industry", "industry", yf_info.get("industry")),
+ ("employees", "full_time_employees", yf_info.get("fullTimeEmployees")),
+ ("website", "web_site", yf_info.get("website")),
+ # Note: yf longName = company name; longBusinessSummary = description
+ ("longName", None, yf_info.get("longName")),
+ ("longBusinessSummary", "long_business_summary", yf_info.get("longBusinessSummary", "")[:60]),
+ ]
+ print(f"\n {'Field':<30} {'yfinance':<35} {'defeatbeta'}")
+ print(f" {'-'*30} {'-'*35} {'-'*30}")
+ for label, db_col, yf_val in fields:
+ db_val = db_info[db_col].iloc[0] if db_col and db_col in db_info.columns else "—"
+ if isinstance(db_val, str) and len(db_val) > 40:
+ db_val = db_val[:40] + "…"
+ if isinstance(yf_val, str) and len(yf_val) > 34:
+ yf_val = yf_val[:34] + "…"
+ print(f" {label:<30} {str(yf_val):<35} {str(db_val)}")
+
+# ── SUMMARY ───────────────────────────────────────────────────────────────────
+print(f"\n{'='*68}")
+print(" ALL CHECKS COMPLETE")
+print(f"{'='*68}\n")
diff --git a/docs/yfinance.org b/docs/yfinance.org
new file mode 100644
index 0000000..709c4ce
--- /dev/null
+++ b/docs/yfinance.org
@@ -0,0 +1,302 @@
+#+title: yfinance API Reference
+#+subtitle: Version 1.3.0
+#+author: Ran Aroussi
+#+date: 2026-04-25
+
+* Overview
+
+yfinance offers a Pythonic way to fetch financial & market data from Yahoo! Finance.
+
+** Installation
+#+begin_src sh
+pip install yfinance
+#+end_src
+
+** Quick Start
+#+begin_src python
+import yfinance as yf
+
+# Single ticker
+dat = yf.Ticker("MSFT")
+dat.info
+dat.history(period='1mo')
+dat.option_chain(dat.options[0]).calls
+
+# Multiple tickers
+tickers = yf.Tickers('MSFT AAPL GOOG')
+tickers.tickers['MSFT'].info
+yf.download(['MSFT', 'AAPL', 'GOOG'], period='1mo')
+
+# Funds
+spy = yf.Ticker('SPY').funds_data
+spy.description
+spy.top_holdings
+#+end_src
+
+* Top-Level Functions
+
+| Function | Description |
+|--------------------------------+-------------------------------------------|
+| ~yf.download(tickers, ...)~ | Download market data for multiple tickers |
+| ~yf.enable_debug_mode()~ | Enable verbose debug logging |
+| ~yf.set_tz_cache_location(path)~ | Set timezone cache directory |
+| ~yf.screen(query)~ | Run equity/fund/ETF screener queries |
+
+* Config
+
+| Setting | Description |
+|---------------------------------+----------------------|
+| ~yf.config.debug.logging = True~ | Enable debug logging |
+| ~yf.config.user_agent = 'custom'~ | Custom user agent |
+
+* Ticker(symbol)
+
+** Price History
+
+| Property/Method | Returns |
+|---------------------------------------------+--------------------------------------|
+| ~.history(period, start, end, interval, ...)~ | OHLCV DataFrame |
+| ~.get_history_metadata()~ | Dict with currency, exchange, etc. |
+| ~.get_dividends()~ | Dividend history (Series) |
+| ~.dividends~ | Cached dividend history |
+| ~.get_splits()~ | Stock split history (Series) |
+| ~.splits~ | Cached split history |
+| ~.get_actions()~ | Dividends + splits combined (Series) |
+| ~.actions~ | Cached actions history |
+| ~.get_capital_gains()~ | Capital gains distributions (Series) |
+| ~.capital_gains~ | Cached capital gains |
+| ~.get_shares_full()~ | Full shares outstanding history |
+
+** Info & News
+
+| Property/Method | Returns |
+|-------------------------------+----------------------------------|
+| ~.get_info()~ / ~.info~ | Full company info dict |
+| ~.get_fast_info()~ / ~.fast_info~ | Quick-access key metrics |
+| ~.get_news()~ / ~.news~ | Recent news articles (DataFrame) |
+| ~.isin~ | ISIN identifier |
+| ~.get_isin()~ | ISIN as string |
+
+** Financial Statements
+
+| Property/Method | Returns |
+|------------------------+-----------------------------------------|
+| ~.income_stmt~ | Annual income statement |
+| ~.quarterly_income_stmt~ | Quarterly income statement |
+| ~.ttm_income_stmt~ | Trailing-twelve-months income statement |
+| ~.balance_sheet~ | Annual balance sheet |
+| ~.cashflow~ | Annual cash flow statement |
+| ~.quarterly_cashflow~ | Quarterly cash flow |
+| ~.ttm_cashflow~ | TTM cash flow |
+| ~.get_income_stmt()~ | Alias for income_stmt |
+| ~.get_balance_sheet()~ | Alias for balance_sheet |
+| ~.get_cashflow()~ | Alias for cashflow |
+
+** Earnings & Calendar
+
+| Property/Method | Returns |
+|-----------------------+--------------------------------------|
+| ~.earnings~ | Annual earnings summary |
+| ~.calendar~ | Upcoming earnings/dividends dates |
+| ~.get_earnings_dates()~ | Historical & upcoming earnings dates |
+| ~.earnings_dates~ | Cached earnings dates |
+| ~.get_sec_filings()~ | SEC filing history |
+| ~.sec_filings~ | Cached SEC filings |
+
+** Analysis & Estimates
+
+| Property/Method | Returns |
+|--------------------------------+------------------------------------|
+| ~.get_recommendations()~ | Historical analyst recommendations |
+| ~.recommendations~ | Cached recommendations |
+| ~.get_recommendations_summary()~ | Summary of buy/hold/sell |
+| ~.recommendations_summary~ | Cached summary |
+| ~.get_upgrades_downgrades()~ | Analyst upgrades/downgrades |
+| ~.upgrades_downgrades~ | Cached upgrades/downgrades |
+| ~.get_sustainability()~ | ESG scores |
+| ~.sustainability~ | Cached ESG |
+| ~.get_analyst_price_targets()~ | Price target estimates |
+| ~.analyst_price_targets~ | Cached price targets |
+| ~.get_earnings_estimate()~ | Earnings estimates |
+| ~.earnings_estimate~ | Cached earnings estimates |
+| ~.get_revenue_estimate()~ | Revenue estimates |
+| ~.revenue_estimate~ | Cached revenue estimates |
+| ~.get_earnings_history()~ | Earnings surprise history |
+| ~.earnings_history~ | Cached earnings history |
+| ~.get_eps_trend()~ | EPS trend revisions |
+| ~.eps_trend~ | Cached EPS trend |
+| ~.get_eps_revisions()~ | EPS revision counts |
+| ~.eps_revisions~ | Cached EPS revisions |
+| ~.get_growth_estimates()~ | Growth rate estimates |
+| ~.growth_estimates~ | Cached growth estimates |
+
+** Holdings & Insider Activity
+
+| Property/Method | Returns |
+|---------------------------------------------------------+-----------------------------------|
+| ~.get_funds_data()~ / ~.funds_data~ | Fund info (for ETFs/mutual funds) |
+| ~.get_insider_purchases()~ / ~.insider_purchases~ | Insider purchase records |
+| ~.get_insider_transactions()~ / ~.insider_transactions~ | Insider trade history |
+| ~.get_insider_roster_holders()~ / ~.insider_roster_holders~ | Insider roster |
+| ~.get_major_holders()~ / ~.major_holders~ | Ownership breakdown |
+| ~.get_institutional_holders()~ / ~.institutional_holders~ | Institutional holders |
+| ~.get_mutualfund_holders()~ / ~.mutualfund_holders~ | Mutual fund holders |
+
+** Options
+
+| Method | Returns |
+|---------------------+-------------------------------------|
+| ~.options~ | Tuple of available expiration dates |
+| ~.option_chain(date)~ | Calls & puts DataFrame for a date |
+| | |
+
+* Tickers('SYM1 SYM2 ...')
+
+Multiple tickers class.
+
+| Property | Returns |
+|---------------------------+----------------------------------|
+| ~.tickers['SYM'].info~ | Access individual Ticker objects |
+| ~.tickers['SYM'].history()~ | Get history per ticker |
+
+* Market(market_code)
+
+Market summary class.
+
+| Method/Property | Returns |
+|-----------------+---------------------------|
+| ~.status~ | Market open/closed status |
+| ~.summary()~ | Market summary data |
+
+* Calendars
+
+Calendar events class.
+
+| Method | Returns |
+|--------------+-------------------|
+| ~.earnings()~ | Earnings calendar |
+| ~.dividends()~ | Dividend calendar |
+| ~.splits()~ | Splits calendar |
+| ~.ipo()~ | IPO calendar |
+
+* Search(query)
+
+Search class for quotes and news.
+
+| Property/Method | Returns |
+|-----------------+----------------------|
+| ~.quotes~ | Search result quotes |
+| ~.news~ | Search result news |
+| ~.lists~ | Watchlists/lists |
+| ~.nav~ | Navigation results |
+| ~.research~ | Research reports |
+| ~.get_quotes()~ | Fetch quotes |
+| ~.get_news()~ | Fetch news |
+
+* Lookup(query)
+
+Ticker lookup class.
+
+| Method | Returns |
+|---------------+------------------------|
+| ~.get_quotes()~ | Lookup matching quotes |
+
+* WebSocket(symbols, ...)
+
+Live streaming data (synchronous).
+
+| Method | Description |
+|-----------------------+---------------------------|
+| ~.connect()~ | Open WebSocket connection |
+| ~.subscribe(symbols)~ | Subscribe to symbols |
+| ~.unsubscribe(symbols)~ | Unsubscribe from symbols |
+| ~.close()~ | Close connection |
+
+* AsyncWebSocket(symbols, ...)
+
+Live streaming data (asynchronous).
+
+Same methods as ~WebSocket~, but async.
+
+* Sector(sector_key)
+
+Sector information class.
+
+| Property/Method | Returns |
+|-----------------+--------------------------|
+| ~.key~ | Sector identifier |
+| ~.name~ | Sector name |
+| ~.symbol~ | Associated ETF symbol |
+| ~.top_etfs~ | Top ETFs in sector |
+| ~.top_companies~ | Top companies in sector |
+| ~.industries~ | Industries within sector |
+
+* Industry(industry_key)
+
+Industry information class.
+
+| Property/Method | Returns |
+|-----------------+---------------------------|
+| ~.key~ | Industry identifier |
+| ~.name~ | Industry name |
+| ~.sector_key~ | Parent sector |
+| ~.top_companies~ | Top companies in industry |
+
+* Query Builders (Screener)
+
+| Class | Description |
+|------------------+------------------------------------|
+| ~EquityQuery~ | Build equity filter queries |
+| ~FundQuery~ | Build mutual fund filter queries |
+| ~ETFQuery~ | Build ETF filter queries |
+| ~yf.screen(query)~ | Execute a query and return results |
+
+* download() Parameters
+
+#+begin_src python
+yf.download(
+ tickers, # str or list of tickers
+ period="1mo", # 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max
+ start=None, # datetime or str "YYYY-MM-DD"
+ end=None,
+ interval="1d", # 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo
+ group_by="column", # "column" or "ticker"
+ auto_adjust=False,
+ back_adjust=False,
+ repair=False, # Enable price repair
+ keepna=False,
+ proxy=None,
+ timeout=10,
+ threads=True,
+ progress=True,
+)
+#+end_src
+
+* Advanced Features
+
+** Logging
+Enable debug logging:
+#+begin_src python
+yf.config.debug.logging = True
+#+end_src
+
+** Caching
+- Persistent cache is enabled by default
+- Set custom cache location: ~yf.set_tz_cache_location(path)~
+
+** Price Repair
+- Enable with ~repair=True~ in ~download()~ or ~history()~
+- Fixes NaN values, bad splits, and dividend adjustments
+- Dividend repair also available
+
+** Multi-Level Column Index
+- When downloading multiple tickers, columns are MultiIndex
+- ~group_by="column"~ or ~group_by="ticker"~
+
+* Legal Disclaimer
+
+Yahoo!, Y!Finance, and Yahoo! finance are registered trademarks of Yahoo, Inc.
+
+yfinance is /not/ affiliated, endorsed, or vetted by Yahoo, Inc. It's an open-source tool that uses Yahoo's publicly available APIs, and is intended for research and educational purposes.
+
+You should refer to Yahoo!'s terms of use for details on your rights to use the actual data downloaded. The Yahoo! finance API is intended for personal use only.
diff --git a/download_data.py b/download_data.py
new file mode 100644
index 0000000..11af934
--- /dev/null
+++ b/download_data.py
@@ -0,0 +1,75 @@
+"""
+One-time download of all defeatbeta parquet files + company_tickers.json.
+Run this once; after that use offline.py for zero-network Ticker() calls.
+
+ uv run python download_data.py
+ uv run python download_data.py --out data/parquet # custom directory
+"""
+import argparse
+import sys
+from pathlib import Path
+
+import requests
+
+BASE = "https://huggingface.co/datasets/defeatbeta/yahoo-finance-data/resolve/main"
+
+PARQUET_TABLES = [
+ "stock_profile",
+ "stock_officers",
+ "stock_tailing_eps",
+ "stock_earning_calendar",
+ "stock_statement",
+ "stock_prices",
+ "stock_dividend_events",
+ "stock_split_events",
+ "exchange_rate",
+ "daily_treasury_yield",
+ "stock_earning_call_transcripts",
+ "stock_news",
+ "stock_revenue_breakdown",
+ "stock_shares_outstanding",
+ "stock_sec_filing",
+]
+
+EXTRA_FILES = [
+ ("data/company_tickers.json", "company_tickers.json"),
+]
+
+
+def download(url: str, dest: Path, label: str) -> None:
+ if dest.exists():
+ print(f" skip {label} ({dest.stat().st_size / 1e6:.1f} MB on disk)")
+ return
+ print(f" fetch {label} ...", end="", flush=True)
+ with requests.get(url, stream=True, timeout=60) as r:
+ r.raise_for_status()
+ tmp = dest.with_suffix(".tmp")
+ with open(tmp, "wb") as f:
+ for chunk in r.iter_content(chunk_size=8 * 1024 * 1024):
+ f.write(chunk)
+ tmp.rename(dest)
+ print(f" {dest.stat().st_size / 1e6:.1f} MB")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--out", default="data/parquet", help="local output directory")
+ args = parser.parse_args()
+
+ out = Path(args.out)
+ out.mkdir(parents=True, exist_ok=True)
+ print(f"Saving to: {out.resolve()}\n")
+
+ for table in PARQUET_TABLES:
+ url = f"{BASE}/data/{table}.parquet"
+ download(url, out / f"{table}.parquet", table)
+
+ for remote_path, local_name in EXTRA_FILES:
+ url = f"{BASE}/{remote_path}"
+ download(url, out / local_name, local_name)
+
+ print(f"\nDone. {sum(1 for _ in out.iterdir())} files in {out.resolve()}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/offline.py b/offline.py
new file mode 100644
index 0000000..ece4937
--- /dev/null
+++ b/offline.py
@@ -0,0 +1,67 @@
+"""
+Patch defeatbeta_api to read from local parquet files with zero network.
+
+Usage:
+ from offline import enable_offline
+ enable_offline("data/parquet") # call once before first Ticker()
+
+ from defeatbeta_api.data.ticker import Ticker
+ t = Ticker("AAPL")
+ t.price() # reads local file, no HTTP
+
+Note: the one-time welcome banner on first import calls get_data_update_time()
+once. After that, nothing touches the network.
+"""
+from pathlib import Path
+
+
+def enable_offline(parquet_dir: str = "data/parquet") -> None:
+ local_dir = Path(parquet_dir).resolve()
+
+ # Importing these submodules triggers defeatbeta_api/__init__.py on first
+ # run (prints the welcome banner — one network call). After that it's a
+ # no-op because _welcome_printed is True.
+ from defeatbeta_api.client.hugging_face_client import HuggingFaceClient
+ import defeatbeta_api.client.duckdb_client as _duckdb_mod
+ from defeatbeta_api.client.duckdb_client import DuckDBClient
+ from defeatbeta_api.client.duckdb_conf import Configuration
+ from defeatbeta_api.data.company_meta import CompanyMeta
+ from defeatbeta_api.utils.util import validate_memory_limit
+
+ # 1. Redirect every table URL to a local parquet file
+ def _local_url(self, table: str) -> str:
+ path = local_dir / f"{table}.parquet"
+ if not path.exists():
+ raise FileNotFoundError(
+ f"Local parquet not found: {path}\n"
+ f"Run download_data.py first."
+ )
+ return str(path)
+
+ HuggingFaceClient.get_url_path = _local_url
+
+ # 2. Return a fixed update time (used by beta() and the welcome banner)
+ HuggingFaceClient.get_data_update_time = lambda self: "offline"
+
+ # 3. Skip the startup cache-validation (hits HuggingFace spec.json)
+ DuckDBClient._validate_httpfs_cache = lambda self: None
+
+ # 4. Skip "INSTALL cache_httpfs FROM community" (hits DuckDB extension registry)
+ # and all the cache_httpfs SET GLOBAL lines that follow — not needed for
+ # local files. Keep only memory and thread settings.
+ def _minimal_settings(self):
+ return [
+ f"SET GLOBAL memory_limit = '{validate_memory_limit(self.memory_limit)}'",
+ f"SET GLOBAL threads = {self.threads}",
+ ]
+
+ Configuration.get_duckdb_settings = _minimal_settings
+
+ # 5. Redirect company_tickers.json to local file
+ CompanyMeta.COMPANY_TICKERS_URL = str(local_dir / "company_tickers.json")
+
+ # 6. Reset the DuckDB singleton so the next Ticker() call reinitialises
+ # using the patched Configuration (no cache_httpfs install/load)
+ _duckdb_mod._instance = None
+
+ print(f"[offline] defeatbeta_api patched → reading from {local_dir}")
diff --git a/persistent_cache.py b/persistent_cache.py
new file mode 100644
index 0000000..4c3db2d
--- /dev/null
+++ b/persistent_cache.py
@@ -0,0 +1,34 @@
+"""
+Use the persistent defeatbeta httpfs cache at ~/.cache/defeatbeta/.
+
+Import this at the top of any notebook or script before using Ticker().
+Run warmup_cache.py once first to populate the cache.
+
+ from persistent_cache import enable_persistent_cache
+ enable_persistent_cache()
+
+ from defeatbeta_api.data.ticker import Ticker
+ t = Ticker("AAPL")
+"""
+from pathlib import Path
+
+CACHE_DIR = Path.home() / ".cache" / "defeatbeta"
+
+def enable_persistent_cache() -> None:
+ import defeatbeta_api.utils.util as _util
+ import defeatbeta_api.client.duckdb_client as _duckdb_mod
+ from defeatbeta_api.client.duckdb_conf import Configuration
+
+ if not CACHE_DIR.exists():
+ raise RuntimeError(
+ f"Cache directory not found: {CACHE_DIR}\n"
+ "Run warmup_cache.py first to populate it."
+ )
+
+ # Redirect cache directory to the persistent location
+ _util.validate_httpfs_cache_directory = lambda: str(CACHE_DIR)
+
+ # Reset singleton so DuckDB reinitialises pointing at the new cache dir
+ _duckdb_mod._instance = None
+
+ print(f"[persistent_cache] cache → {CACHE_DIR}")
diff --git a/pyproject.toml b/pyproject.toml
index 0e9d7ef..cca4c58 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,7 +37,7 @@ dependencies = [
"torch>=2.0.0",
"transformers>=4.0.0",
"tscv>=0.1.3",
- "vectorbt>=1.0.0",
+ "vectorbt[full,rust]>=1.0.0",
"xgboost>=3.2.0",
"yfinance>=1.3.0",
"defeatbeta-api>=0.0.45",
diff --git a/quant.py b/quant.py
new file mode 100644
index 0000000..43cbd1d
--- /dev/null
+++ b/quant.py
@@ -0,0 +1,599 @@
+import marimo
+
+__generated_with = "0.23.2"
+app = marimo.App()
+
+
+@app.cell
+def _():
+ import marimo as mo
+
+ return (mo,)
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ # Quant Trading Scaffold
+ ## Data Ingestion → Indicators → Walk-Forward ML → Backtesting → Tearsheet
+
+ Pipeline:
+ 1. **Ingest** OHLCV data via yfinance
+ 2. **Engineer features** — momentum, trend, volatility, volume indicators
+ 3. **Label** — binary classification (next-N-day return > 0)
+ 4. **Walk-forward split** with purging (no leakage)
+ 5. **Train** XGBoost classifier per fold
+ 6. **Evaluate** with quantstats tearsheet
+ """)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ ## 1. Config & Imports
+ """)
+ return
+
+
+@app.cell
+def _():
+ from __future__ import annotations
+ import warnings
+ warnings.filterwarnings("ignore", category=FutureWarning)
+
+ import numpy as np
+ import pandas as pd
+ import pandas_ta as ta
+ import yfinance as yf
+ import plotly.graph_objects as go
+ from plotly.subplots import make_subplots
+ from sklearn.model_selection import TimeSeriesSplit
+ from sklearn.metrics import accuracy_score, classification_report
+ from xgboost import XGBClassifier
+ import quantstats as qs
+
+ # ── Config ──────────────────────────────────────────────────────
+ TICKER = "AAPL"
+ START = "2015-01-01"
+ END = "2025-12-31"
+ HORIZON = 5 # predict N-day forward return
+ PURGE_GAP = 5 # gap between train/test to prevent leakage
+ N_SPLITS = 5 # walk-forward folds
+ TRAIN_MIN = 504 # ~2 years minimum training window
+
+ print(f"Config: {TICKER} | {START}→{END} | horizon={HORIZON}d | {N_SPLITS} folds")
+ return (
+ END,
+ HORIZON,
+ N_SPLITS,
+ PURGE_GAP,
+ START,
+ TICKER,
+ TRAIN_MIN,
+ XGBClassifier,
+ accuracy_score,
+ classification_report,
+ go,
+ make_subplots,
+ np,
+ pd,
+ qs,
+ ta,
+ yf,
+ )
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ ## 2. Data Ingestion
+ """)
+ return
+
+
+@app.cell
+def _(END, START, TICKER, pd, yf):
+ raw = yf.download(TICKER, start=START, end=END, auto_adjust=True)
+ # yfinance may return MultiIndex columns for single ticker — flatten
+ if isinstance(raw.columns, pd.MultiIndex):
+ raw.columns = raw.columns.droplevel("Ticker")
+ raw.index = pd.DatetimeIndex(raw.index)
+ df = raw.copy()
+ print(f"Downloaded {len(df)} bars: {df.index[0].date()} → {df.index[-1].date()}")
+ df.tail(3)
+ return (df,)
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ ## 3. Feature Engineering — Technical Indicators
+
+ We compute features across 4 categories:
+ - **Momentum**: RSI, MACD, Stochastic, Williams %R, ROC
+ - **Trend**: SMA/EMA crossovers, ADX, Ichimoku
+ - **Volatility**: Bollinger Bands, ATR, Keltner Channels
+ - **Volume**: OBV, MFI, Accumulation/Distribution
+ """)
+ return
+
+
+@app.cell
+def _(df, ta):
+ # ── Momentum ────────────────────────────────────────────────────
+ df["rsi_14"] = ta.rsi(df["Close"], length=14)
+ df["rsi_7"] = ta.rsi(df["Close"], length=7)
+
+ macd = ta.macd(df["Close"], fast=12, slow=26, signal=9)
+ df["macd"] = macd.iloc[:, 0] # MACD line
+ df["macd_signal"] = macd.iloc[:, 1] # signal line
+ df["macd_hist"] = macd.iloc[:, 2] # histogram
+
+ stoch = ta.stoch(df["High"], df["Low"], df["Close"])
+ df["stoch_k"] = stoch.iloc[:, 0]
+ df["stoch_d"] = stoch.iloc[:, 1]
+
+ df["willr_14"] = ta.willr(df["High"], df["Low"], df["Close"], length=14)
+ df["roc_10"] = ta.roc(df["Close"], length=10)
+ df["roc_21"] = ta.roc(df["Close"], length=21)
+ df["mom_10"] = ta.mom(df["Close"], length=10)
+
+ # ── Trend ───────────────────────────────────────────────────────
+ df["sma_20"] = ta.sma(df["Close"], length=20)
+ df["sma_50"] = ta.sma(df["Close"], length=50)
+ df["sma_200"] = ta.sma(df["Close"], length=200)
+ df["ema_12"] = ta.ema(df["Close"], length=12)
+ df["ema_26"] = ta.ema(df["Close"], length=26)
+
+ # crossover features (price relative to MAs)
+ df["close_over_sma20"] = (df["Close"] / df["sma_20"]) - 1
+ df["close_over_sma50"] = (df["Close"] / df["sma_50"]) - 1
+ df["close_over_sma200"] = (df["Close"] / df["sma_200"]) - 1
+ df["sma20_over_sma50"] = (df["sma_20"] / df["sma_50"]) - 1
+ df["sma50_over_sma200"] = (df["sma_50"] / df["sma_200"]) - 1
+
+ adx = ta.adx(df["High"], df["Low"], df["Close"], length=14)
+ df["adx"] = adx.iloc[:, 0]
+ df["di_plus"] = adx.iloc[:, 1]
+ df["di_minus"] = adx.iloc[:, 2]
+
+ # ── Volatility ──────────────────────────────────────────────────
+ bbands = ta.bbands(df["Close"], length=20, std=2)
+ df["bb_upper"] = bbands.iloc[:, 0]
+ df["bb_mid"] = bbands.iloc[:, 1]
+ df["bb_lower"] = bbands.iloc[:, 2]
+ df["bb_width"] = bbands.iloc[:, 3]
+ df["bb_pctb"] = bbands.iloc[:, 4] # %B: where price is within bands
+
+ df["atr_14"] = ta.atr(df["High"], df["Low"], df["Close"], length=14)
+ df["atr_pct"] = df["atr_14"] / df["Close"] # normalized ATR
+
+ kc = ta.kc(df["High"], df["Low"], df["Close"], length=20)
+ df["kc_upper"] = kc.iloc[:, 0]
+ df["kc_lower"] = kc.iloc[:, 1]
+
+ # volatility: rolling std of returns
+ df["vol_10"] = df["Close"].pct_change().rolling(10).std()
+ df["vol_21"] = df["Close"].pct_change().rolling(21).std()
+
+ # ── Volume ──────────────────────────────────────────────────────
+ df["obv"] = ta.obv(df["Close"], df["Volume"])
+ df["obv_sma20"] = ta.sma(df["obv"], length=20)
+ df["mfi_14"] = ta.mfi(df["High"], df["Low"], df["Close"], df["Volume"], length=14)
+ ad = ta.ad(df["High"], df["Low"], df["Close"], df["Volume"])
+ df["ad_line"] = ad
+
+ # volume relative to average
+ df["vol_ratio_20"] = df["Volume"] / df["Volume"].rolling(20).mean()
+
+ # ── Returns features ────────────────────────────────────────────
+ df["ret_1d"] = df["Close"].pct_change(1)
+ df["ret_5d"] = df["Close"].pct_change(5)
+ df["ret_10d"] = df["Close"].pct_change(10)
+ df["ret_21d"] = df["Close"].pct_change(21)
+
+ print(f"Total columns after feature engineering: {len(df.columns)}")
+ df.tail(3)
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ ## 4. Labeling — Forward Return Classification
+
+ Target: is the N-day forward return positive? (buy signal = 1, sell/hold signal = 0)
+ """)
+ return
+
+
+@app.cell
+def _(HORIZON, df):
+ # forward return (what we're predicting)
+ df["fwd_ret"] = df["Close"].pct_change(HORIZON).shift(-HORIZON)
+ df["label"] = (df["fwd_ret"] > 0).astype(int)
+
+ # ── Define feature columns (exclude raw OHLCV, target, and non-stationary cols)
+ EXCLUDE = {
+ "Open", "High", "Low", "Close", "Volume",
+ "fwd_ret", "label",
+ "sma_20", "sma_50", "sma_200", "ema_12", "ema_26", # non-stationary
+ "bb_upper", "bb_mid", "bb_lower", # non-stationary
+ "kc_upper", "kc_lower", # non-stationary
+ "obv", "obv_sma20", "ad_line", # non-stationary
+ }
+ FEATURES = [c for c in df.columns if c not in EXCLUDE]
+
+ # drop rows with NaN (from indicator warm-up + forward label)
+ model_df = df[FEATURES + ["label", "fwd_ret"]].dropna()
+
+ print(f"Features: {len(FEATURES)}")
+ print(f"Usable rows: {len(model_df)} ({model_df.index[0].date()} → {model_df.index[-1].date()})")
+ print(f"Label balance: {model_df['label'].value_counts(normalize=True).to_dict()}")
+ print(f"\nFeature list:\n{FEATURES}")
+ return FEATURES, model_df
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ ## 5. Walk-Forward Split with Purge Gap
+
+ Time series data **cannot** use random k-fold — future data would leak into training.
+
+ We use **expanding-window walk-forward** with a **purge gap** between train/test:
+
+ ```
+ Fold 1: [====TRAIN====]--gap--[TEST]
+ Fold 2: [========TRAIN========]--gap--[TEST]
+ Fold 3: [============TRAIN============]--gap--[TEST]
+ ```
+
+ The gap prevents label leakage from overlapping forward-return windows.
+ """)
+ return
+
+
+@app.cell
+def _(FEATURES, N_SPLITS, PURGE_GAP, TRAIN_MIN, go, model_df, np):
+ def walk_forward_splits(n_samples: int, n_splits: int, test_size: int=126, purge_gap: int=5, min_train: int=504):
+ """
+ Expanding-window walk-forward with purge gap.
+
+ Yields (train_idx, test_idx) index arrays.
+ test_size: ~6 months of trading days
+ min_train: ~2 years of trading days
+ purge_gap: days between train end and test start
+ """
+ total_test = n_splits * test_size
+ if min_train + total_test + n_splits * purge_gap > n_samples:
+ raise ValueError(f'Not enough data for {n_splits} splits. Need {min_train + total_test + n_splits * purge_gap}, have {n_samples}')
+ for i in range(n_splits):
+ test_end = n_samples - (n_splits - 1 - i) * test_size
+ test_start = test_end - test_size
+ train_end = test_start - purge_gap
+ train_start = 0
+ train_idx = np.arange(train_start, train_end)
+ test_idx = np.arange(test_start, test_end)
+ yield (train_idx, test_idx) # expanding window (use max(0, train_end - fixed_window) for sliding)
+ X = model_df[FEATURES].values
+ y = model_df['label'].values
+ dates = model_df.index
+ _fig = go.Figure()
+ for _fold, (_tr_idx, _te_idx) in enumerate(walk_forward_splits(len(X), N_SPLITS, purge_gap=PURGE_GAP, min_train=TRAIN_MIN)):
+ _fig.add_trace(go.Scatter(x=[dates[_tr_idx[0]], dates[_tr_idx[-1]]], y=[_fold, _fold], mode='lines', line=dict(color='steelblue', width=8), name=f'Train {_fold}' if _fold == 0 else None, showlegend=_fold == 0))
+ # ── Visualize the splits ────────────────────────────────────────
+ _fig.add_trace(go.Scatter(x=[dates[_te_idx[0]], dates[_te_idx[-1]]], y=[_fold, _fold], mode='lines', line=dict(color='coral', width=8), name=f'Test {_fold}' if _fold == 0 else None, showlegend=_fold == 0))
+ print(f'Fold {_fold}: train {dates[_tr_idx[0]].date()}→{dates[_tr_idx[-1]].date()} ({len(_tr_idx)}d) | test {dates[_te_idx[0]].date()}→{dates[_te_idx[-1]].date()} ({len(_te_idx)}d)')
+ _fig.update_layout(title='Walk-Forward Splits', yaxis_title='Fold', height=300)
+ _fig.show()
+ return X, dates, walk_forward_splits, y
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ ## 6. Train XGBoost per Fold — Walk-Forward
+
+ Train on expanding window, predict test fold, collect out-of-sample predictions.
+ """)
+ return
+
+
+@app.cell
+def _(
+ N_SPLITS,
+ PURGE_GAP,
+ TRAIN_MIN,
+ X,
+ XGBClassifier,
+ accuracy_score,
+ classification_report,
+ dates,
+ model_df,
+ walk_forward_splits,
+ y,
+):
+ oos_preds = [] # out-of-sample predictions
+ oos_proba = [] # predicted probabilities
+ oos_labels = []
+ oos_dates = []
+ oos_fwd_ret = []
+ fold_metrics = []
+ for _fold, (_tr_idx, _te_idx) in enumerate(walk_forward_splits(len(X), N_SPLITS, purge_gap=PURGE_GAP, min_train=TRAIN_MIN)):
+ X_train, y_train = (X[_tr_idx], y[_tr_idx])
+ X_test, y_test = (X[_te_idx], y[_te_idx])
+ model = XGBClassifier(n_estimators=300, max_depth=4, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=1.0, random_state=42, eval_metric='logloss', early_stopping_rounds=30)
+ model.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=False)
+ preds = model.predict(X_test)
+ proba = model.predict_proba(X_test)[:, 1]
+ acc = accuracy_score(y_test, preds)
+ oos_preds.extend(preds)
+ oos_proba.extend(proba)
+ oos_labels.extend(y_test)
+ oos_dates.extend(dates[_te_idx])
+ oos_fwd_ret.extend(model_df['fwd_ret'].values[_te_idx])
+ fold_metrics.append({'fold': _fold, 'accuracy': acc, 'train_size': len(_tr_idx), 'test_size': len(_te_idx)})
+ print(f'Fold {_fold}: acc={acc:.3f} | train={len(_tr_idx)} | test={len(_te_idx)}')
+ print(f'\nOverall OOS accuracy: {accuracy_score(oos_labels, oos_preds):.3f}')
+ print(classification_report(oos_labels, oos_preds, target_names=['SELL/HOLD', 'BUY']))
+ return model, oos_dates, oos_fwd_ret, oos_preds, oos_proba
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ ## 7. Feature Importance (Last Fold)
+ """)
+ return
+
+
+@app.cell
+def _(FEATURES, go, model, pd):
+ imp = pd.Series(model.feature_importances_, index=FEATURES).sort_values(ascending=True)
+ _fig = go.Figure(go.Bar(x=imp.tail(20), y=imp.tail(20).index, orientation='h'))
+ _fig.update_layout(title='Top 20 Feature Importances (last fold)', height=500, margin=dict(l=150))
+ _fig.show()
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ ## 8. Strategy Simulation — Signal → Returns
+
+ Convert model predictions to a strategy equity curve:
+ - **Signal = 1 (BUY)**: go long (earn the market return)
+ - **Signal = 0 (SELL/HOLD)**: stay in cash (earn 0)
+
+ Compare against buy-and-hold benchmark.
+ """)
+ return
+
+
+@app.cell
+def _(df, go, oos_dates, oos_fwd_ret, oos_preds, oos_proba, pd):
+ # Build strategy returns series from OOS predictions
+ strat = pd.DataFrame({'date': oos_dates, 'signal': oos_preds, 'proba': oos_proba, 'fwd_ret': oos_fwd_ret}).set_index('date')
+ daily_ret = df['Close'].pct_change().reindex(strat.index)
+ strat['strat_ret'] = daily_ret * strat['signal']
+ strat['bench_ret'] = daily_ret
+ strat['strat_equity'] = (1 + strat['strat_ret']).cumprod()
+ strat['bench_equity'] = (1 + strat['bench_ret']).cumprod()
+ _fig = go.Figure()
+ # daily returns: we use daily close-to-close returns, masked by signal
+ # align with actual daily returns (not forward returns) for proper equity curve
+ _fig.add_trace(go.Scatter(x=strat.index, y=strat['strat_equity'], name='Strategy', line=dict(color='steelblue')))
+ _fig.add_trace(go.Scatter(x=strat.index, y=strat['bench_equity'], name='Buy & Hold', line=dict(color='gray', dash='dot')))
+ # strategy return: market return when signal=1, 0 when signal=0
+ in_market = strat['signal'] == 1
+ _changes = in_market.astype(int).diff().fillna(0)
+ entries = strat.index[_changes == 1]
+ # cumulative
+ exits = strat.index[_changes == -1]
+ if in_market.iloc[0]:
+ entries = entries.insert(0, strat.index[0])
+ # plot
+ if in_market.iloc[-1]:
+ exits = exits.append(pd.DatetimeIndex([strat.index[-1]]))
+ for ent, ext in zip(entries, exits):
+ _fig.add_vrect(x0=ent, x1=ext, fillcolor='green', opacity=0.07, line_width=0)
+ # shade buy signals
+ _fig.update_layout(title='Strategy vs Buy & Hold (OOS)', yaxis_title='Equity ($1 start)', height=450)
+ _fig.show()
+ print(f"Strategy final: ${strat['strat_equity'].iloc[-1]:.2f}")
+ # align: if first signal is 1, start from beginning
+ print(f"Benchmark final: ${strat['bench_equity'].iloc[-1]:.2f}")
+ return (strat,)
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ ## 9. QuantStats Tearsheet
+
+ Full performance report: Sharpe, Sortino, max drawdown, rolling metrics, monthly heatmap.
+ """)
+ return
+
+
+@app.cell
+def _(pd, qs, strat):
+ # quantstats expects a returns series with datetime index
+ strategy_returns = strat["strat_ret"].copy()
+ strategy_returns.index = pd.DatetimeIndex(strategy_returns.index)
+ benchmark_returns = strat["bench_ret"].copy()
+ benchmark_returns.index = pd.DatetimeIndex(benchmark_returns.index)
+
+ qs.extend_pandas()
+
+ # key metrics
+ print("=" * 50)
+ print("STRATEGY METRICS (out-of-sample)")
+ print("=" * 50)
+ print(f"Sharpe: {qs.stats.sharpe(strategy_returns):.2f}")
+ print(f"Sortino: {qs.stats.sortino(strategy_returns):.2f}")
+ print(f"Max Drawdown: {qs.stats.max_drawdown(strategy_returns):.2%}")
+ print(f"CAGR: {qs.stats.cagr(strategy_returns):.2%}")
+ print(f"Calmar: {qs.stats.calmar(strategy_returns):.2f}")
+ print(f"Win Rate: {qs.stats.win_rate(strategy_returns):.2%}")
+ print(f"Volatility: {qs.stats.volatility(strategy_returns):.2%}")
+ print(f"Avg Win: {qs.stats.avg_win(strategy_returns):.4f}")
+ print(f"Avg Loss: {qs.stats.avg_loss(strategy_returns):.4f}")
+ print(f"Profit Factor:{qs.stats.profit_factor(strategy_returns):.2f}")
+ print("=" * 50)
+ return benchmark_returns, strategy_returns
+
+
+@app.cell
+def _(TICKER, benchmark_returns, qs, strategy_returns):
+ # full HTML tearsheet — saved to file + displayed inline
+ qs.reports.html(strategy_returns, benchmark=benchmark_returns,
+ title=f"{TICKER} ML Signal Strategy (OOS Walk-Forward)",
+ output="tearsheet.html")
+ print("Tearsheet saved to tearsheet.html")
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ ## 10. Signal Dashboard — Price + Indicators + Buy/Sell Signals
+ """)
+ return
+
+
+@app.cell
+def _(TICKER, df, go, make_subplots, strat):
+ # show last fold's test period with signals overlaid on price
+ last_test_dates = strat.index[-126:] # last ~6 months
+ viz = df.loc[last_test_dates].copy()
+ sig = strat.loc[last_test_dates]
+ _fig = make_subplots(rows=4, cols=1, shared_xaxes=True, row_heights=[0.4, 0.2, 0.2, 0.2], vertical_spacing=0.03, subplot_titles=['Price + Bollinger Bands + Signals', 'RSI(14)', 'MACD', 'Volume'])
+ _fig.add_trace(go.Candlestick(x=viz.index, open=viz['Open'], high=viz['High'], low=viz['Low'], close=viz['Close'], name='OHLC', increasing_line_color='steelblue', decreasing_line_color='salmon'), row=1, col=1)
+ _fig.add_trace(go.Scatter(x=viz.index, y=viz['bb_upper'], line=dict(color='gray', width=1, dash='dot'), name='BB Upper'), row=1, col=1)
+ _fig.add_trace(go.Scatter(x=viz.index, y=viz['bb_lower'], line=dict(color='gray', width=1, dash='dot'), name='BB Lower', fill='tonexty', fillcolor='rgba(128,128,128,0.05)'), row=1, col=1)
+ _fig.add_trace(go.Scatter(x=viz.index, y=viz['sma_50'], line=dict(color='orange', width=1), name='SMA 50'), row=1, col=1)
+ buy_mask = sig['signal'] == 1
+ _changes = buy_mask.astype(int).diff()
+ buy_entries = sig.index[_changes == 1]
+ # Row 1: Candlestick + BB + signals
+ sell_entries = sig.index[_changes == -1]
+ if len(buy_entries):
+ _fig.add_trace(go.Scatter(x=buy_entries, y=viz.loc[buy_entries, 'Low'] * 0.995, mode='markers', marker=dict(symbol='triangle-up', size=10, color='green'), name='BUY'), row=1, col=1)
+ if len(sell_entries):
+ _fig.add_trace(go.Scatter(x=sell_entries, y=viz.loc[sell_entries, 'High'] * 1.005, mode='markers', marker=dict(symbol='triangle-down', size=10, color='red'), name='SELL'), row=1, col=1)
+ _fig.add_trace(go.Scatter(x=viz.index, y=viz['rsi_14'], line=dict(color='purple', width=1.5), name='RSI 14'), row=2, col=1)
+ _fig.add_hline(y=70, line_dash='dash', line_color='red', opacity=0.5, row=2, col=1)
+ _fig.add_hline(y=30, line_dash='dash', line_color='green', opacity=0.5, row=2, col=1)
+ # buy/sell markers
+ _fig.add_trace(go.Scatter(x=viz.index, y=viz['macd'], line=dict(color='blue', width=1.5), name='MACD'), row=3, col=1)
+ _fig.add_trace(go.Scatter(x=viz.index, y=viz['macd_signal'], line=dict(color='orange', width=1), name='Signal'), row=3, col=1)
+ colors = ['green' if v >= 0 else 'red' for v in viz['macd_hist']]
+ _fig.add_trace(go.Bar(x=viz.index, y=viz['macd_hist'], marker_color=colors, name='Hist', opacity=0.5), row=3, col=1)
+ _fig.add_trace(go.Bar(x=viz.index, y=viz['Volume'], marker_color='steelblue', name='Volume', opacity=0.5), row=4, col=1)
+ _fig.add_trace(go.Scatter(x=viz.index, y=viz['Volume'].rolling(20).mean(), line=dict(color='orange', width=1), name='Vol SMA20'), row=4, col=1)
+ _fig.update_layout(height=900, title=f'{TICKER} — Last Test Fold Signal Dashboard', xaxis_rangeslider_visible=False, showlegend=False)
+ _fig.update_xaxes(rangeslider_visible=False)
+ # Row 2: RSI
+ # Row 3: MACD
+ # Row 4: Volume
+ _fig.show()
+ return
+
+
+@app.cell(hide_code=True)
+def _(mo):
+ mo.md(r"""
+ ## Next Steps
+
+ Things to iterate on from here:
+
+ 1. **Multi-asset**: swap `TICKER` to BTC-USD, QQQ, GLD, etc. or loop over a universe
+ 2. **Probability threshold**: instead of binary 0/1, use `proba > 0.6` for higher-conviction signals
+ 3. **Position sizing**: Kelly criterion via `PyPortfolioOpt` based on predicted probability
+ 4. **Regime filter**: add ADX/volatility regime detection — only trade in trending regimes
+ 5. **Transaction costs**: subtract realistic slippage (e.g., 5bps per trade) from returns
+ 6. **Alternative splitters you have installed**:
+ - `from tscv import GapWalkForward` — sklearn-compatible, handles gap + purge natively
+ - `from sktime.split import ExpandingWindowSplitter, SlidingWindowSplitter`
+ - `from sklearn.model_selection import TimeSeriesSplit` — basic but solid
+ 7. **LightGBM**: drop-in replacement for XGBoost, often faster on large feature sets
+ 8. **Meta-labeling** (Lopez de Prado): train a secondary model on whether the primary model's signals are correct
+ """)
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+if __name__ == "__main__":
+ app.run()
diff --git a/quant_scaffold.ipynb b/quant_scaffold.ipynb
index 95ee3f3..1c49b42 100644
--- a/quant_scaffold.ipynb
+++ b/quant_scaffold.ipynb
@@ -628,7 +628,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.13.12"
+ "version": "3.12.12"
}
},
"nbformat": 4,
diff --git a/uv.lock b/uv.lock
index 9e12d62..61dd653 100644
--- a/uv.lock
+++ b/uv.lock
@@ -131,6 +131,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
]
+[[package]]
+name = "alpaca-py"
+version = "0.43.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "msgpack" },
+ { name = "pandas" },
+ { name = "pydantic" },
+ { name = "pytz" },
+ { name = "requests" },
+ { name = "sseclient-py" },
+ { name = "websockets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/31/40/e0b3470786de2dd917276759d669b3adc6577432323e8535724e71e53c2e/alpaca_py-0.43.3.tar.gz", hash = "sha256:d97e815eb503fe084113b139ed94227913dc4da8025f28460a14d489371bd07d", size = 97985, upload-time = "2026-04-24T18:28:37.068Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/b4/062dbf87910c4a5c0accde3bd073c2fa3f7ac9e66571489ef1a7c0f44531/alpaca_py-0.43.3-py3-none-any.whl", hash = "sha256:0eb36921696bbfc5ea839f083e7822c9ccab062d8d523a888bc4470985f1b3ef", size = 122528, upload-time = "2026-04-24T18:28:38.25Z" },
+]
+
[[package]]
name = "annotated-doc"
version = "0.0.4"
@@ -202,6 +220,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/e3/03dc0f97eab839f72061342d69bd34424e89876ce4026509aab3d74d4f23/appscript-1.4.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5efce3302c00674b769b79938cc5f66f7791ef45c6419e850a5f1c8f9fcefcc1", size = 85610, upload-time = "2025-10-08T07:56:38.103Z" },
]
+[[package]]
+name = "apscheduler"
+version = "3.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytz" },
+ { name = "setuptools" },
+ { name = "six" },
+ { name = "tzlocal" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/89/3d/f65972547c5aa533276ada2bea3c2ef51bb4c4de55b67a66129c111b89ad/APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244", size = 96309, upload-time = "2019-11-05T07:51:50.394Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f3/34/9ef20ed473c4fd2c3df54ef77a27ae3fc7500b16b192add4720cab8b2c09/APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526", size = 58881, upload-time = "2019-11-05T07:51:48.621Z" },
+]
+
[[package]]
name = "arch"
version = "8.0.0"
@@ -491,6 +524,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-py3-none-any.whl", hash = "sha256:8c3f48a6090b39edec1a273d2d57949aaf13723b01e02f9d518396887519f64d", size = 25921, upload-time = "2026-04-22T20:53:43.251Z" },
]
+[[package]]
+name = "cachetools"
+version = "4.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/ba/619250fa6bc11ce6aa4de0604d45843090a53cd7d10d7253b89669313370/cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff", size = 23682, upload-time = "2021-04-27T21:19:57.252Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/28/c4f5796c67ad06bb91d98d543a5e01805c1ff065e08871f78e52d2a331ad/cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", size = 11998, upload-time = "2021-04-27T21:19:55.559Z" },
+]
+
[[package]]
name = "ccxt"
version = "4.5.50"
@@ -2039,7 +2081,7 @@ dependencies = [
{ name = "torch", version = "2.11.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" },
{ name = "transformers" },
{ name = "tscv" },
- { name = "vectorbt" },
+ { name = "vectorbt", extra = ["full", "rust"] },
{ name = "xgboost" },
{ name = "yfinance" },
]
@@ -2079,7 +2121,7 @@ requires-dist = [
{ name = "torch", specifier = ">=2.0.0", index = "https://download.pytorch.org/whl/cpu" },
{ name = "transformers", specifier = ">=4.0.0" },
{ name = "tscv", specifier = ">=0.1.3" },
- { name = "vectorbt", specifier = ">=1.0.0" },
+ { name = "vectorbt", extras = ["full", "rust"], specifier = ">=1.0.0" },
{ name = "xgboost", specifier = ">=3.2.0" },
{ name = "yfinance", specifier = ">=1.3.0" },
]
@@ -2509,6 +2551,50 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
]
+[[package]]
+name = "msgpack"
+version = "1.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
+ { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
+ { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
+ { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
+ { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
+ { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
+ { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
+ { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
+ { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
+ { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
+ { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
+ { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
+ { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
+ { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
+ { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
+ { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
+ { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
+ { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
+]
+
[[package]]
name = "msgspec"
version = "0.21.1"
@@ -2993,6 +3079,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/be/2f/c67d49afd31c3b02a02ecb5dd07399ed35298042e1b50d166efe2068bb0e/pandas_ta-0.4.71b0-py3-none-any.whl", hash = "sha256:b1f37831811462685be3ef456cfebc0615ce9c8a4eb31bbaa6b341e1a7767a84", size = 240265, upload-time = "2025-09-14T19:08:34.83Z" },
]
+[[package]]
+name = "pandas-ta-classic"
+version = "0.4.47"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "pandas" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/83/6789ac41666a71d995a3d00d66067cae42f76f0996917e4560bb5eae5aa8/pandas_ta_classic-0.4.47-py3-none-any.whl", hash = "sha256:5dcb3b61c5affe9a7625079ccbab40e647d4ba0d49932e287ba43c3c3a28b0ec", size = 266591, upload-time = "2026-03-17T12:22:35.248Z" },
+]
+
[[package]]
name = "pandocfilters"
version = "1.5.1"
@@ -3470,6 +3568,36 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
+[[package]]
+name = "pycryptodome"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
+ { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
+ { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
+ { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
+ { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
+ { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
+ { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
+ { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
+ { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
+]
+
[[package]]
name = "pydantic"
version = "2.13.3"
@@ -3672,6 +3800,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" },
]
+[[package]]
+name = "python-binance"
+version = "1.0.36"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "dateparser" },
+ { name = "pycryptodome" },
+ { name = "requests" },
+ { name = "six" },
+ { name = "websockets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8d/49/20a473a228fc78e6cad0a1d0198d7b8dbbb20211aefd77b9ff3327c87308/python_binance-1.0.36.tar.gz", hash = "sha256:d7b0668ff1b620b30d95b52f9f61b557748eac2d6c4d5f17fe21c356e5e0c541", size = 191487, upload-time = "2026-03-24T11:23:15.936Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/a3/1501f902fc71d6e03081844b7af16acd69f5a476d33441287f2de4af6bd6/python_binance-1.0.36-py2.py3-none-any.whl", hash = "sha256:4a7ffdf9c0e43cdc8136a5169993f6f6520bb8b3334e2fbc9ed127ab4fb3c8b6", size = 148503, upload-time = "2026-03-24T11:23:13.969Z" },
+]
+
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -3711,6 +3856,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
]
+[[package]]
+name = "python-telegram-bot"
+version = "13.13"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "apscheduler" },
+ { name = "cachetools" },
+ { name = "certifi" },
+ { name = "pytz" },
+ { name = "tornado" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/9a/52e8bfc2981cee700ee83b6d7c6f781c4f21e441898bf60834936c0e2452/python-telegram-bot-13.13.tar.gz", hash = "sha256:4296d81a38b7e5ef1f9795651128e58fb354678b8dc4db93ca166c96828c57b2", size = 351895, upload-time = "2022-06-28T17:56:05.405Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/59/39/30c73b9c049875870f46dae148528ad8671a5182263448b9c824c512e032/python_telegram_bot-13.13-py3-none-any.whl", hash = "sha256:13f83ec4433f24a67e7a2df0206dfde83c1627c92880a2fcf95a83585b3cc589", size = 513361, upload-time = "2022-06-28T17:55:57.953Z" },
+]
+
[[package]]
name = "pytz"
version = "2026.1.post1"
@@ -3871,6 +4032,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/d4/484041d5c5a5d3ec8df5c74fef3054fec004dab554f6c3c00187888f8cc1/quantstats-0.0.81-py3-none-any.whl", hash = "sha256:6af2b501f61917c8c960faaf8007eb858d970ab02a3cf0d7dc19f048953e15f3", size = 90067, upload-time = "2026-01-13T18:18:18.451Z" },
]
+[[package]]
+name = "ray"
+version = "2.55.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "filelock" },
+ { name = "jsonschema" },
+ { name = "msgpack" },
+ { name = "packaging" },
+ { name = "protobuf" },
+ { name = "pyyaml" },
+ { name = "requests" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ac/3a/4d34f471a68b958b7f94c974c19ad6836a61a2dc16393df4294169a2e4b0/ray-2.55.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:137f9006eee28caab8260803cca314f37bbda3fc94fdfa31c770b5d019626ad8", size = 65822379, upload-time = "2026-04-22T20:09:58.064Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/13/0db535102d0256b350ca116d8987588aca1a1f9ebb4638e1e1ff88bbcef8/ray-2.55.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:26541f69bb55607ef8335baac75b2ed12ff2ce02d56313219b29eda003039221", size = 72910802, upload-time = "2026-04-22T20:10:04.382Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/f8/fffadf3f4285eebd460e4d7f2ed1c0cd641ed89613c3f49eb881ee9fa7e2/ray-2.55.1-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:263705f6bab29e7622a94f82da25fd7f9cead76cdf89a07aab28f79cdf8f9d95", size = 73765203, upload-time = "2026-04-22T20:10:10.495Z" },
+ { url = "https://files.pythonhosted.org/packages/10/f7/5acb86fc9625a0e6bbc40e1c7d42c60770e78585439a921c32738b6d675a/ray-2.55.1-cp312-cp312-win_amd64.whl", hash = "sha256:9ad56704c8bd7e92130162f9c58e4ef473609515637673d5a36e761f95335206", size = 27865547, upload-time = "2026-04-22T20:10:15.364Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/95/898699cc1a6a5f304ea95376d079843b5c05f4c8c1ec7e55a5cc7ffcea50/ray-2.55.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:f9844a9272ef2e6eb5771025866072cf4234cf4c7cc1a31e235b7de7111864be", size = 65766823, upload-time = "2026-04-22T20:10:20.786Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/13/87deecc090c672e45a0cf6f5eef511de448b93f37ef18fd10eb8e8557a0d/ray-2.55.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:b415d590e062f248907e0fe42994943f11726b7178fcf4b1cf5546721fb1a5f8", size = 72818676, upload-time = "2026-04-22T20:10:26.705Z" },
+ { url = "https://files.pythonhosted.org/packages/71/d7/fc95d3b8824c62105c64aa1b59c59600b581f608d78a2af753e010936dc9/ray-2.55.1-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:1380e043eb57cde69b7e9199c6f2558ceeb8f0fc41c97d1d5e50ea042115f302", size = 73678908, upload-time = "2026-04-22T20:10:32.795Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/03/7e552325572e067b23a4584bda8dc6a67af8bd7e03c424d2610bfa93112d/ray-2.55.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:b062045c64c2bce39a51661624f7292c7bbf30f2a9d878627aae31d46da5712d", size = 65774106, upload-time = "2026-04-22T20:10:39.885Z" },
+ { url = "https://files.pythonhosted.org/packages/94/62/607a8859520ce350861425f11f8e15d66c15ee33e6aac812f9e2889b5df4/ray-2.55.1-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:4e618d61e1b14b6fde9a586151f3fd9d435b0b85048b997bcaa7f4a533747b2b", size = 72814044, upload-time = "2026-04-22T20:10:46.985Z" },
+ { url = "https://files.pythonhosted.org/packages/04/5a/0699bef04a72d7dc54462960d07ef7a19cd8b1e09979880aba2b6d13cca2/ray-2.55.1-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:156ed3e72ad95b645d2006cd71a8dddbcc89b56bfc00027f6225adf78bd9cb74", size = 73644244, upload-time = "2026-04-22T20:10:52.973Z" },
+]
+
[[package]]
name = "referencing"
version = "0.37.0"
@@ -4421,6 +4609,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" },
]
+[[package]]
+name = "sseclient-py"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/2e/59920f7d66b7f9932a3d83dd0ec53fab001be1e058bf582606fe414a5198/sseclient_py-1.9.0-py3-none-any.whl", hash = "sha256:340062b1587fc2880892811e2ab5b176d98ef3eee98b3672ff3a3ba1e8ed0f6f", size = 8351, upload-time = "2026-01-02T23:39:30.995Z" },
+]
+
[[package]]
name = "stack-data"
version = "0.6.3"
@@ -4877,6 +5073,44 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/08/c15c7ade057b0633ac39fe6f5fffa37c306304745538c4f9187a05e9aa69/vectorbt-1.0.0-py3-none-any.whl", hash = "sha256:c596e11bdad985181150f3b3c8db0a7322c738fbb64c7a919a0418e99326cc13", size = 451657, upload-time = "2026-04-22T13:29:59.982Z" },
]
+[package.optional-dependencies]
+full = [
+ { name = "alpaca-py" },
+ { name = "ccxt" },
+ { name = "pandas-ta-classic" },
+ { name = "python-binance" },
+ { name = "python-telegram-bot" },
+ { name = "quantstats" },
+ { name = "ray" },
+ { name = "ta" },
+ { name = "ta-lib" },
+ { name = "yfinance" },
+]
+rust = [
+ { name = "vectorbt-rust" },
+]
+
+[[package]]
+name = "vectorbt-rust"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/32/a5b7193cfaaa4485e6b3c0156e15701b21bb505b66e035f01594675d1242/vectorbt_rust-1.0.0.tar.gz", hash = "sha256:d9ef94a24f970be4d566d5c41d20c436cb1593ae2e960ee2ae9851ce5042b9aa", size = 74476, upload-time = "2026-04-22T13:29:36.715Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/62/3df4be3717f3c2df07f51d380f9beae7100e78656b1879710e62c2d36f28/vectorbt_rust-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ceceb76bb1f485dc66eb3aa4ee2e4934b704c8753884a4c2eebf156427ecf374", size = 845019, upload-time = "2026-04-22T13:29:22.458Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/5f/b96d327002901177fc05c64d9a99bbff5673a20081bb1a3a2e35e02a8707/vectorbt_rust-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:689794e3f3a91c0b8cfc3565076b01864a51a95a8a003410c972942a424a8014", size = 766156, upload-time = "2026-04-22T13:29:24.034Z" },
+ { url = "https://files.pythonhosted.org/packages/35/fc/2ef2905a752f17329c768ddbfdff3cd14deab932e1010ca140bde9ab32b4/vectorbt_rust-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a204323805b5cc721ce4706f8d117886d0e860a2c74758aad965c65912aabc8f", size = 781441, upload-time = "2026-04-22T13:29:25.418Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/c6/708752ae698161575fe949ba659341e24ebd6341ed683e1f2b66b7a84a35/vectorbt_rust-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a379f7ae66ef5733d5de6ae1f23f8434d4411918d04b9b2baf7e8db105ca759", size = 873255, upload-time = "2026-04-22T13:29:27.081Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/fe/fe1b7b07520fbb1559187ea5a962db0246b5c3f6e24ace2f7cd6a5346fab/vectorbt_rust-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f6adbd831d1615eca9e6499296345d092f670d668440d2b5043523f3753c62fc", size = 885340, upload-time = "2026-04-22T13:29:28.381Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/f5/02dae5b57efab7e8974f5d9753e69cbc4cff23f8432b0d6c15a86beea7b2/vectorbt_rust-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:65305a46fd522978208bb860c4f6a7a01f84a2105801661cf48ee24d5d8f1b20", size = 844571, upload-time = "2026-04-22T13:29:29.672Z" },
+ { url = "https://files.pythonhosted.org/packages/98/fb/efd9ec9a448c87c069e1e82bf5362ddf58034fccad0c3ca71463e3ffffab/vectorbt_rust-1.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:29caf153a3e0ca9fb285970ae7086c6da667b309535e665f4e78d4f5797f10b0", size = 765910, upload-time = "2026-04-22T13:29:31.11Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/91/6749335b37b4b88468c89c8d30e5723a821e870b5eed62a9f2c3fca6808c/vectorbt_rust-1.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc623f8a824b65a15b049461d07eeb545928ef9e0524389d549dba3060a786da", size = 781120, upload-time = "2026-04-22T13:29:32.689Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/3d/523288d04f77660698a6db2d64ec0c7f72299993e711d09c00ae98993952/vectorbt_rust-1.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf8ae04a2dbd6ead5f4f36dace37c211e31a9b354d2daed20fcdc55a3dc5aefe", size = 872877, upload-time = "2026-04-22T13:29:34.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/9b/7f4138ff4309b6aa7f4cc3accde9bf8260d959c303dcfe3fc59271725679/vectorbt_rust-1.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8702515db6f46ef9a80cdd1d2c8c8617a148bf1861721d308cc299c8cc408e35", size = 884993, upload-time = "2026-04-22T13:29:35.331Z" },
+]
+
[[package]]
name = "wcwidth"
version = "0.6.0"
diff --git a/warmup_cache.py b/warmup_cache.py
new file mode 100644
index 0000000..42ea9d4
--- /dev/null
+++ b/warmup_cache.py
@@ -0,0 +1,172 @@
+"""
+Pre-warm the defeatbeta httpfs cache to a persistent directory.
+
+By default the library caches to /tmp/defeatbeta/cache/ which is wiped on
+reboot. This script redirects the cache to ~/.cache/defeatbeta/ and then
+does a full SELECT * on every table in parallel, forcing all parquet blocks
+to be fetched and stored. Re-running is safe: already-warmed tables are
+skipped.
+
+Usage:
+ uv run python warmup_cache.py
+
+After this runs once (~3-4 GB download), use persistent_cache.py in your
+notebooks/scripts to read from the cache with no network:
+
+ from persistent_cache import enable_persistent_cache
+ enable_persistent_cache()
+
+ from defeatbeta_api.data.ticker import Ticker
+ t = Ticker("AAPL")
+"""
+import json
+import threading
+import time
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from pathlib import Path
+
+# ── Persistent cache location ────────────────────────────────────────────────
+CACHE_DIR = Path.home() / ".cache" / "defeatbeta"
+CACHE_DIR.mkdir(parents=True, exist_ok=True)
+STATE_FILE = CACHE_DIR / "warmup_done.json"
+
+# ── Redirect cache dir before the library touches it ────────────────────────
+import defeatbeta_api.utils.util as _util
+_util.validate_httpfs_cache_directory = lambda: str(CACHE_DIR)
+
+from defeatbeta_api.client.duckdb_conf import Configuration
+from defeatbeta_api.client.duckdb_client import get_duckdb_client
+from defeatbeta_api.client.hugging_face_client import HuggingFaceClient
+from defeatbeta_api.utils.const import tables
+
+WORKERS = 2 # keep concurrent connections low to avoid HuggingFace 429s
+
+config = Configuration(
+ cache_httpfs_disk_size=500 * 1024 * 1024,
+ http_retries=10,
+ http_retry_wait_ms=10_000, # 10s base wait on 429/5xx
+ http_retry_backoff=2.0, # doubles each retry: 10s, 20s, 40s …
+ http_timeout=180,
+)
+client = get_duckdb_client(config=config)
+hf = HuggingFaceClient()
+
+
+# ── Resume state ─────────────────────────────────────────────────────────────
+
+def _load_done() -> set[str]:
+ try:
+ return set(json.loads(STATE_FILE.read_text())["done"])
+ except Exception:
+ return set()
+
+def _save_done(done: set[str]) -> None:
+ tmp = STATE_FILE.with_suffix(".tmp")
+ tmp.write_text(json.dumps({"done": sorted(done)}))
+ tmp.rename(STATE_FILE)
+
+
+already_done = _load_done()
+todo = [t for t in tables if t not in already_done]
+
+
+# ── Helpers ───────────────────────────────────────────────────────────────────
+
+def cache_mb() -> float:
+ try:
+ return sum(f.stat().st_size for f in CACHE_DIR.iterdir() if f.is_file()) / 1e6
+ except Exception:
+ return 0.0
+
+
+# ── Shared state for the live reporter ───────────────────────────────────────
+_lock = threading.Lock()
+_in_flight: set[str] = set()
+_done: list[str] = list(already_done) # pre-seed with already-completed tables
+_stop_reporter = threading.Event()
+
+
+def _reporter() -> None:
+ while not _stop_reporter.is_set():
+ with _lock:
+ n_done = len(_done)
+ flying = ", ".join(sorted(_in_flight)) or "—"
+ n_flight = len(_in_flight)
+ mb = cache_mb()
+ print(
+ f"\r [live] cache={mb:>6.0f} MB | "
+ f"done={n_done}/{len(tables)} | "
+ f"in-flight({n_flight}): {flying} ",
+ end="", flush=True,
+ )
+ _stop_reporter.wait(timeout=2)
+ print("\r" + " " * 80 + "\r", end="", flush=True)
+
+
+def fetch_table(table: str) -> tuple[str, int, float]:
+ url = hf.get_url_path(table)
+ with _lock:
+ _in_flight.add(table)
+ t0 = time.perf_counter()
+ for attempt in range(1, 4):
+ try:
+ df = client.query(f"SELECT * FROM '{url}'")
+ break
+ except Exception as exc:
+ if attempt == 3:
+ with _lock:
+ _in_flight.discard(table)
+ raise
+ wait = 60 * attempt
+ print(f"\n ! {table} failed (attempt {attempt}): {exc} — retrying in {wait}s")
+ time.sleep(wait)
+ elapsed = time.perf_counter() - t0
+ with _lock:
+ _in_flight.discard(table)
+ _done.append(table)
+ _save_done(set(_done))
+ return table, len(df), elapsed
+
+
+# ── Run ───────────────────────────────────────────────────────────────────────
+print(f"Cache dir : {CACHE_DIR}")
+print(f"Tables : {len(todo)} to fetch ({len(already_done)} already done, skipping)")
+print(f"Workers : {WORKERS}")
+print()
+
+if not todo:
+ print("All tables already warmed. Nothing to do.")
+ raise SystemExit(0)
+
+for t in sorted(already_done):
+ print(f" - {t:<40} (skip)")
+print()
+
+reporter = threading.Thread(target=_reporter, daemon=True)
+reporter.start()
+
+t_start = time.perf_counter()
+results: list[tuple[str, int, float]] = []
+
+with ThreadPoolExecutor(max_workers=WORKERS) as pool:
+ futures = {pool.submit(fetch_table, t): t for t in todo}
+ for future in as_completed(futures):
+ table, rows, elapsed = future.result()
+ results.append((table, rows, elapsed))
+ with _lock:
+ n_done = len(_done)
+ mb = cache_mb()
+ print(
+ f" ✓ {table:<40} {rows:>9,} rows {elapsed:5.1f}s "
+ f"[{n_done}/{len(tables)} cache={mb:.0f} MB]"
+ )
+
+_stop_reporter.set()
+reporter.join()
+
+total_elapsed = time.perf_counter() - t_start
+total_rows = sum(r for _, r, _ in results)
+print()
+print(f"Finished in {total_elapsed:.0f}s")
+print(f"Fetched : {total_rows:,} rows across {len(results)} tables")
+print(f"Cache size : {cache_mb():.0f} MB → {CACHE_DIR}")