vikuna 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vikuna-0.1.0/PKG-INFO +155 -0
- vikuna-0.1.0/README.md +139 -0
- vikuna-0.1.0/pyproject.toml +30 -0
- vikuna-0.1.0/setup.cfg +4 -0
- vikuna-0.1.0/tests/test_account.py +50 -0
- vikuna-0.1.0/tests/test_db.py +142 -0
- vikuna-0.1.0/tests/test_market_data.py +108 -0
- vikuna-0.1.0/tests/test_options.py +331 -0
- vikuna-0.1.0/tests/test_trading.py +167 -0
- vikuna-0.1.0/vikuna/__init__.py +37 -0
- vikuna-0.1.0/vikuna/account.py +72 -0
- vikuna-0.1.0/vikuna/data_types.py +139 -0
- vikuna-0.1.0/vikuna/db.py +274 -0
- vikuna-0.1.0/vikuna/market_data.py +157 -0
- vikuna-0.1.0/vikuna/options.py +428 -0
- vikuna-0.1.0/vikuna/trading.py +376 -0
- vikuna-0.1.0/vikuna.egg-info/PKG-INFO +155 -0
- vikuna-0.1.0/vikuna.egg-info/SOURCES.txt +20 -0
- vikuna-0.1.0/vikuna.egg-info/dependency_links.txt +1 -0
- vikuna-0.1.0/vikuna.egg-info/entry_points.txt +2 -0
- vikuna-0.1.0/vikuna.egg-info/requires.txt +8 -0
- vikuna-0.1.0/vikuna.egg-info/top_level.txt +1 -0
vikuna-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vikuna
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Alpaca paper trading environment
|
|
5
|
+
Author-email: William Kruta <wjkruta@gmail.com>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: alpaca-py>=0.43.2
|
|
9
|
+
Requires-Dist: duckdb>=1.5.1
|
|
10
|
+
Requires-Dist: polars>=1.39.3
|
|
11
|
+
Requires-Dist: pytz>=2024.1
|
|
12
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
13
|
+
Requires-Dist: yahoors>=0.1.2
|
|
14
|
+
Requires-Dist: build>=1.4.2
|
|
15
|
+
Requires-Dist: twine>=6.2.0
|
|
16
|
+
|
|
17
|
+
# Vikuna
|
|
18
|
+
|
|
19
|
+
Lightweight Python library for Alpaca paper trading, portfolio inspection, equity and options market data access, and DuckDB-backed bar caching. The package is structured so trading, account, market data, options, and storage can be used independently.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- Place and manage Alpaca paper-trading orders
|
|
24
|
+
- Submit advanced equity orders including trailing stop, bracket, OCO, and OTO
|
|
25
|
+
- Read account snapshots and open positions
|
|
26
|
+
- Fetch historical bars and latest quotes
|
|
27
|
+
- Discover option contracts, read option quotes/bars, and place single-leg option orders
|
|
28
|
+
- Submit multi-leg option orders and inspect option chain snapshots with greeks
|
|
29
|
+
- Validate invalid order combinations before hitting the Alpaca API
|
|
30
|
+
- Cache bars locally in DuckDB for incremental sync workflows
|
|
31
|
+
- Work with typed dataclasses and Polars dataframes
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Use the checked-in `uv` environment when possible:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv sync --dev
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Fallback:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python -m venv .venv
|
|
45
|
+
. .venv/bin/activate
|
|
46
|
+
pip install -r requirements.txt
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
Create a local `.env` file from `.env.example`:
|
|
52
|
+
|
|
53
|
+
```env
|
|
54
|
+
ALPACA_API_KEY=your_api_key_here
|
|
55
|
+
ALPACA_SECRET_KEY=your_secret_key_here
|
|
56
|
+
ALPACA_BASE_URL=https://paper-api.alpaca.markets
|
|
57
|
+
DB_PATH=./data/market_data.duckdb
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use paper-trading credentials for development.
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from datetime import datetime, timedelta, timezone
|
|
66
|
+
from vikuna import (
|
|
67
|
+
AlpacaAccountClient,
|
|
68
|
+
AlpacaMarketData,
|
|
69
|
+
AlpacaOptionsClient,
|
|
70
|
+
MarketDatabase,
|
|
71
|
+
MultiLegOrderRequest,
|
|
72
|
+
OptionOrderLeg,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
account_client = AlpacaAccountClient()
|
|
76
|
+
account = account_client.get_account()
|
|
77
|
+
print(account.portfolio_value)
|
|
78
|
+
|
|
79
|
+
market_data = AlpacaMarketData()
|
|
80
|
+
db = MarketDatabase()
|
|
81
|
+
|
|
82
|
+
end = datetime.now(timezone.utc)
|
|
83
|
+
start = end - timedelta(days=30)
|
|
84
|
+
bars = market_data.get_bars("AAPL", "1d", start, end)
|
|
85
|
+
db.save_bars(bars)
|
|
86
|
+
|
|
87
|
+
latest = db.get_latest_timestamp("AAPL", "1d")
|
|
88
|
+
print(latest)
|
|
89
|
+
db.close()
|
|
90
|
+
|
|
91
|
+
options = AlpacaOptionsClient()
|
|
92
|
+
contracts = options.list_contracts("AAPL", contract_type="call", limit=5)
|
|
93
|
+
print(contracts)
|
|
94
|
+
|
|
95
|
+
chain = options.get_chain_snapshot("AAPL", contract_type="call")
|
|
96
|
+
print(chain.select(["symbol", "bid_price", "ask_price", "delta", "theta"]))
|
|
97
|
+
|
|
98
|
+
db.save_option_bars(options.get_bars("AAPL240621C00190000", "1d", start, end))
|
|
99
|
+
db.save_option_snapshots(options.list_chain_snapshots("AAPL", contract_type="call"))
|
|
100
|
+
|
|
101
|
+
spread = MultiLegOrderRequest(
|
|
102
|
+
qty=1,
|
|
103
|
+
order_type="limit",
|
|
104
|
+
limit_price=1.25,
|
|
105
|
+
legs=[
|
|
106
|
+
OptionOrderLeg("AAPL240621C00190000", "buy", "buy_to_open"),
|
|
107
|
+
OptionOrderLeg("AAPL240621C00200000", "sell", "sell_to_open"),
|
|
108
|
+
],
|
|
109
|
+
)
|
|
110
|
+
# options.submit_multileg_order(spread)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## API Overview
|
|
114
|
+
|
|
115
|
+
- `AlpacaTradingClient`: `buy`, `sell`, `cancel_order`, `cancel_all_orders`, `get_order`, `list_orders`
|
|
116
|
+
- `AlpacaAccountClient`: `get_account`, `get_positions`, `get_position`, `close_position`, `close_all_positions`
|
|
117
|
+
- `AlpacaMarketData`: `get_bars`, `get_latest_bar`, `get_quote`
|
|
118
|
+
- `AlpacaOptionsClient`: `list_contracts`, `get_contract`, `get_bars`, `get_latest_quote`, `buy`, `sell`
|
|
119
|
+
- `AlpacaOptionsClient`: also `submit_multileg_order` and `get_chain_snapshot`
|
|
120
|
+
- `AlpacaOptionsClient`: also `list_chain_snapshots` for typed snapshot objects
|
|
121
|
+
- `MarketDatabase`: `save_bars`, `load_bars`, `get_latest_timestamp`, `sync`
|
|
122
|
+
- `MarketDatabase`: also `save_option_bars`, `load_option_bars`, `get_latest_option_timestamp`, `sync_option_bars`
|
|
123
|
+
- `MarketDatabase`: also `save_option_snapshots`, `load_option_snapshots`, `sync_option_snapshots`
|
|
124
|
+
|
|
125
|
+
Shared types exported by the package:
|
|
126
|
+
|
|
127
|
+
- `Bar`
|
|
128
|
+
- `Order`
|
|
129
|
+
- `Position`
|
|
130
|
+
- `Account`
|
|
131
|
+
- `OptionContract`
|
|
132
|
+
- `OptionOrderLeg`
|
|
133
|
+
- `MultiLegOrderRequest`
|
|
134
|
+
- `OptionQuote`
|
|
135
|
+
- `OptionSnapshot`
|
|
136
|
+
- `BAR_SCHEMA`
|
|
137
|
+
|
|
138
|
+
## Notes
|
|
139
|
+
|
|
140
|
+
- Options support is implemented against Alpaca’s current options trading and market data APIs.
|
|
141
|
+
- Futures are not implemented in this library yet because Alpaca does not expose a stable futures API surface here.
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
Run the test suite:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
.venv/bin/python -m pytest tests -q
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Run the local smoke example:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
python main.py
|
|
155
|
+
```
|
vikuna-0.1.0/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Vikuna
|
|
2
|
+
|
|
3
|
+
Lightweight Python library for Alpaca paper trading, portfolio inspection, equity and options market data access, and DuckDB-backed bar caching. The package is structured so trading, account, market data, options, and storage can be used independently.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Place and manage Alpaca paper-trading orders
|
|
8
|
+
- Submit advanced equity orders including trailing stop, bracket, OCO, and OTO
|
|
9
|
+
- Read account snapshots and open positions
|
|
10
|
+
- Fetch historical bars and latest quotes
|
|
11
|
+
- Discover option contracts, read option quotes/bars, and place single-leg option orders
|
|
12
|
+
- Submit multi-leg option orders and inspect option chain snapshots with greeks
|
|
13
|
+
- Validate invalid order combinations before hitting the Alpaca API
|
|
14
|
+
- Cache bars locally in DuckDB for incremental sync workflows
|
|
15
|
+
- Work with typed dataclasses and Polars dataframes
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Use the checked-in `uv` environment when possible:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv sync --dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Fallback:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -m venv .venv
|
|
29
|
+
. .venv/bin/activate
|
|
30
|
+
pip install -r requirements.txt
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
Create a local `.env` file from `.env.example`:
|
|
36
|
+
|
|
37
|
+
```env
|
|
38
|
+
ALPACA_API_KEY=your_api_key_here
|
|
39
|
+
ALPACA_SECRET_KEY=your_secret_key_here
|
|
40
|
+
ALPACA_BASE_URL=https://paper-api.alpaca.markets
|
|
41
|
+
DB_PATH=./data/market_data.duckdb
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use paper-trading credentials for development.
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from datetime import datetime, timedelta, timezone
|
|
50
|
+
from vikuna import (
|
|
51
|
+
AlpacaAccountClient,
|
|
52
|
+
AlpacaMarketData,
|
|
53
|
+
AlpacaOptionsClient,
|
|
54
|
+
MarketDatabase,
|
|
55
|
+
MultiLegOrderRequest,
|
|
56
|
+
OptionOrderLeg,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
account_client = AlpacaAccountClient()
|
|
60
|
+
account = account_client.get_account()
|
|
61
|
+
print(account.portfolio_value)
|
|
62
|
+
|
|
63
|
+
market_data = AlpacaMarketData()
|
|
64
|
+
db = MarketDatabase()
|
|
65
|
+
|
|
66
|
+
end = datetime.now(timezone.utc)
|
|
67
|
+
start = end - timedelta(days=30)
|
|
68
|
+
bars = market_data.get_bars("AAPL", "1d", start, end)
|
|
69
|
+
db.save_bars(bars)
|
|
70
|
+
|
|
71
|
+
latest = db.get_latest_timestamp("AAPL", "1d")
|
|
72
|
+
print(latest)
|
|
73
|
+
db.close()
|
|
74
|
+
|
|
75
|
+
options = AlpacaOptionsClient()
|
|
76
|
+
contracts = options.list_contracts("AAPL", contract_type="call", limit=5)
|
|
77
|
+
print(contracts)
|
|
78
|
+
|
|
79
|
+
chain = options.get_chain_snapshot("AAPL", contract_type="call")
|
|
80
|
+
print(chain.select(["symbol", "bid_price", "ask_price", "delta", "theta"]))
|
|
81
|
+
|
|
82
|
+
db.save_option_bars(options.get_bars("AAPL240621C00190000", "1d", start, end))
|
|
83
|
+
db.save_option_snapshots(options.list_chain_snapshots("AAPL", contract_type="call"))
|
|
84
|
+
|
|
85
|
+
spread = MultiLegOrderRequest(
|
|
86
|
+
qty=1,
|
|
87
|
+
order_type="limit",
|
|
88
|
+
limit_price=1.25,
|
|
89
|
+
legs=[
|
|
90
|
+
OptionOrderLeg("AAPL240621C00190000", "buy", "buy_to_open"),
|
|
91
|
+
OptionOrderLeg("AAPL240621C00200000", "sell", "sell_to_open"),
|
|
92
|
+
],
|
|
93
|
+
)
|
|
94
|
+
# options.submit_multileg_order(spread)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API Overview
|
|
98
|
+
|
|
99
|
+
- `AlpacaTradingClient`: `buy`, `sell`, `cancel_order`, `cancel_all_orders`, `get_order`, `list_orders`
|
|
100
|
+
- `AlpacaAccountClient`: `get_account`, `get_positions`, `get_position`, `close_position`, `close_all_positions`
|
|
101
|
+
- `AlpacaMarketData`: `get_bars`, `get_latest_bar`, `get_quote`
|
|
102
|
+
- `AlpacaOptionsClient`: `list_contracts`, `get_contract`, `get_bars`, `get_latest_quote`, `buy`, `sell`
|
|
103
|
+
- `AlpacaOptionsClient`: also `submit_multileg_order` and `get_chain_snapshot`
|
|
104
|
+
- `AlpacaOptionsClient`: also `list_chain_snapshots` for typed snapshot objects
|
|
105
|
+
- `MarketDatabase`: `save_bars`, `load_bars`, `get_latest_timestamp`, `sync`
|
|
106
|
+
- `MarketDatabase`: also `save_option_bars`, `load_option_bars`, `get_latest_option_timestamp`, `sync_option_bars`
|
|
107
|
+
- `MarketDatabase`: also `save_option_snapshots`, `load_option_snapshots`, `sync_option_snapshots`
|
|
108
|
+
|
|
109
|
+
Shared types exported by the package:
|
|
110
|
+
|
|
111
|
+
- `Bar`
|
|
112
|
+
- `Order`
|
|
113
|
+
- `Position`
|
|
114
|
+
- `Account`
|
|
115
|
+
- `OptionContract`
|
|
116
|
+
- `OptionOrderLeg`
|
|
117
|
+
- `MultiLegOrderRequest`
|
|
118
|
+
- `OptionQuote`
|
|
119
|
+
- `OptionSnapshot`
|
|
120
|
+
- `BAR_SCHEMA`
|
|
121
|
+
|
|
122
|
+
## Notes
|
|
123
|
+
|
|
124
|
+
- Options support is implemented against Alpaca’s current options trading and market data APIs.
|
|
125
|
+
- Futures are not implemented in this library yet because Alpaca does not expose a stable futures API surface here.
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
Run the test suite:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
.venv/bin/python -m pytest tests -q
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Run the local smoke example:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python main.py
|
|
139
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vikuna"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Alpaca paper trading environment"
|
|
5
|
+
authors = [{ name = "William Kruta", email = "wjkruta@gmail.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"alpaca-py>=0.43.2",
|
|
10
|
+
"duckdb>=1.5.1",
|
|
11
|
+
"polars>=1.39.3",
|
|
12
|
+
"pytz>=2024.1",
|
|
13
|
+
"python-dotenv>=1.2.2",
|
|
14
|
+
"yahoors>=0.1.2",
|
|
15
|
+
"build>=1.4.2",
|
|
16
|
+
"twine>=6.2.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
alpacatrading = "main:main"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = ["pytest", "pytest-mock"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
testpaths = ["tests"]
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["setuptools>=61.0"]
|
|
30
|
+
build-backend = "setuptools.build_meta"
|
vikuna-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from alpaca_trading.account import AlpacaAccountClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_utc(*args):
|
|
12
|
+
return datetime(*args, tzinfo=timezone.utc)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def make_position(symbol="AAPL", side="long", asset_class="us_equity"):
|
|
16
|
+
position = MagicMock()
|
|
17
|
+
position.symbol = symbol
|
|
18
|
+
position.qty = Decimal("10")
|
|
19
|
+
position.side = MagicMock(value=side)
|
|
20
|
+
position.avg_entry_price = Decimal("185.50")
|
|
21
|
+
position.current_price = Decimal("188.00")
|
|
22
|
+
position.market_value = Decimal("1880.00")
|
|
23
|
+
position.unrealized_pl = Decimal("25.00")
|
|
24
|
+
position.unrealized_plpc = Decimal("0.0135")
|
|
25
|
+
position.asset_class = MagicMock(value=asset_class)
|
|
26
|
+
return position
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def mock_trading_client():
|
|
31
|
+
with patch("alpaca_trading.account.TradingClient") as mock_client:
|
|
32
|
+
yield mock_client.return_value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def client(mock_trading_client):
|
|
37
|
+
return AlpacaAccountClient(api_key="test_key", secret_key="test_secret")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_get_positions_can_filter_option_positions(client, mock_trading_client):
|
|
41
|
+
mock_trading_client.get_all_positions.return_value = [
|
|
42
|
+
make_position("AAPL", asset_class="us_equity"),
|
|
43
|
+
make_position("AAPL240621C00190000", asset_class="us_option"),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
result = client.get_option_positions()
|
|
47
|
+
|
|
48
|
+
assert isinstance(result, pl.DataFrame)
|
|
49
|
+
assert len(result) == 1
|
|
50
|
+
assert result["ticker"][0] == "AAPL240621C00190000"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
from unittest.mock import MagicMock
|
|
3
|
+
|
|
4
|
+
import polars as pl
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from alpaca_trading.data_types import BAR_SCHEMA, OptionSnapshot
|
|
8
|
+
from alpaca_trading.db import MarketDatabase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_utc(*args):
|
|
12
|
+
return datetime(*args, tzinfo=timezone.utc)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def sample_bars_df(ticker="AAPL", interval="1d", n=3):
|
|
16
|
+
base = make_utc(2024, 1, 2)
|
|
17
|
+
return pl.DataFrame(
|
|
18
|
+
{
|
|
19
|
+
"ticker": [ticker] * n,
|
|
20
|
+
"interval": [interval] * n,
|
|
21
|
+
"timestamp": [base + timedelta(days=i) for i in range(n)],
|
|
22
|
+
"open": [185.0 + i for i in range(n)],
|
|
23
|
+
"high": [186.0 + i for i in range(n)],
|
|
24
|
+
"low": [184.0 + i for i in range(n)],
|
|
25
|
+
"close": [185.5 + i for i in range(n)],
|
|
26
|
+
"volume": [50_000_000 + i * 1000 for i in range(n)],
|
|
27
|
+
"vwap": [185.2 + i for i in range(n)],
|
|
28
|
+
"trade_count": [300_000 + i * 100 for i in range(n)],
|
|
29
|
+
},
|
|
30
|
+
schema=BAR_SCHEMA,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def sample_option_snapshots():
|
|
35
|
+
return [
|
|
36
|
+
OptionSnapshot(
|
|
37
|
+
symbol="AAPL240621C00190000",
|
|
38
|
+
bid_price=4.5,
|
|
39
|
+
ask_price=4.6,
|
|
40
|
+
bid_size=10,
|
|
41
|
+
ask_size=12,
|
|
42
|
+
implied_volatility=0.28,
|
|
43
|
+
delta=0.52,
|
|
44
|
+
gamma=0.08,
|
|
45
|
+
rho=0.03,
|
|
46
|
+
theta=-0.04,
|
|
47
|
+
vega=0.11,
|
|
48
|
+
),
|
|
49
|
+
OptionSnapshot(
|
|
50
|
+
symbol="AAPL240621P00190000",
|
|
51
|
+
bid_price=3.8,
|
|
52
|
+
ask_price=3.9,
|
|
53
|
+
bid_size=8,
|
|
54
|
+
ask_size=9,
|
|
55
|
+
implied_volatility=0.31,
|
|
56
|
+
delta=-0.48,
|
|
57
|
+
gamma=0.07,
|
|
58
|
+
rho=-0.02,
|
|
59
|
+
theta=-0.03,
|
|
60
|
+
vega=0.10,
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def db():
|
|
67
|
+
database = MarketDatabase(db_path=":memory:")
|
|
68
|
+
yield database
|
|
69
|
+
database.close()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_save_and_load_bars(db):
|
|
73
|
+
df = sample_bars_df()
|
|
74
|
+
db.save_bars(df)
|
|
75
|
+
result = db.load_bars("AAPL", "1d", make_utc(2024, 1, 1), make_utc(2024, 1, 10))
|
|
76
|
+
assert len(result) == 3
|
|
77
|
+
assert result["ticker"][0] == "AAPL"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_get_latest_timestamp_returns_max(db):
|
|
81
|
+
db.save_bars(sample_bars_df(n=3))
|
|
82
|
+
ts = db.get_latest_timestamp("AAPL", "1d")
|
|
83
|
+
assert ts == datetime(2024, 1, 4, tzinfo=timezone.utc)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_sync_fetches_and_saves_new_bars(db):
|
|
87
|
+
mock_market_data = MagicMock()
|
|
88
|
+
new_bars = sample_bars_df(n=5)
|
|
89
|
+
mock_market_data.get_bars.return_value = new_bars
|
|
90
|
+
|
|
91
|
+
result = db.sync("AAPL", "1d", mock_market_data)
|
|
92
|
+
|
|
93
|
+
assert len(result) == 5
|
|
94
|
+
stored = db.load_bars("AAPL", "1d", make_utc(2024, 1, 1), make_utc(2024, 1, 10))
|
|
95
|
+
assert len(stored) == 5
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_save_and_load_option_bars(db):
|
|
99
|
+
df = sample_bars_df(ticker="AAPL240621C00190000")
|
|
100
|
+
db.save_option_bars(df)
|
|
101
|
+
result = db.load_option_bars(
|
|
102
|
+
"AAPL240621C00190000",
|
|
103
|
+
"1d",
|
|
104
|
+
make_utc(2024, 1, 1),
|
|
105
|
+
make_utc(2024, 1, 10),
|
|
106
|
+
)
|
|
107
|
+
assert len(result) == 3
|
|
108
|
+
assert result["ticker"][0] == "AAPL240621C00190000"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_sync_option_bars_fetches_and_saves_new_bars(db):
|
|
112
|
+
mock_options_data = MagicMock()
|
|
113
|
+
new_bars = sample_bars_df(ticker="AAPL240621C00190000", n=4)
|
|
114
|
+
mock_options_data.get_bars.return_value = new_bars
|
|
115
|
+
|
|
116
|
+
result = db.sync_option_bars("AAPL240621C00190000", "1d", mock_options_data)
|
|
117
|
+
|
|
118
|
+
assert len(result) == 4
|
|
119
|
+
stored = db.load_option_bars(
|
|
120
|
+
"AAPL240621C00190000",
|
|
121
|
+
"1d",
|
|
122
|
+
make_utc(2024, 1, 1),
|
|
123
|
+
make_utc(2024, 1, 10),
|
|
124
|
+
)
|
|
125
|
+
assert len(stored) == 4
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_save_and_load_option_snapshots(db):
|
|
129
|
+
db.save_option_snapshots(sample_option_snapshots())
|
|
130
|
+
result = db.load_option_snapshots()
|
|
131
|
+
assert len(result) == 2
|
|
132
|
+
assert result["symbol"][0] == "AAPL240621C00190000"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_sync_option_snapshots_fetches_and_saves_chain(db):
|
|
136
|
+
mock_options_data = MagicMock()
|
|
137
|
+
mock_options_data.list_chain_snapshots.return_value = sample_option_snapshots()
|
|
138
|
+
|
|
139
|
+
result = db.sync_option_snapshots("AAPL", mock_options_data, contract_type="call")
|
|
140
|
+
|
|
141
|
+
assert len(result) == 2
|
|
142
|
+
mock_options_data.list_chain_snapshots.assert_called_once()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import polars as pl
|
|
5
|
+
import pytest
|
|
6
|
+
from alpaca.common.exceptions import APIError
|
|
7
|
+
|
|
8
|
+
from alpaca_trading.data_types import Bar
|
|
9
|
+
from alpaca_trading.market_data import AlpacaMarketData
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def make_utc(*args):
|
|
13
|
+
return datetime(*args, tzinfo=timezone.utc)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def make_alpaca_bar(ts, o, h, l, c, v, vwap=None, trade_count=None):
|
|
17
|
+
bar = MagicMock()
|
|
18
|
+
bar.timestamp = ts
|
|
19
|
+
bar.open = o
|
|
20
|
+
bar.high = h
|
|
21
|
+
bar.low = l
|
|
22
|
+
bar.close = c
|
|
23
|
+
bar.volume = v
|
|
24
|
+
bar.vwap = vwap
|
|
25
|
+
bar.trade_count = trade_count
|
|
26
|
+
return bar
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def mock_data_client():
|
|
31
|
+
with patch("alpaca_trading.market_data.StockHistoricalDataClient") as mock_client:
|
|
32
|
+
yield mock_client.return_value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def mock_yahoo():
|
|
37
|
+
with patch("alpaca_trading.market_data.Candles") as mock_yahoo_cls:
|
|
38
|
+
yield mock_yahoo_cls.return_value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def market_data(mock_data_client, mock_yahoo):
|
|
43
|
+
return AlpacaMarketData(api_key="test_key", secret_key="test_secret")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_get_bars_returns_polars_dataframe(market_data, mock_data_client):
|
|
47
|
+
alpaca_bar = make_alpaca_bar(make_utc(2024, 1, 2), 185.0, 186.5, 184.0, 185.9, 50_000_000, 185.4, 312_000)
|
|
48
|
+
bar_set = MagicMock()
|
|
49
|
+
bar_set.__getitem__ = MagicMock(return_value=[alpaca_bar])
|
|
50
|
+
mock_data_client.get_stock_bars.return_value = bar_set
|
|
51
|
+
|
|
52
|
+
result = market_data.get_bars("AAPL", "1d", make_utc(2024, 1, 1), make_utc(2024, 1, 3))
|
|
53
|
+
|
|
54
|
+
assert isinstance(result, pl.DataFrame)
|
|
55
|
+
assert len(result) == 1
|
|
56
|
+
assert result["ticker"][0] == "AAPL"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_get_bars_falls_back_to_yahoo_on_api_error(market_data, mock_data_client, mock_yahoo):
|
|
60
|
+
mock_data_client.get_stock_bars.side_effect = APIError({"message": "subscription does not permit querying recent SIP data"})
|
|
61
|
+
mock_yahoo.get_candles.return_value = pl.DataFrame(
|
|
62
|
+
{
|
|
63
|
+
"date": [make_utc(2024, 1, 2)],
|
|
64
|
+
"ticker": ["AAPL"],
|
|
65
|
+
"interval": ["1d"],
|
|
66
|
+
"close": [185.9],
|
|
67
|
+
"open": [185.0],
|
|
68
|
+
"low": [184.0],
|
|
69
|
+
"high": [186.5],
|
|
70
|
+
"volume": [50_000_000],
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
result = market_data.get_bars("AAPL", "1d", make_utc(2024, 1, 1), make_utc(2024, 1, 3))
|
|
75
|
+
|
|
76
|
+
assert len(result) == 1
|
|
77
|
+
assert result["close"][0] == 185.9
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_get_latest_bar_falls_back_to_yahoo(market_data, mock_data_client, mock_yahoo):
|
|
81
|
+
mock_data_client.get_stock_latest_bar.side_effect = APIError({"message": "subscription does not permit querying recent SIP data"})
|
|
82
|
+
mock_yahoo.get_candles.return_value = pl.DataFrame(
|
|
83
|
+
{
|
|
84
|
+
"date": [make_utc(2024, 1, 2)],
|
|
85
|
+
"ticker": ["AAPL"],
|
|
86
|
+
"interval": ["1d"],
|
|
87
|
+
"close": [185.9],
|
|
88
|
+
"open": [185.0],
|
|
89
|
+
"low": [184.0],
|
|
90
|
+
"high": [186.5],
|
|
91
|
+
"volume": [50_000_000],
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
result = market_data.get_latest_bar("AAPL", "1d")
|
|
96
|
+
|
|
97
|
+
assert isinstance(result, Bar)
|
|
98
|
+
assert result.close == 185.9
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_get_quote_falls_back_to_yahoo_last_price(market_data, mock_data_client, mock_yahoo):
|
|
102
|
+
mock_data_client.get_stock_latest_quote.side_effect = APIError({"message": "subscription does not permit querying recent SIP data"})
|
|
103
|
+
mock_yahoo.get_last_price.return_value = {"AAPL": 185.9}
|
|
104
|
+
|
|
105
|
+
result = market_data.get_quote("AAPL")
|
|
106
|
+
|
|
107
|
+
assert result["bid"] == 185.9
|
|
108
|
+
assert result["ask"] == 185.9
|