Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2eb5dd574 | ||
|
|
8d3e663e71 |
+11
@@ -0,0 +1,11 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.venv/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
data/ibkr/
|
||||||
@@ -19,8 +19,10 @@ Keep these areas loosely separated in code and documentation. Avoid coupling the
|
|||||||
|
|
||||||
- Use Python for implementation.
|
- Use Python for implementation.
|
||||||
- Use `mise` to control the Python version.
|
- Use `mise` to control the Python version.
|
||||||
|
- Use `uv` to manage Python packages and virtual environments.
|
||||||
- The local Python version is pinned in `.mise.toml`.
|
- The local Python version is pinned in `.mise.toml`.
|
||||||
- Prefer commands run through `mise exec -- ...` when the Python environment matters.
|
- Prefer commands run through `mise exec -- ...` when the Python environment matters.
|
||||||
|
- Prefer `mise exec -- uv run ...` for project Python commands once dependencies are synced.
|
||||||
- Git is the version control system for this project. A remote will be added later.
|
- Git is the version control system for this project. A remote will be added later.
|
||||||
|
|
||||||
## Safety And Trading Constraints
|
## Safety And Trading Constraints
|
||||||
|
|||||||
@@ -15,15 +15,24 @@ This repository is at the planning and scaffolding stage. The machine learning m
|
|||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
Use `mise` to manage Python.
|
Use `mise` to manage Python and `uv` to manage Python packages.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
mise install
|
mise install
|
||||||
mise exec -- python --version
|
mise exec -- uv sync
|
||||||
|
mise exec -- uv run python --version
|
||||||
```
|
```
|
||||||
|
|
||||||
The current local Python version is pinned in `.mise.toml`.
|
The current local Python version is pinned in `.mise.toml`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Runtime dependencies are declared in `pyproject.toml`.
|
||||||
|
|
||||||
|
- `ib-insync` for the IBKR API connection.
|
||||||
|
- `pandas` for tabular candle data handling.
|
||||||
|
- `pyarrow` for Parquet file support.
|
||||||
|
|
||||||
## Trading Safety
|
## Trading Safety
|
||||||
|
|
||||||
The default target is paper trading. Real trading should only be added later with explicit configuration, clear documentation, and tests around order generation and broker integration.
|
The default target is paper trading. Real trading should only be added later with explicit configuration, clear documentation, and tests around order generation and broker integration.
|
||||||
@@ -33,3 +42,7 @@ Do not commit secrets such as IBKR credentials, account identifiers, API tokens,
|
|||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
See [docs/README.md](docs/README.md) for the initial architecture notes and decision log.
|
See [docs/README.md](docs/README.md) for the initial architecture notes and decision log.
|
||||||
|
|
||||||
|
The first data collection design note is [docs/data-fetcher.md](docs/data-fetcher.md).
|
||||||
|
|
||||||
|
Manual test instructions for the IBKR fetcher skeleton are in [docs/manual-test/README.md](docs/manual-test/README.md).
|
||||||
|
|||||||
+5
-1
@@ -10,6 +10,10 @@ Build a Python-based trading bot that uses machine learning to determine which a
|
|||||||
|
|
||||||
Responsible for acquiring and storing market, asset, and any future feature data needed for model training and evaluation.
|
Responsible for acquiring and storing market, asset, and any future feature data needed for model training and evaluation.
|
||||||
|
|
||||||
|
The first planned tool is an IBKR daily candle fetcher. It should fetch open, high, low, close, and volume data for a ticker and date range, then eventually persist that data to a ticker-named Parquet file. See [data-fetcher.md](data-fetcher.md).
|
||||||
|
|
||||||
|
Parquet files are partitioned by ticker, not by date.
|
||||||
|
|
||||||
Open decisions:
|
Open decisions:
|
||||||
|
|
||||||
- asset universe;
|
- asset universe;
|
||||||
@@ -72,7 +76,7 @@ Possible scope:
|
|||||||
## Near-Term Priorities
|
## Near-Term Priorities
|
||||||
|
|
||||||
1. Decide the initial project package structure.
|
1. Decide the initial project package structure.
|
||||||
2. Add Python packaging and dependency management.
|
2. Keep Python packaging and dependency management current with `uv`.
|
||||||
3. Add a minimal configuration system.
|
3. Add a minimal configuration system.
|
||||||
4. Define interfaces for data collection, model artifacts, and broker execution.
|
4. Define interfaces for data collection, model artifacts, and broker execution.
|
||||||
5. Add tests for the core trading decision boundaries before connecting real broker behavior.
|
5. Add tests for the core trading decision boundaries before connecting real broker behavior.
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Data Fetcher Design
|
||||||
|
|
||||||
|
## Initial Goal
|
||||||
|
|
||||||
|
Create a Python module/tool that fetches daily candlestick data from the IBKR API for a specific ticker and date range.
|
||||||
|
|
||||||
|
The intended example workflow is:
|
||||||
|
|
||||||
|
- ticker: `SPY`;
|
||||||
|
- date range: `2026-06-01` to `2026-06-30`;
|
||||||
|
- bar size: one trading day;
|
||||||
|
- fields: open, high, low, close, volume;
|
||||||
|
- output: one Parquet file named for the ticker, such as `SPY.parquet`.
|
||||||
|
|
||||||
|
## Current Implementation
|
||||||
|
|
||||||
|
The first implementation is intentionally small:
|
||||||
|
|
||||||
|
- ticker passed as a required command line argument;
|
||||||
|
- end date passed with `--end-date YYYYMMDD`, defaulting to yesterday;
|
||||||
|
- end date can also be derived with `--end-date-from-parquet`;
|
||||||
|
- duration passed with `--duration`, defaulting to `1 W`;
|
||||||
|
- hard-coded IBKR Gateway target: `127.0.0.1:4002`;
|
||||||
|
- uses the IBKR API through `ib_insync`;
|
||||||
|
- prints fetched candles as CSV-like rows;
|
||||||
|
- writes candles to a symbol-named Parquet file;
|
||||||
|
- appends to an existing symbol file and keeps one row per date.
|
||||||
|
|
||||||
|
Run it with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mise exec -- uv run python src/trading_bot/data/fetch_ibkr_daily.py SPY
|
||||||
|
```
|
||||||
|
|
||||||
|
To override the requested range:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mise exec -- uv run python src/trading_bot/data/fetch_ibkr_daily.py SPY --end-date 20250605 --duration "1 M"
|
||||||
|
```
|
||||||
|
|
||||||
|
To fetch backward from the oldest date already stored in the symbol file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mise exec -- uv run python src/trading_bot/data/fetch_ibkr_daily.py SPY --end-date-from-parquet
|
||||||
|
```
|
||||||
|
|
||||||
|
`--end-date` and `--end-date-from-parquet` cannot be used together. If the symbol Parquet file does not exist or has no rows, `--end-date-from-parquet` uses today's US/Eastern date.
|
||||||
|
|
||||||
|
This expects a local IBKR Gateway session to be running and accepting API connections on `127.0.0.1:4002`.
|
||||||
|
|
||||||
|
By default, output is written to `data/ibkr/daily/SPY.parquet`. Use `--output-dir` to choose another directory.
|
||||||
|
|
||||||
|
Manual test instructions are in [manual-test/README.md](manual-test/README.md).
|
||||||
|
|
||||||
|
## Intended Future Behavior
|
||||||
|
|
||||||
|
Later, this tool should broaden configuration around data source and normalization choices.
|
||||||
|
|
||||||
|
Decided storage behavior:
|
||||||
|
|
||||||
|
- Parquet files are partitioned by ticker, not by date.
|
||||||
|
- Each ticker should have its own Parquet file, such as `SPY.parquet`.
|
||||||
|
- The trading date is used as the row key for merges.
|
||||||
|
- Re-fetching a date replaces the existing row for that date in that symbol's file.
|
||||||
|
|
||||||
|
Candidate output schema:
|
||||||
|
|
||||||
|
| Column | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `date` | date index | Trading session date |
|
||||||
|
| `symbol` | string | Asset ticker |
|
||||||
|
| `open` | float | Daily open price |
|
||||||
|
| `high` | float | Daily high price |
|
||||||
|
| `low` | float | Daily low price |
|
||||||
|
| `close` | float | Daily close price |
|
||||||
|
| `volume` | integer | Daily traded volume |
|
||||||
|
|
||||||
|
Open decisions:
|
||||||
|
|
||||||
|
- how to handle adjusted versus unadjusted prices;
|
||||||
|
- how to handle missing sessions and IBKR pacing limits;
|
||||||
|
- whether to use `ib_insync` long term or a lower-level IBKR client wrapper.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Manual Test Instructions
|
||||||
|
|
||||||
|
## IBKR Daily Fetcher
|
||||||
|
|
||||||
|
This test checks the IBKR data fetcher and confirms it writes a symbol-named Parquet file.
|
||||||
|
|
||||||
|
The fetcher currently requests:
|
||||||
|
|
||||||
|
- gateway: `127.0.0.1:4002`;
|
||||||
|
- client id: `101`;
|
||||||
|
- symbol: provided as a command line argument;
|
||||||
|
- end date: provided with `--end-date YYYYMMDD`, defaulting to yesterday;
|
||||||
|
- end date can also be derived with `--end-date-from-parquet`;
|
||||||
|
- duration: provided with `--duration`, defaulting to `1 W`;
|
||||||
|
- bar size: `1 day`;
|
||||||
|
- data type: `TRADES`;
|
||||||
|
- regular trading hours only;
|
||||||
|
- output: printed CSV-like rows and a Parquet file in `data/ibkr/daily/`.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Start IBKR Gateway.
|
||||||
|
2. Log in to the paper trading account.
|
||||||
|
3. Confirm API access is enabled in IBKR Gateway.
|
||||||
|
4. Confirm the API socket port is `4002`.
|
||||||
|
5. Confirm no other API client is already using client id `101`.
|
||||||
|
6. Sync Python dependencies:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mise exec -- uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run The Test
|
||||||
|
|
||||||
|
From the repository root, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mise exec -- uv run python src/trading_bot/data/fetch_ibkr_daily.py SPY
|
||||||
|
```
|
||||||
|
|
||||||
|
To fetch a specific IBKR range, pass an end date and duration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mise exec -- uv run python src/trading_bot/data/fetch_ibkr_daily.py SPY --end-date 20250605 --duration "1 M"
|
||||||
|
```
|
||||||
|
|
||||||
|
To fetch backward from the oldest date already stored in `data/ibkr/daily/SPY.parquet`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mise exec -- uv run python src/trading_bot/data/fetch_ibkr_daily.py SPY --end-date-from-parquet
|
||||||
|
```
|
||||||
|
|
||||||
|
`--end-date` and `--end-date-from-parquet` cannot be used together. If the symbol Parquet file does not exist or has no rows, `--end-date-from-parquet` uses today's US/Eastern date.
|
||||||
|
|
||||||
|
## Expected Output
|
||||||
|
|
||||||
|
The tool should first print the request range and connection target:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Fetching SPY daily candles ending 2025-06-05 for duration 1 M
|
||||||
|
Connecting to IBKR Gateway at 127.0.0.1:4002 with client id 101
|
||||||
|
```
|
||||||
|
|
||||||
|
If the request succeeds, it should then print a header and one row per returned trading day:
|
||||||
|
|
||||||
|
```text
|
||||||
|
date,symbol,open,high,low,close,volume
|
||||||
|
2025-06-05,SPY,...
|
||||||
|
```
|
||||||
|
|
||||||
|
Exact prices and volume depend on what IBKR returns.
|
||||||
|
|
||||||
|
The tool should then write or update:
|
||||||
|
|
||||||
|
```text
|
||||||
|
data/ibkr/daily/SPY.parquet
|
||||||
|
```
|
||||||
|
|
||||||
|
If the Parquet file already exists, rows from the latest fetch are merged into it. The trading date is used as the row key, so a symbol file keeps only one row for each date.
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
- Connection refused: IBKR Gateway is not running, the port is not `4002`, or API access is disabled.
|
||||||
|
- Client id already in use: change `IBKR_CLIENT_ID` in the fetcher or disconnect the other client.
|
||||||
|
- No historical bars: confirm the account has market data permissions and that IBKR accepts the requested historical data range.
|
||||||
|
- Pacing or permission errors: note the IBKR error message before changing the request.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[project]
|
||||||
|
name = "trading-bot"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Python trading bot using machine learning and the IBKR API."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11,<3.12"
|
||||||
|
dependencies = [
|
||||||
|
"ib-insync>=0.9.86",
|
||||||
|
"pandas>=2.3.0",
|
||||||
|
"pyarrow>=20.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = []
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
package = true
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.8.0,<0.9.0"]
|
||||||
|
build-backend = "uv_build"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Trading bot package."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Training data collection tools."""
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
"""IBKR daily candle fetcher."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
DEFAULT_OUTPUT_DIR = Path("data/ibkr/daily")
|
||||||
|
DEFAULT_DURATION = "1 W"
|
||||||
|
EASTERN_TZ = ZoneInfo("America/New_York")
|
||||||
|
DURATION_PATTERN = re.compile(r"^\d+\s+[SDWMY]$")
|
||||||
|
|
||||||
|
IBKR_HOST = "127.0.0.1"
|
||||||
|
IBKR_PORT = 4002
|
||||||
|
IBKR_CLIENT_ID = 101
|
||||||
|
IBKR_CONNECT_TIMEOUT_SECONDS = 10
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DailyCandle:
|
||||||
|
"""Daily OHLCV market data for one trading session."""
|
||||||
|
|
||||||
|
trading_day: date
|
||||||
|
open: float
|
||||||
|
high: float
|
||||||
|
low: float
|
||||||
|
close: float
|
||||||
|
volume: int
|
||||||
|
|
||||||
|
|
||||||
|
def default_end_date() -> date:
|
||||||
|
"""Return yesterday's date in the US/Eastern market timezone."""
|
||||||
|
|
||||||
|
return datetime.now(EASTERN_TZ).date() - timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
|
def current_market_date() -> date:
|
||||||
|
"""Return today's date in the US/Eastern market timezone."""
|
||||||
|
|
||||||
|
return datetime.now(EASTERN_TZ).date()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_end_date(value: str) -> date:
|
||||||
|
"""Parse an end date in IBKR-friendly YYYYMMDD form."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return datetime.strptime(value, "%Y%m%d").date()
|
||||||
|
except ValueError as exc:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
"end date must use YYYYMMDD format, such as 20250605"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def parse_duration(value: str) -> str:
|
||||||
|
"""Normalize and validate an IBKR duration string like '1 W' or '1 M'."""
|
||||||
|
|
||||||
|
normalized = " ".join(value.upper().split())
|
||||||
|
if not DURATION_PATTERN.fullmatch(normalized):
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
"duration must look like an IBKR value, such as '1 W' or '1 M'"
|
||||||
|
)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def format_ibkr_end_datetime(end_date: date) -> str:
|
||||||
|
"""Convert a date to the US/Eastern end datetime string IBKR expects."""
|
||||||
|
|
||||||
|
return f"{end_date:%Y%m%d} 23:59:59 US/Eastern"
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_bar_date(value: date | datetime | str) -> date:
|
||||||
|
"""Normalize an IBKR historical bar date value."""
|
||||||
|
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value.date()
|
||||||
|
if isinstance(value, date):
|
||||||
|
return value
|
||||||
|
return parse_end_date(value)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_daily_candles(
|
||||||
|
symbol: str, end_date: date, duration: str
|
||||||
|
) -> list[DailyCandle]:
|
||||||
|
"""Fetch daily candles for a symbol from IBKR."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ib_insync import IB, Stock # type: ignore[import-not-found]
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit(
|
||||||
|
"Missing dependency: ib_insync. Install it before running the "
|
||||||
|
"IBKR fetcher."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
ib = IB()
|
||||||
|
try:
|
||||||
|
print(
|
||||||
|
f"Connecting to IBKR Gateway at {IBKR_HOST}:{IBKR_PORT} "
|
||||||
|
f"with client id {IBKR_CLIENT_ID}"
|
||||||
|
)
|
||||||
|
ib.connect(
|
||||||
|
IBKR_HOST,
|
||||||
|
IBKR_PORT,
|
||||||
|
clientId=IBKR_CLIENT_ID,
|
||||||
|
timeout=IBKR_CONNECT_TIMEOUT_SECONDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
contract = Stock(symbol, "SMART", "USD")
|
||||||
|
ib.qualifyContracts(contract)
|
||||||
|
bars = ib.reqHistoricalData(
|
||||||
|
contract,
|
||||||
|
endDateTime=format_ibkr_end_datetime(end_date),
|
||||||
|
durationStr=duration,
|
||||||
|
barSizeSetting="1 day",
|
||||||
|
whatToShow="TRADES",
|
||||||
|
useRTH=True,
|
||||||
|
formatDate=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not bars:
|
||||||
|
print(f"IBKR returned no historical bars for {symbol}.")
|
||||||
|
|
||||||
|
candles: list[DailyCandle] = []
|
||||||
|
for bar in bars:
|
||||||
|
trading_day = normalize_bar_date(bar.date)
|
||||||
|
candles.append(
|
||||||
|
DailyCandle(
|
||||||
|
trading_day=trading_day,
|
||||||
|
open=float(bar.open),
|
||||||
|
high=float(bar.high),
|
||||||
|
low=float(bar.low),
|
||||||
|
close=float(bar.close),
|
||||||
|
volume=int(bar.volume),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return candles
|
||||||
|
finally:
|
||||||
|
if ib.isConnected():
|
||||||
|
ib.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def candles_to_frame(symbol: str, candles: list[DailyCandle]) -> pd.DataFrame:
|
||||||
|
"""Convert candles to a date-indexed dataframe ready for Parquet storage."""
|
||||||
|
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"date": candle.trading_day,
|
||||||
|
"symbol": symbol,
|
||||||
|
"open": candle.open,
|
||||||
|
"high": candle.high,
|
||||||
|
"low": candle.low,
|
||||||
|
"close": candle.close,
|
||||||
|
"volume": candle.volume,
|
||||||
|
}
|
||||||
|
for candle in candles
|
||||||
|
]
|
||||||
|
frame = pd.DataFrame.from_records(rows)
|
||||||
|
if frame.empty:
|
||||||
|
return pd.DataFrame(
|
||||||
|
columns=["symbol", "open", "high", "low", "close", "volume"],
|
||||||
|
index=pd.Index([], name="date"),
|
||||||
|
)
|
||||||
|
|
||||||
|
frame["date"] = pd.to_datetime(frame["date"]).dt.date
|
||||||
|
return frame.set_index("date")
|
||||||
|
|
||||||
|
|
||||||
|
def read_existing_candles(path: Path) -> pd.DataFrame:
|
||||||
|
"""Read an existing candle Parquet file as a date-indexed dataframe."""
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
return pd.DataFrame(
|
||||||
|
columns=["symbol", "open", "high", "low", "close", "volume"],
|
||||||
|
index=pd.Index([], name="date"),
|
||||||
|
)
|
||||||
|
|
||||||
|
frame = pd.read_parquet(path)
|
||||||
|
if "date" in frame.columns:
|
||||||
|
frame["date"] = pd.to_datetime(frame["date"]).dt.date
|
||||||
|
frame = frame.set_index("date")
|
||||||
|
|
||||||
|
frame.index = pd.to_datetime(frame.index).date
|
||||||
|
frame.index.name = "date"
|
||||||
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
def oldest_stored_date_or_today(path: Path) -> date:
|
||||||
|
"""Return the oldest stored candle date, or today if no data exists yet."""
|
||||||
|
|
||||||
|
existing = read_existing_candles(path)
|
||||||
|
if existing.empty:
|
||||||
|
return current_market_date()
|
||||||
|
return min(existing.index)
|
||||||
|
|
||||||
|
|
||||||
|
def write_candles(path: Path, symbol: str, candles: list[DailyCandle]) -> pd.DataFrame:
|
||||||
|
"""Append candles to a ticker Parquet file, keeping one row per date."""
|
||||||
|
|
||||||
|
existing = read_existing_candles(path)
|
||||||
|
fetched = candles_to_frame(symbol, candles)
|
||||||
|
combined = pd.concat([existing, fetched])
|
||||||
|
if not combined.empty:
|
||||||
|
combined = combined[~combined.index.duplicated(keep="last")]
|
||||||
|
combined = combined.sort_index()
|
||||||
|
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
combined.to_parquet(path, index=True)
|
||||||
|
return combined
|
||||||
|
|
||||||
|
|
||||||
|
def print_candles(symbol: str, candles: list[DailyCandle]) -> None:
|
||||||
|
"""Print candles in a compact table."""
|
||||||
|
|
||||||
|
print("date,symbol,open,high,low,close,volume")
|
||||||
|
for candle in candles:
|
||||||
|
print(
|
||||||
|
f"{candle.trading_day.isoformat()},"
|
||||||
|
f"{symbol},"
|
||||||
|
f"{candle.open:.2f},"
|
||||||
|
f"{candle.high:.2f},"
|
||||||
|
f"{candle.low:.2f},"
|
||||||
|
f"{candle.close:.2f},"
|
||||||
|
f"{candle.volume}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
"""Parse command line arguments."""
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Fetch daily IBKR candles.")
|
||||||
|
parser.add_argument("symbol", help="Ticker symbol to fetch, such as SPY.")
|
||||||
|
end_date_group = parser.add_mutually_exclusive_group()
|
||||||
|
end_date_group.add_argument(
|
||||||
|
"--end-date",
|
||||||
|
type=parse_end_date,
|
||||||
|
help="Request end date in YYYYMMDD format. Defaults to yesterday.",
|
||||||
|
)
|
||||||
|
end_date_group.add_argument(
|
||||||
|
"--end-date-from-parquet",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Use the oldest date from the symbol Parquet file as the request "
|
||||||
|
"end date. Defaults to today if the file is missing or empty."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--duration",
|
||||||
|
type=parse_duration,
|
||||||
|
default=DEFAULT_DURATION,
|
||||||
|
help=(
|
||||||
|
"IBKR duration string, such as '1 W' or '1 M'. "
|
||||||
|
f"Defaults to {DEFAULT_DURATION}."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-dir",
|
||||||
|
type=Path,
|
||||||
|
default=DEFAULT_OUTPUT_DIR,
|
||||||
|
help=f"Directory for Parquet files. Defaults to {DEFAULT_OUTPUT_DIR}.",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Run the daily candle fetcher."""
|
||||||
|
|
||||||
|
args = parse_args()
|
||||||
|
symbol = args.symbol.upper()
|
||||||
|
output_path = args.output_dir / f"{symbol}.parquet"
|
||||||
|
end_date = (
|
||||||
|
oldest_stored_date_or_today(output_path)
|
||||||
|
if args.end_date_from_parquet
|
||||||
|
else args.end_date or default_end_date()
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Fetching {symbol} daily candles ending {end_date:%Y-%m-%d} "
|
||||||
|
f"for duration {args.duration}"
|
||||||
|
)
|
||||||
|
candles = fetch_daily_candles(symbol, end_date, args.duration)
|
||||||
|
print_candles(symbol, candles)
|
||||||
|
stored = write_candles(output_path, symbol, candles)
|
||||||
|
print(f"Wrote {len(stored)} total daily rows to {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = "==3.11.*"
|
||||||
|
resolution-markers = [
|
||||||
|
"sys_platform == 'win32'",
|
||||||
|
"sys_platform == 'emscripten'",
|
||||||
|
"sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "eventkit"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/16/1e/0fac4e45d71ace143a2673ec642701c3cd16f833a0e77a57fa6a40472696/eventkit-1.0.3.tar.gz", hash = "sha256:99497f6f3c638a50ff7616f2f8cd887b18bbff3765dc1bd8681554db1467c933", size = 28320, upload-time = "2023-12-11T11:41:35.339Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/d9/7497d650b69b420e1a913329a843e16c715dac883750679240ef00a921e2/eventkit-1.0.3-py3-none-any.whl", hash = "sha256:0e199527a89aff9d195b9671ad45d2cc9f79ecda0900de8ecfb4c864d67ad6a2", size = 31837, upload-time = "2023-12-11T11:41:33.358Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ib-insync"
|
||||||
|
version = "0.9.86"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "eventkit" },
|
||||||
|
{ name = "nest-asyncio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/55/bb/733d5c81c8c2f54e90898afc7ff3a99f4d53619e6917c848833f9cc1ab56/ib_insync-0.9.86.tar.gz", hash = "sha256:73af602ca2463f260999970c5bd937b1c4325e383686eff301743a4de08d381e", size = 69859, upload-time = "2023-07-02T12:43:31.968Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/f3/28ea87be30570f4d6b8fd24380d12fa74e59467ee003755e76aeb29082b8/ib_insync-0.9.86-py3-none-any.whl", hash = "sha256:a61fbe56ff405d93d211dad8238d7300de76dd6399eafc04c320470edec9a4a4", size = 72980, upload-time = "2023-07-02T12:43:29.928Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nest-asyncio"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pandas"
|
||||||
|
version = "3.0.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyarrow"
|
||||||
|
version = "25.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/27/f3/95428098d1fa7d04432fb750eed06b41304c2f6a5d3319985e64db2d9d41/pyarrow-25.0.0.tar.gz", hash = "sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4", size = 1199181, upload-time = "2026-07-10T08:29:50.116Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/98/ae2b5acf9876dbeffa6f320776242c52caab062df55c8ac5501ed2679e74/pyarrow-25.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517", size = 35939080, upload-time = "2026-07-10T08:26:04.53Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/09/3de2a968edbd496c86cb8b932cdbee2d4b08c4a28e9884a15e5c705a646b/pyarrow-25.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e", size = 37633420, upload-time = "2026-07-10T08:26:10.354Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/86/8399243a4ce080426ec37db18d5e29148b7ec960a8a8c7f9059a7bf6ef0a/pyarrow-25.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062", size = 46861050, upload-time = "2026-07-10T08:26:16.397Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/79/72d704b02bc5fc6d06954d76a0208c1e79cad3ab370f6d6a91ffe5078870/pyarrow-25.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be", size = 50056458, upload-time = "2026-07-10T08:26:23.271Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/5d/3c31a60b6403d63cad2e0f829096f5fc5763a129ead4207a5d4690b96448/pyarrow-25.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f", size = 49957793, upload-time = "2026-07-10T08:26:30.232Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/f7/8f8a019061f9863a831915329264372a87ed25eaf9109ce56eb0e84012c5/pyarrow-25.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e", size = 53100544, upload-time = "2026-07-10T08:26:36.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/e2/738071e95c5ddad7b3dfc12f569ffa992db89d7d7b4a95258fd184191249/pyarrow-25.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8", size = 27848311, upload-time = "2026-07-10T08:26:41.429Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trading-bot"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "ib-insync" },
|
||||||
|
{ name = "pandas" },
|
||||||
|
{ name = "pyarrow" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "ib-insync", specifier = ">=0.9.86" },
|
||||||
|
{ name = "pandas", specifier = ">=2.3.0" },
|
||||||
|
{ name = "pyarrow", specifier = ">=20.0.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = []
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2026.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user