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 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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