Files
Danglewood 869cfed125 [Release] openbb-v5.0.0 (#7741)
* merge branch develop with main

* bump desktop version

* fix the merge

* sync Cargo.lock to desktop 2.0.0

* fix cli conftest.py

---------

Co-authored-by: deeleeramone <>
2026-09-29 10:58:44 -07:00

463 lines
13 KiB
Python

"""Shared fixtures for the offline TMX suite."""
import pytest
@pytest.fixture(autouse=True, scope="session")
def _isolated_result_cache(tmp_path_factory):
"""Keep the suite's stubbed results out of the real cache."""
import os
from openbb_tmx.utils.memo import CACHE_DIR_ENV
directory = tmp_path_factory.mktemp("tmx_results")
previous = os.environ.get(CACHE_DIR_ENV)
os.environ[CACHE_DIR_ENV] = str(directory)
yield
if previous is None:
os.environ.pop(CACHE_DIR_ENV, None)
else:
os.environ[CACHE_DIR_ENV] = previous
SAMPLES: dict = {
"getQuoteBySymbol": {
"getQuoteBySymbol": {
"symbol": "AC",
"name": "Air Canada",
"price": 23.27,
"priceChange": 0.62,
"percentChange": 2.74,
"exchangeName": "Toronto Stock Exchange",
"exShortName": "TSX",
"exchangeCode": "XTSE",
"sector": "Industrials",
"industry": "Airlines",
"volume": 1958163,
"openPrice": 22.78,
"dayHigh": 23.43,
"dayLow": 22.6,
"MarketCap": 8000000000,
"MarketCapAllClasses": 8000000000,
"peRatio": 6.1,
"prevClose": 22.65,
"beta": 1.9,
"eps": 3.8,
"longDescription": "Air Canada is an airline.",
"website": "https://aircanada.com",
"employees": 38000,
"totalSharesOutStanding": 350000000,
"vwap": 23.1,
"weeks52high": 25.0,
"weeks52low": 14.0,
"issueType": "Equity",
"currency": "CAD",
"close": 23.27,
"qmdescription": "Air Canada",
}
},
"getQuoteForSymbols": {
"getQuoteForSymbols": [
{
"symbol": "^TSX",
"longname": "S&P/TSX",
"price": 35369.1,
"percentChange": 0.4,
"priceChange": 140.0,
"volume": 1,
},
]
},
"getTimeSeriesData": {
"getTimeSeriesData": [
{
"dateTime": "2026-07-23T16:00:00-04:00",
"open": 22.9,
"high": 23.4,
"low": 22.8,
"close": 23.2,
"volume": 100,
},
{
"dateTime": "2026-07-24T16:00:00-04:00",
"open": 23.0,
"high": 23.5,
"low": 22.9,
"close": 23.27,
"volume": 120,
},
]
},
"getCompanyPriceHistory": {
"getCompanyPriceHistory": [
{
"datetime": "2026-07-24",
"openPrice": 23.0,
"closePrice": 23.27,
"high": 23.5,
"low": 22.9,
"volume": 120,
"tradeValue": 2700.0,
"numberOfTrade": 10,
"change": 0.62,
"changePercent": 2.74,
"vwap": 23.1,
},
]
},
"getCompanyMostRecentTrades": {
"getCompanyMostRecentTrades": [
{
"price": 23.27,
"volume": 95,
"datetime": "2026-07-24T16:00:00-04:00",
"sellerId": "79",
"sellerName": "CIBC",
"buyerId": "002",
"buyerName": "RBC",
"exchangeCode": "TSX",
},
]
},
"GetCompanyShortInterest": {
"getCompanyShortInterest": {
"TICKER": "AC",
"BUSINESS_DATE": "2026-07-24",
"SHORT_INTEREST": 6443193,
"SHORTINTERESTPCT": 0.0231,
"DAYSTOCOVER10DAY": None,
"DAYSTOCOVER30DAY": 2.44,
"DAYSTOCOVER90DAY": None,
}
},
"getSplitsForSymbol": {
"getSplitsForSymbol": [{"splitDate": "2004-04-02", "ratio": 0.5}]
},
"getDividendsForSymbol": {
"dividendHistory": {
"pageNumber": 1,
"hasNextPage": False,
"dividends": [
{
"exDate": "2026-04-01",
"amount": 1.06,
"currency": "CAD",
"payableDate": "2026-04-28",
"declarationDate": "2026-02-25",
"recordDate": "2026-04-02",
}
],
}
},
"getCompanyFilings": {
"filings": [
{
"size": "1.2 MB",
"filingDate": "2026-06-01",
"description": "Annual",
"name": "AIF",
"urlToPdf": "https://example.com/a.pdf",
}
]
},
"getNewsAndEvents": {
"news": [
{
"headline": "Air Canada reports",
"datetime": "2026-07-24T10:00:00-04:00",
"source": "CNW",
"newsid": "123",
"summary": "Air Canada reports",
}
],
"events": [
{
"title": "Q2 Earnings",
"date": "2026-08-01",
"status": "Confirmed",
"type": "Earnings",
}
],
},
"getCompanyAnalysts": {
"getCompanyAnalysts": {
"totalAnalysts": 12,
"priceTarget": {
"lowPriceTarget": 20.0,
"highPriceTarget": 32.0,
"priceTarget": 27.0,
"priceTargetUpside": 0.16,
},
"consensusAnalysts": {"consensus": "Buy", "buy": 8, "sell": 1, "hold": 3},
}
},
"getEnhancedEarningsForDate": {
"getEnhancedEarningsForDate": [
{
"symbol": "AC",
"companyName": "Air Canada",
"announceTime": "After Close",
"estimatedEps": 0.5,
"actualEps": 0.6,
"epsSurprisePercent": 20.0,
"epsSurpriseDollar": 0.1,
}
]
},
"getCompanyInsidersActivities": {
"getCompanyInsidersActivities": {
"insiderActivities": [
{
"periodkey": "3",
"buy": [
{
"name": "Doe, Jane",
"trades": 1,
"shares": 100,
"sharesHeld": 500,
"tradeValue": 2300.0,
}
],
"sell": [],
}
],
"activitySummary": [
{
"periodkey": "3",
"buyShares": 100,
"soldShares": 0,
"netActivity": 100,
"totalShares": 100,
"buyTrades": 1,
"sellTrades": 0,
"totalTrades": 1,
}
],
}
},
"getStockListSymbolsWithQuote": {
"stockList": {
"stockListId": "TOP_VOLUME",
"name": "Top Volume",
"description": "d",
"longDescription": "ld",
"metricTitle": "Volume",
"totalPriceChange": 1.0,
"totalPercentChange": 0.5,
"createdAt": "2026-01-01",
"updatedAt": "2026-07-24",
"listItems": [
{
"symbol": "AC",
"longName": "Air Canada",
"rank": 1,
"metric": 100,
"price": 23.27,
"priceChange": 0.62,
"percentChange": 2.74,
"volume": 1958163,
}
],
}
},
"getIndexConstituents": {
"constituents": [
{
"symbol": "RY",
"longName": "Royal Bank of Canada",
"shortName": "Royal Bank Of Ca",
"exchange": "TSX",
"exLongName": "Toronto Stock Exchange",
"quotedMarketValue": 412336362030,
"weight": 8.168,
}
],
"keyData": {"numConstituents": 219},
},
"getIndexKeyData": {
"getIndexKeyData": {
"peRatio": 21.26,
"pbRatio": 2.71,
"priceToSales": 2.77,
"pcfRatio": 36.44,
"divYield": 2.13,
"numConstituents": 219,
}
},
"getMarketMovers": {
"getMarketMovers": [
{
"symbol": "AC",
"name": "Air Canada",
"exchangeName": "TSX",
"exchangeCode": "XTSE",
"price": 23.27,
"priceChange": 0.62,
"percentChange": 2.74,
"volume": 1958163,
"tradeVolume": 9452,
"open": 22.78,
"high": 23.43,
"low": 22.6,
"weeks52low": 14.0,
"weeks52high": 25.0,
}
]
},
}
@pytest.fixture
def gql(monkeypatch):
"""Answer every GraphQL operation from the sample payloads."""
calls: list = []
async def fake(
operation, query, variables=None, symbol=None, use_cache=True, **kwargs
):
calls.append((operation, variables))
return SAMPLES.get(operation, {})
monkeypatch.setattr("openbb_tmx.utils.cache.amake_gql_request", fake)
return calls
@pytest.fixture
def http(monkeypatch):
"""Answer every cached HTTP request from a routing table."""
routes: dict = {}
async def fake(url, use_cache=True, accept_type="json", **kwargs):
for fragment, payload in routes.items():
if fragment in url:
return payload
return None
monkeypatch.setattr("openbb_tmx.utils.cache.amake_request", fake)
return routes
UNDERLYING_PRICE = 23.0
CHAIN_STRIKES = (19.0, 21.0, 23.0, 25.0, 27.0)
CHAIN_TENORS = (7, 35, 63)
def _contract_quote(expiry, strike, side, index, priced=True):
"""Build one vendor quote for the synthetic chain."""
distance = abs(strike - UNDERLYING_PRICE) / UNDERLYING_PRICE
premium = round(max(0.05, (UNDERLYING_PRICE * 0.04) - (distance * 6.9)), 2)
contract = f"{expiry.replace('-', '')}{side[0]}{int(strike * 1000):08d}"
return {
"contract": {
"expirydate": expiry,
"callput": side,
"strike": strike,
"type": "STANDARD",
"openinterest": 100 + index * 7,
"contracthigh": premium + 0.5,
"contractlow": max(0.01, premium - 0.5),
},
"pricedata": {
"last": premium,
"bid": round(premium - 0.02, 2),
"ask": round(premium + 0.02, 2),
"bidsize": 10,
"asksize": 12,
"contractvolume": 5 + index,
"prevclose": premium,
"change": 0.01,
"changepercent": 0.5,
"open": premium,
"high": premium + 0.1,
"low": premium - 0.1,
"tick": 1,
},
"greeks": {
"impvol": round(0.25 + distance, 4) if priced else 0.0,
"delta": round(
0.5 - distance if side == "Call" else -0.5 + distance,
4,
),
"gamma": 0.02,
"theta": -0.01,
"vega": 0.03,
"rho": 0.001,
},
"key": {"symbol": [f"@AC{contract}:CA"]},
}
GREEK_FIELDS = (
"implied_volatility",
"delta",
"gamma",
"theta",
"vega",
"rho",
)
def build_options_chain(
priced: bool = True, greeks: bool = True, expired: bool = False
):
"""Validate a TMX options chain from a synthetic vendor payload.
Parameters
----------
priced : bool
When False, every contract carries a zero implied volatility.
greeks : bool
When False, the chain is published without any greek columns, the way
the end-of-day download is.
expired : bool
When True, the chain also carries a contract that has already expired,
which the chain lists but leaves out of its frame.
Returns
-------
TmxOptionsChainsData
The validated chain.
"""
from datetime import date, timedelta
from openbb_tmx.models.options_chains import TmxOptionsChainsData, _flatten_chain
today = date.today()
index = 0
groups: list = []
tenors = (-7, *CHAIN_TENORS) if expired else CHAIN_TENORS
for tenor in tenors:
expiry = (today + timedelta(days=tenor)).isoformat()
quotes: list = []
for strike in CHAIN_STRIKES:
for side in ("Call", "Put"):
quotes.append(_contract_quote(expiry, strike, side, index, priced))
index += 1
groups.append({"expirydate": expiry, "callputgroup": [{"quote": quotes}]})
flat = _flatten_chain({"expiryGroup": groups}, "AC")
flat["underlying_price"] = [UNDERLYING_PRICE] * len(flat["contract_symbol"])
if not greeks:
for field in GREEK_FIELDS:
flat.pop(field, None)
return TmxOptionsChainsData.model_validate(flat)
@pytest.fixture
def options_chain():
"""Serve a validated TMX options chain."""
return build_options_chain()