Skip to content

Commit

Permalink
Merge pull request #5 from Verdenroz/hours
Browse files Browse the repository at this point in the history
Hours
  • Loading branch information
Verdenroz authored Nov 20, 2024
2 parents a6d05a0 + 14c5c88 commit bd2baa9
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 218 deletions.
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,19 @@ FinanceQuery is a simple API to query financial data. It provides endpoints to g
GET /v1/gainers
```

#### Get market open/closed status

```
GET /hours
```


## Websockets Guide

> **The websockets depend on Redis PubSub and will require Redis credentials in your [.env](https://github.com/Verdenroz/finance-query?tab=readme-ov-file#environment-variables)**
There are currently three implemented websocket routes: `profile`, `quotes`, and `market`. These will not be accessible through Lambda. If you are interested in deployment, I highly deploying to [Render](https://render.com/) as it will be able to host the entire FastAPI server, including the websockets. If you are testing locally, your requests will be `ws` instead of `wss`. Data is returned on a set interval every 10 seconds.
There are currently four implemented websocket routes: `profile`, `quotes`, `market`, and `hours`. These will not be accessible through Lambda. If you are interested in deployment, I recommend deploying to [Render](https://render.com/) as it will be able to host the entire FastAPI server, including the websockets. If you are testing locally, your requests will be `ws` instead of `wss`. Data is returned on a set interval every 10 seconds.

### Quote profile
> #### Combines `quote`, `similar stocks`, `sector for symbol`, `news for symbol`
Expand All @@ -160,7 +168,7 @@ WSS /profile/{symbol}
```

### Watchlist
> #### Requires comma separated list of symbols to be sent intially, streaming simplified quotes for all provided symbols
> #### Requires comma separated list of symbols to be sent initially, streaming simplified quotes for all provided symbols
```
WSS /quotes
Expand All @@ -173,6 +181,12 @@ WSS /quotes
WSS /market
```

### Market status
> #### Streams whether the market is open or closed, sending a message only when the status changes
```
WSS /hours
```

## Usage/Examples

Expand Down
5 changes: 3 additions & 2 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from src.redis import r
from src.routes import (quotes_router, indices_router, movers_router, historical_prices_router,
similar_quotes_router, finance_news_router, indicators_router, search_router,
sectors_router, sockets_router, stream_router)
sectors_router, sockets_router, stream_router, hours_router)
from src.schemas.sector import Sector
from src.schemas.time_series import TimePeriod, Interval
from src.security import RateLimitMiddleware, RateLimitManager
Expand Down Expand Up @@ -51,7 +51,7 @@ async def lifespan(app: FastAPI):

app = FastAPI(
title="FinanceQuery",
version="1.5.2",
version="1.5.3",
description="FinanceQuery is a simple API to query financial data."
" It provides endpoints to get quotes, historical prices, indices,"
" market movers, similar stocks, finance news, indicators, search, and sectors."
Expand Down Expand Up @@ -272,6 +272,7 @@ async def ping(response: Response):
app.include_router(search_router, prefix="/v1")
app.include_router(sectors_router, prefix="/v1")
app.include_router(stream_router, prefix="/v1")
app.include_router(hours_router)
app.include_router(sockets_router)

handler = Mangum(app)
71 changes: 71 additions & 0 deletions src/market.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from datetime import datetime, time, date
from enum import Enum
from typing import Optional

import pytz


class MarketStatus(str, Enum):
OPEN = "Open"
CLOSED = "Closed"
EARLY_CLOSE = "Early Close"


class MarketSchedule:
def __init__(self):
# Regular trading hours (Eastern Time)
self.regular_open = time(9, 30) # 9:30 AM ET
self.regular_close = time(16, 0) # 4:00 PM ET
self.early_close_time = time(13, 0) # 1:00 PM ET

# 2024 Full Holiday Closures
self.full_holidays = {
date(2024, 1, 1): "New Year's Day",
date(2024, 1, 15): "Martin Luther King Jr. Day",
date(2024, 2, 19): "Presidents Day",
date(2024, 3, 29): "Good Friday",
date(2024, 5, 27): "Memorial Day",
date(2024, 6, 19): "Juneteenth",
date(2024, 7, 4): "Independence Day",
date(2024, 9, 2): "Labor Day",
date(2024, 11, 28): "Thanksgiving Day",
date(2024, 12, 25): "Christmas Day",
}

# 2024 Early Closures (1:00 PM ET)
self.early_close_dates = {
date(2024, 7, 3): "July 3rd",
date(2024, 11, 29): "Black Friday",
date(2024, 12, 24): "Christmas Eve",
}

def get_market_status(self) -> tuple[MarketStatus, Optional[str]]:
et_tz = pytz.timezone('America/New_York')
current_et = datetime.now(et_tz)
current_date = current_et.date()
current_time = current_et.time()

# Check if it's a weekend
if current_et.weekday() >= 5: # 5 is Saturday, 6 is Sunday
return MarketStatus.CLOSED, "Weekend"

# Check if it's a holiday
if current_date in self.full_holidays:
return MarketStatus.CLOSED, f"Holiday: {self.full_holidays[current_date]}"

# Check if it's an early closure day
if current_date in self.early_close_dates:
if current_time < self.regular_open:
return MarketStatus.CLOSED, "Pre-market"
elif current_time >= self.early_close_time:
return MarketStatus.CLOSED, f"Early Close: {self.early_close_dates[current_date]}"
else:
return MarketStatus.EARLY_CLOSE, f"Early Close Day: {self.early_close_dates[current_date]}"

# Regular trading day logic
if current_time < self.regular_open:
return MarketStatus.CLOSED, "Pre-market"
elif current_time >= self.regular_close:
return MarketStatus.CLOSED, "After-hours"
else:
return MarketStatus.OPEN, "Regular trading hours"
7 changes: 4 additions & 3 deletions src/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
from pydantic import BaseModel
from redis import asyncio as aioredis, RedisError

from src.market import MarketSchedule, MarketStatus
from src.schemas import TimeSeries, SimpleQuote, Quote, MarketMover, Index, News, MarketSector
from src.schemas.analysis import SMAData, EMAData, WMAData, VWMAData, RSIData, SRSIData, STOCHData, CCIData, MACDData, \
ADXData, AROONData, BBANDSData, OBVData, SuperTrendData, IchimokuData, Analysis, Indicator
from src.schemas.sector import MarketSectorDetails
from src.utils import is_market_open

load_dotenv()

Expand Down Expand Up @@ -51,7 +51,7 @@
}


def cache(expire, market_closed_expire=None):
def cache(expire, market_closed_expire=None, market_schedule=MarketSchedule()):
"""
This decorator caches the result of the function it decorates.
Expand Down Expand Up @@ -178,8 +178,9 @@ async def wrapper(*args, **kwargs):
if result is None:
return None

is_closed = market_schedule.get_market_status()[0] != MarketStatus.OPEN
# Determine expiration time
expire_time = market_closed_expire if (market_closed_expire and not is_market_open()) else expire
expire_time = market_closed_expire if (market_closed_expire and is_closed) else expire

# Cache the result
await cache_result(key, result, expire_time)
Expand Down
1 change: 1 addition & 0 deletions src/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .finance_news import router as finance_news_router
from .historical_prices import router as historical_prices_router
from .hours import router as hours_router
from .indicators import router as indicators_router
from .indices import router as indices_router
from .movers import router as movers_router
Expand Down
25 changes: 25 additions & 0 deletions src/routes/hours.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from datetime import datetime

import pytz
from fastapi import APIRouter, Depends, Security
from fastapi.security import APIKeyHeader

from src.market import MarketSchedule

router = APIRouter()


@router.get("/hours",
summary="Get the current market status",
description="Returns the current status of the market (open, closed, early close).",
response_description="Market status",
tags=["Hours"],
dependencies=[Security(APIKeyHeader(name="x-api-key", auto_error=False))],
)
async def get_market_hours(market_schedule: MarketSchedule = Depends(MarketSchedule)):
status, reason = market_schedule.get_market_status()
return {
"status": status,
"reason": reason,
"timestamp": datetime.now(pytz.UTC).isoformat()
}
Loading

0 comments on commit bd2baa9

Please sign in to comment.