Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

udated test and option chain download #4

Merged
merged 1 commit into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ schwab_client.get_tokens_manually()
from_date = 2024-07-01
to_date = 2024-07-01
ticker = '$SPX'
asyncio.run(schwab_client.download_option_chain(ticker, from_date, to_date))
asyncio.run(opt_chain_result = schwab_client.download_option_chain(ticker, from_date, to_date))

# get call-put dataframe pairs by expiration
opt_df_pairs = opt_chain_result.to_dataframe_pairs_by_expiration()

for df in opt_df_pairs:
print(df.expiration)
print(f"call dataframe size: {df.call_df.shape}. expiration: {df.expiration}")
print(f"put dataframe size: {df.put_df.shape}. expiration: {df.expiration}")
print(df.call_df.head(5))
print(df.put_df.head(5))

```
10 changes: 6 additions & 4 deletions cschwabpy/SchwabAsyncClient.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from cschwabpy.models.token import Tokens, ITokenStore, LocalTokenStore
from cschwabpy.models import OptionChainQueryFilter, OptionContractType
from cschwabpy.models import OptionChainQueryFilter, OptionContractType, OptionChain
from typing import Optional, List, Mapping
from cschwabpy.costants import (
SCHWAB_API_BASE_URL,
Expand Down Expand Up @@ -100,7 +100,9 @@ async def download_option_chain(
from_date: str,
to_date: str,
contract_type: str = "ALL",
) -> None:
) -> OptionChain:
await self._ensure_valid_access_token()

query_filter = OptionChainQueryFilter(
symbol=underlying_symbol,
contractType=OptionContractType(contract_type),
Expand All @@ -110,14 +112,14 @@ async def download_option_chain(
target_url = (
f"{SCHWAB_MARKET_DATA_API_BASE_URL}/chains?{query_filter.to_query_params()}"
)
print("target_url: ", target_url)
print("auth header: ", self.__auth_header())

client = httpx.AsyncClient() if self.__client is None else self.__client
try:
response = await client.get(
url=target_url, params={}, headers=self.__auth_header()
)
json_res = response.json()
return OptionChain(**json_res)
finally:
if not self.__keep_client_alive:
await client.aclose()
Expand Down
100 changes: 99 additions & 1 deletion cschwabpy/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
"""models folder."""
from datetime import datetime
from dataclasses import dataclass
from pydantic import BaseModel, ConfigDict, Field
from typing import MutableMapping, Mapping, MutableSet, Any, List, Optional
from enum import Enum
import cschwabpy.util as util
import pandas as pd

OptionChain_Headers = [
"underlying_price",
"strike",
"symbol",
"last_price",
"open_interest",
"ask",
"bid",
"expiration_date",
"bid_date",
"volume",
"updated_at",
"gamma",
"delta",
"vega",
"volatility",
]


class JSONSerializableBaseModel(BaseModel):
model_config = ConfigDict(use_enum_values=True, populate_by_name=True)
Expand Down Expand Up @@ -128,14 +149,33 @@ class OptionContract(JSONSerializableBaseModel):
lastTradingDay: Optional[int] = None
multiplier: Optional[float] = None
settlementType: str # AM, PM
isIndex: bool
isIndex: Optional[bool] = None
percentChange: Optional[float] = None
markChange: Optional[float] = None
markPercentChange: Optional[float] = None
model_config = ConfigDict(
validate_assignment=False, use_enum_values=True, populate_by_name=True
)

def to_dataframe_row(self) -> List[Any]:
result: List[Any] = [
self.strikePrice,
self.symbol.strip().replace(" ", ""),
self.lastPrice,
self.openInterest,
self.askPrice,
self.bidPrice,
self.expirationDate,
util.ts_to_date_string(self.quoteTimeInLong),
self.totalVolume,
util.ts_to_date_string(self.quoteTimeInLong),
self.gamma,
self.delta,
self.vega,
self.volatility,
]
return result


class Underlying(JSONSerializableBaseModel):
ask: float
Expand All @@ -158,6 +198,18 @@ class Underlying(JSONSerializableBaseModel):
totalVolume: Optional[int] = None
tradeTime: Optional[int] = None

@property
def quote_time(self) -> datetime:
return util.ts_to_datetime(self.quoteTime)


@dataclass
class OptionChainDataFrames:
expiration: str
underlying_symbol: str
call_df: pd.DataFrame
put_df: pd.DataFrame


class OptionChain(JSONSerializableBaseModel):
symbol: str
Expand All @@ -178,3 +230,49 @@ class OptionChain(JSONSerializableBaseModel):
callExpDateMap: Mapping[
str, Mapping[str, List[OptionContract]]
] # key: expiration:27 value:[strike: OptionContract]

def to_dataframe_pairs_by_expiration(self) -> List[OptionChainDataFrames]:
"""
List of OptionChainDataFrames by expiration.
Each OptionChainDataFrames object contains call and put chain in dataframe format.
"""
results: List[OptionChainDataFrames] = []
call_map = self.break_down_option_map(self.callExpDateMap)
put_map = self.break_down_option_map(self.putExpDateMap)
for expiration in call_map.keys():
call_df = call_map[expiration]
put_df = put_map[expiration]

cur_df_pair = OptionChainDataFrames(
expiration=expiration,
underlying_symbol=self.symbol,
call_df=call_df,
put_df=put_df,
)
results.append(cur_df_pair)

return results

def break_down_option_map(
self, optionExpMap: Mapping[str, Mapping[str, List[OptionContract]]]
) -> Mapping[str, pd.DataFrame]:
result: MutableMapping[str, pd.DataFrame] = {}
for exp_date, strike_map in optionExpMap.items():
expiration = exp_date.split(":")[0]
if expiration not in result:
result[expiration] = {}

all_rows = []
strike_df = pd.DataFrame()
for strike_str, option_contracts in strike_map.items():
strike = float(strike_str)
for option_contract in option_contracts:
row = option_contract.to_dataframe_row()
row.insert(0, self.underlying.mark)
all_rows.append(row)

strike_df = pd.DataFrame(all_rows)
strike_df.columns = OptionChain_Headers
result[expiration] = strike_df

return result
15 changes: 14 additions & 1 deletion cschwabpy/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from datetime import datetime
import pytz
from typing import Optional

eastern_tz: pytz.BaseTzInfo = pytz.timezone("US/Eastern")
YMD_FMT = "%Y-%m-%d"
Expand All @@ -17,12 +18,24 @@ def now_unix_ts() -> float:
return now().timestamp()


def ts_to_datetime(ts: float, tz: pytz.BaseTzInfo = eastern_tz) -> datetime:
def ts_to_datetime(
ts: Optional[float] = None, tz: pytz.BaseTzInfo = eastern_tz
) -> Optional[datetime]:
if ts is None:
return None
while ts > 1e10:
ts = ts / 1000
return datetime.fromtimestamp(ts, tz)


def ts_to_date_string(
ts: Optional[float] = None, tz: pytz.BaseTzInfo = eastern_tz
) -> Optional[str]:
if ts is None:
return None
return ts_to_datetime(ts, tz).strftime(YMD_FMT)


def today_str(tz: pytz.BaseTzInfo = eastern_tz) -> str: # type: ignore
"""Today in string. Returns Y-m-d.""" # noqa: DAR201
return now(tz=tz).strftime(YMD_FMT)
13 changes: 8 additions & 5 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ def test_option_chain_parsing() -> None:
assert opt_chain_result is not None
assert opt_chain_result.status == "SUCCESS"

# for key, value in opt_chain_result.callExpDateMap.items():
# # Do something with key and value
# print(key[:10])
# print(value)#
# print("----------------")
opt_df_pairs = opt_chain_result.to_dataframe_pairs_by_expiration()
assert opt_df_pairs is not None
for df in opt_df_pairs:
print(df.expiration)
print(f"call dataframe size: {df.call_df.shape}. expiration: {df.expiration}")
print(f"put dataframe size: {df.put_df.shape}. expiration: {df.expiration}")
print(df.call_df.head(5))
print(df.put_df.head(5))
Loading