Compare commits

...
2 Commits
Author SHA1 Message Date
jarno e2eb5dd574 Data fetcher implementation 2026-07-26 14:18:07 +03:00
jarno 8d3e663e71 Initial hello world IBKR API connection. 2026-07-25 22:19:31 +03:00
11 changed files with 671 additions and 3 deletions
+11
View File
@@ -0,0 +1,11 @@
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.ruff_cache/
.venv/
dist/
build/
*.egg-info/
data/ibkr/
+2
View File
@@ -19,8 +19,10 @@ Keep these areas loosely separated in code and documentation. Avoid coupling the
- Use Python for implementation.
- 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`.
- 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.
## Safety And Trading Constraints
+15 -2
View File
@@ -15,15 +15,24 @@ This repository is at the planning and scaffolding stage. The machine learning m
## Environment
Use `mise` to manage Python.
Use `mise` to manage Python and `uv` to manage Python packages.
```sh
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`.
## 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
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
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
View File
@@ -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.
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:
- asset universe;
@@ -72,7 +76,7 @@ Possible scope:
## Near-Term Priorities
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.
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.
+82
View File
@@ -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.
+86
View File
@@ -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.
+21
View File
@@ -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"
+1
View File
@@ -0,0 +1 @@
"""Trading bot package."""
+1
View File
@@ -0,0 +1 @@
"""Training data collection tools."""
+293
View File
@@ -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()
Generated
+154
View File
@@ -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" },
]