Compare commits

...

10 Commits

Author SHA1 Message Date
Giulio De Pasquale
b060d988e7 more mbleh 2025-10-15 19:03:36 +01:00
Giulio De Pasquale
a06dcab7f9 mbleh 2025-10-15 19:03:28 +01:00
Giulio De Pasquale
9fa0e61cbb feat: indicators + comments 2025-10-15 19:02:05 +01:00
Giulio De Pasquale
8fe934b971 feat(data): add data models for ticker and trading features 2025-10-15 18:54:54 +01:00
Giulio De Pasquale
3ee3372203 feat(trading): add trading day utilities 2025-10-15 18:51:05 +01:00
Giulio De Pasquale
fed83c07f9 movedul` 2025-10-15 17:27:08 +01:00
Giulio De Pasquale
22c43b51c7 feat(taapi): add get_stocks method to retrieve supported stocks 2025-10-15 17:24:14 +01:00
Giulio De Pasquale
e772fed6d8 dio 2025-10-15 10:48:08 +01:00
Giulio De Pasquale
0410f3aa42 reorg 2025-10-12 18:05:31 +01:00
Giulio De Pasquale
6452802124 prettify 2025-10-12 14:33:37 +01:00
10 changed files with 1987 additions and 253 deletions

200
.gitignore vendored
View File

@ -9,3 +9,203 @@ devenv.local.nix
.pre-commit-config.yaml
.env
# Created by https://www.toptal.com/developers/gitignore/api/python,go
# Edit at https://www.toptal.com/developers/gitignore?templates=python,go
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python,go
.aider*

View File

@ -1,4 +1,4 @@
{ pkgs, lib, config, inputs, ... }:
{ pkgs, ... }:
{
env.GREET = "devenv";

View File

@ -1,271 +1,36 @@
#!/usr/bin/env python
import requests
import math
from sys import exit
from datetime import datetime
from dotenv import load_dotenv
from typing import NoReturn, List
from typing import NoReturn
from paperone.utils import (
parse_date_yyyymmdd,
is_trading_day,
get_last_n_trading_days,
)
from os import environ
from enum import Enum
from dataclasses import dataclass
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from paperone.client import Client
from rich.progress import track
load_dotenv()
@dataclass(frozen=True)
class Indicator:
endpoint: str
params: dict[str, int]
@dataclass
class QueryResult:
datetime: datetime
value: float
class IndicatorEnum(Enum):
# Momentum Indicators
RSI = Indicator(endpoint="rsi", params={"period": 20})
STOCH = Indicator(endpoint="stoch", params={"fast_k": 14, "slow_k": 3, "slow_d": 3})
CCI = Indicator(endpoint="cci", params={"period": 20})
# Trend Indicators
MACD = Indicator(
endpoint="macd",
params={"fast_period": 12, "slow_period": 26, "signal_period": 9},
)
EMA_20 = Indicator(endpoint="ema", params={"period": 20})
EMA_50 = Indicator(endpoint="ema", params={"period": 50})
SMA_200 = Indicator(endpoint="sma", params={"period": 200})
ADX = Indicator(endpoint="adx", params={"period": 14})
# Volatility Indicators
BBANDS = Indicator(endpoint="bbands", params={"period": 20, "stddev": 2})
ATR = Indicator(endpoint="atr", params={"period": 14})
# Volume Indicators
OBV = Indicator(endpoint="obv", params={})
VOLUME = Indicator(endpoint="volume", params={})
class Interval(Enum):
OneMinute = 60
FiveMinutes = 300
FifteenMinutes = 900
ThirtyMinutes = 1800
OneHour = 3600
TwoHours = 7200
FourHours = 14400
TwelveHours = 43200
OneDay = 86400
OneWeek = 604800
class TaapiClient:
def __init__(self, api_key: str) -> None:
self._api_key: str = api_key
self._base_url: str = "https://api.taapi.io"
self._session: requests.Session = self._create_session_with_retries()
def __build_indicator_url__(self, indicator: Indicator) -> str:
return f"{self._base_url}/{indicator.endpoint}"
@staticmethod
def _create_session_with_retries() -> requests.Session:
session: requests.Session = requests.Session()
retry_strategy: Retry = Retry(
total=5, # Maximum 5 retry attempts
backoff_factor=1, # Exponential backoff: 1s, 2s, 4s, 8s, 16s
status_forcelist=[429, 500, 502, 503, 504], # Retry on these HTTP codes
allowed_methods=["GET"], # Only retry GET requests
raise_on_status=False, # Don't raise exceptions, return response
)
adapter: HTTPAdapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
def _do_get(self, url, params) -> requests.Response:
timeout = 5
return self._session.get(url, params=params, timeout=timeout)
def query_indicator(
self,
ticker: str,
indicator: Indicator,
target_date: datetime,
interval: str = "1d",
results: int = 14,
) -> List[QueryResult] | None:
ret: List[QueryResult] = []
backtrack_candles: int = self.__candles_to_target_date__(target_date, interval)
target_url: str = self.__build_indicator_url__(indicator)
params: dict[str, str | int | bool] = {
"secret": self._api_key,
"symbol": ticker,
"interval": interval,
"type": "stocks",
"gaps": "false",
"addResultTimestamp": "true",
"backtrack": backtrack_candles,
"results": str(results),
}
if indicator.params:
params = params | indicator.params
response = self._do_get(target_url, params)
if response.status_code != 200:
return None
data: dict[str, list[float] | list[int]] = response.json()
for val, ts in zip(data["value"], data["timestamp"]):
dt: datetime = datetime.fromtimestamp(ts)
ret.append(QueryResult(dt, val))
return ret
def query_price_on_day(
self,
ticker: str,
target_date: datetime,
) -> QueryResult | None:
backtrack_candles: int = self.__candles_to_target_date__(target_date, "1d")
target_url: str = f"{self._base_url}/price"
params: dict[str, str | int | bool] = {
"secret": self._api_key,
"symbol": ticker,
"interval": "1d",
"type": "stocks",
"gaps": "false",
"addResultTimestamp": "true",
"backtrack": backtrack_candles,
"results": "1",
}
response = self._do_get(target_url, params)
if response.status_code != 200:
return None
data = response.json()
dt: datetime = (
datetime.fromtimestamp(data["timestamp"][0])
if "timestamp" in data
else target_date
)
return QueryResult(dt, data["value"][0])
@staticmethod
def __candles_to_target_date__(
target_date: datetime,
interval: str = "1h",
current_time: datetime | None = None,
) -> int:
if current_time is None:
current_time = datetime.now()
# Calculate time difference
time_diff: datetime = current_time - target_date
time_diff_seconds: float = time_diff.total_seconds()
# Parse interval to get candle duration in seconds
interval_map: dict[str, int] = {
"1m": 60,
"5m": 300,
"15m": 900,
"30m": 1800,
"1h": 3600,
"2h": 7200,
"4h": 14400,
"12h": 43200,
"1d": 86400,
"1w": 604800,
}
candle_duration_seconds: int = interval_map[interval]
# Calculate number of candles (round up)
num_candles: int = math.ceil(time_diff_seconds / candle_duration_seconds)
return num_candles
def close(self) -> None:
self._session.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.close()
def is_trading_day(date: datetime) -> bool:
return date.weekday() not in [5, 6]
def parse_date_yyyymmdd(date_str: str) -> datetime:
return datetime.strptime(date_str, "%Y%m%d")
def format_date_readable(date: datetime) -> str:
return date.strftime("%B %d, %Y")
def main() -> NoReturn:
api_key = environ.get("API_KEY")
if not api_key:
print("API_KEY not set")
exit(0)
client = Client(api_key)
date = parse_date_yyyymmdd("20250821")
days_range = 60
dates_range = get_last_n_trading_days(date, days_range)
# tickers = ["VIX"]
# indicators = list(IndicatorEnum)
with TaapiClient(api_key) as client:
# for t in ["AAPL", "NVDA", "AMD", "META", "MSFT", "GOOG"]:
for t in ["AAPL"]:
print(f"TICKER: {t}\n")
for i in IndicatorEnum:
try:
indicator_results = client.query_indicator(t, i.value, date)
except Exception as e:
# print(f"Could not retrieve data: {e}")
continue
if not indicator_results:
# print("Could not retrieve data")
continue
print(f"Indicator: {i}")
trading_day_values = [
x for x in indicator_results if is_trading_day(x.datetime)
]
for r in trading_day_values:
price = client.query_price_on_day(t, r.datetime)
print(
f"{format_date_readable(r.datetime)} (${price.value:.2f}) - {i.name}: {r.value:.2f}"
)
print("---------------")
for x in track([x for x in dates_range if is_trading_day(x)]):
print(client.ticker_data_for("AAPL", x))
exit(0)

3
paperone/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from beartype.claw import beartype_this_package
beartype_this_package()

102
paperone/client.py Normal file
View File

@ -0,0 +1,102 @@
from datetime import datetime, timedelta
from .taapi import TaapiClient
from typing import List, Dict
import yfinance as yf
import pandas as pd
from dataclasses import dataclass
@dataclass
class TickerData:
name: str
date: datetime
open: float
close: float
low: float
high: float
avg: float
volume: int
@dataclass
class TimeSeriesFeatures:
"""Holds time-series data for a ticker with multiple lookback windows"""
ticker: str
target_date: datetime
current_day: TickerData
vix_current: TickerData
past_30d: List[TickerData] # Previous 30 trading days
class Client:
def __init__(self, taapi_key: str):
self._taapi = TaapiClient(taapi_key)
@staticmethod
def ticker_data_for(ticker: str, date: datetime) -> TickerData | None:
# Set end date to next day to ensure we get the target date
start_date = date.strftime("%Y-%m-%d")
end_date = (date + timedelta(days=1)).strftime("%Y-%m-%d")
try:
data = yf.download(
ticker,
start=start_date,
end=end_date,
auto_adjust=True,
progress=False,
)
if data.empty:
return None
row = data.iloc[0]
open_price = (
float(row["Open"].iloc[0])
if isinstance(row["Open"], pd.Series)
else float(row["Open"])
)
high = (
float(row["High"].iloc[0])
if isinstance(row["High"], pd.Series)
else float(row["High"])
)
low = (
float(row["Low"].iloc[0])
if isinstance(row["Low"], pd.Series)
else float(row["Low"])
)
close = (
float(row["Close"].iloc[0])
if isinstance(row["Close"], pd.Series)
else float(row["Close"])
)
volume = (
int(row["Volume"].iloc[0])
if isinstance(row["Volume"], pd.Series)
else int(row["Volume"])
if "Volume" in row
else 0
)
# Calculate average price
avg = (high + low) / 2.0
return TickerData(
name=ticker,
date=date,
open=round(open_price, 2),
high=round(high, 2),
low=round(low, 2),
close=round(close, 2),
avg=round(avg, 2),
volume=volume,
)
except Exception as e:
print(f"Error fetching data for {ticker} on {start_date}: {str(e)}")
return None

275
paperone/data.py Normal file
View File

@ -0,0 +1,275 @@
from dataclasses import dataclass
from datetime import datetime
from typing import List
@dataclass
class TickerData:
date: datetime
open: float
close: float
low: float
high: float
avg: float
volume: float
@dataclass
class TimeSeriesTickerData:
ticker: str
target_date: datetime
current_day_data: TickerData
past_30d_data: List[TickerData]
@dataclass
class TradeFeatures:
"""
Comprehensive feature set for ML-based trading models.
This class combines raw price data, engineered time-series features,
and technical indicators across multiple categories to provide a
complete market picture for prediction models.
Feature Categories:
- Raw OHLCV data (current day)
- Lagged features (5-day lookback)
- Rolling window statistics (5d, 10d, 30d)
- VIX volatility index features
- Momentum indicators (trend direction and strength)
- Volatility indicators (price dispersion and risk)
- Trend indicators (trend presence and sustainability)
- Volume indicators (institutional participation)
- Support/Resistance levels (key price zones)
- Market regime indicators (market condition classification)
"""
ticker: str
target_date: datetime
# ========================================================================
# CURRENT DAY FEATURES (Raw OHLCV Data)
# ========================================================================
# Basic price and volume data for the target trading day.
# Research shows raw price data often outperforms technical indicators
# in feature importance for ML models.
current_open: float # Opening price of the trading day
current_high: float # Highest price reached during the day
current_low: float # Lowest price reached during the day
current_close: float # Closing price of the trading day
current_volume: float # Total shares/contracts traded during the day
# ========================================================================
# LAGGED PRICE FEATURES (Last 5 Trading Days)
# ========================================================================
# Historical closing prices from the previous 5 trading days.
# Captures short-term price memory and recent momentum patterns.
# Lag-1 (previous day) typically has highest predictive power.
close_lag_1: float # Closing price 1 trading day ago (t-1)
close_lag_2: float # Closing price 2 trading days ago (t-2)
close_lag_3: float # Closing price 3 trading days ago (t-3)
close_lag_4: float # Closing price 4 trading days ago (t-4)
close_lag_5: float # Closing price 5 trading days ago (t-5)
# ========================================================================
# LAGGED VOLUME FEATURES (Last 5 Trading Days)
# ========================================================================
# Historical volume from the previous 5 trading days.
# Volume patterns often precede price movements and indicate
# institutional participation or distribution.
volume_lag_1: float # Volume 1 trading day ago (t-1)
volume_lag_2: float # Volume 2 trading days ago (t-2)
volume_lag_3: float # Volume 3 trading days ago (t-3)
volume_lag_4: float # Volume 4 trading days ago (t-4)
volume_lag_5: float # Volume 5 trading days ago (t-5)
# ========================================================================
# 5-DAY ROLLING WINDOW FEATURES (Short-term trend)
# ========================================================================
# Statistical aggregates over the last 5 trading days (1 week).
# Captures short-term momentum, volatility, and recent price action.
rolling_5d_mean: float # Average closing price over 5 days
rolling_5d_std: float # Standard deviation (volatility measure)
rolling_5d_min: float # Minimum closing price in window
rolling_5d_max: float # Maximum closing price in window
rolling_5d_range: float # Price range (max high - min low)
rolling_5d_volume_mean: float # Average volume over 5 days
rolling_5d_returns: float # Total return over 5-day period
# ========================================================================
# 10-DAY ROLLING WINDOW FEATURES (Medium-term trend)
# ========================================================================
# Statistical aggregates over the last 10 trading days (2 weeks).
# Captures medium-term trends and smooths out short-term noise.
rolling_10d_mean: float # Average closing price over 10 days
rolling_10d_std: float # Standard deviation (volatility measure)
rolling_10d_min: float # Minimum closing price in window
rolling_10d_max: float # Maximum closing price in window
rolling_10d_range: float # Price range (max high - min low)
rolling_10d_volume_mean: float # Average volume over 10 days
rolling_10d_returns: float # Total return over 10-day period
# ========================================================================
# 30-DAY ROLLING WINDOW FEATURES (Long-term trend)
# ========================================================================
# Statistical aggregates over the last 30 trading days (~1 month).
# Captures longer-term trends and establishes baseline behavior.
rolling_30d_mean: float # Average closing price over 30 days
rolling_30d_std: float # Standard deviation (volatility measure)
rolling_30d_min: float # Minimum closing price in window
rolling_30d_max: float # Maximum closing price in window
rolling_30d_range: float # Price range (max high - min low)
rolling_30d_volume_mean: float # Average volume over 30 days
rolling_30d_returns: float # Total return over 30-day period
# ========================================================================
# VIX FEATURES (Market-wide volatility and fear gauge)
# ========================================================================
# CBOE Volatility Index (VIX) features. VIX measures market expectation
# of 30-day volatility from S&P 500 options. Often called the "fear index".
# High VIX (>30) indicates fear/uncertainty, low VIX (<15) indicates complacency.
# VIX often inversely correlates with market returns.
vix_current: float # Current VIX level
vix_lag_1: float # VIX level 1 day ago (recent change)
vix_lag_5: float # VIX level 5 days ago (weekly change)
vix_rolling_5d_mean: float # Average VIX over last 5 days (short-term fear)
vix_rolling_10d_mean: float # Average VIX over last 10 days (medium-term fear)
vix_rolling_30d_mean: float # Average VIX over last 30 days (baseline volatility)
vix_rolling_30d_std: float # VIX volatility (volatility of volatility)
# ========================================================================
# MOMENTUM INDICATORS (Trend Direction & Strength)
# ========================================================================
# Indicators that measure the rate of price change and identify
# overbought/oversold conditions. Essential for trend-following strategies.
# RSI (Relative Strength Index)
# Measures momentum on a 0-100 scale. Above 70 = overbought, below 30 = oversold.
# 14-period is standard, 20-period provides smoother, longer-term signal.
rsi_14: float # Standard 14-period RSI
rsi_20: float # Longer 20-period RSI for smoother signal
# MACD (Moving Average Convergence Divergence)
# Trend-following momentum indicator showing relationship between two EMAs.
# Particularly effective with GRU/LSTM neural networks for stock prediction.
# Crossovers and divergences signal potential trend changes.
macd_line: float # MACD line (12 EMA - 26 EMA)
macd_signal: float # Signal line (9-period EMA of MACD)
macd_histogram: float # Histogram (MACD - Signal), shows momentum strength
# Stochastic Oscillator
# Compares closing price to price range over period. 0-100 scale.
# Above 80 = overbought, below 20 = oversold. Captures short-term extremes.
# %K is fast line (14-period), %D is slow line (3-period SMA of %K).
stoch_k: float # Fast stochastic %K (14-period)
stoch_d: float # Slow stochastic %D (3-period SMA of %K)
# ========================================================================
# VOLATILITY INDICATORS (Price Dispersion & Risk)
# ========================================================================
# Indicators that measure how much price fluctuates. Critical for
# risk management and identifying potential breakout/breakdown scenarios.
# Bollinger Bands
# Volatility bands plotted at standard deviations from moving average.
# Performs exceptionally well with LSTM networks by reducing noise.
# Bands expand during high volatility, contract during low volatility.
# Price at upper band = strong uptrend, at lower band = strong downtrend.
bb_upper: float # Upper Bollinger Band (SMA + 2*std)
bb_middle: float # Middle band (20-period SMA)
bb_lower: float # Lower Bollinger Band (SMA - 2*std)
bb_width: float # Band width (upper - lower), measures volatility magnitude
bb_percent: (
float # %B indicator: (close - lower) / (upper - lower), position in bands
)
# ATR (Average True Range)
# Measures absolute volatility independent of price direction.
# Higher ATR = higher volatility, useful for stop-loss placement.
# 14-period is industry standard.
atr_14: float # 14-period Average True Range
# ========================================================================
# TREND INDICATORS (Trend Presence & Sustainability)
# ========================================================================
# Unlike momentum indicators, these measure whether a trend EXISTS
# and how strong it is, not just the direction.
# ADX (Average Directional Index)
# Measures trend strength on 0-100 scale, regardless of direction.
# ADX > 25 = strong trend worth trading, ADX < 20 = weak/no trend.
# +DI and -DI show bullish vs bearish pressure.
adx_14: float # 14-period ADX (trend strength)
di_plus: float # +DI (bullish directional indicator)
di_minus: float # -DI (bearish directional indicator)
# Parabolic SAR (Stop and Reverse)
# Provides dynamic support/resistance levels and trailing stop points.
# SAR below price = uptrend (long), SAR above price = downtrend (short).
# Dots "flip" when trend reverses.
sar: float # Current Parabolic SAR level
# ========================================================================
# VOLUME INDICATORS (Institutional Participation)
# ========================================================================
# Volume precedes price. These indicators track smart money flow
# and institutional accumulation/distribution patterns.
# OBV (On-Balance Volume)
# Cumulative volume flow indicator. Rising OBV = accumulation (bullish),
# falling OBV = distribution (bearish). OBV divergences often predict reversals.
# 60-70% of volatility contraction pattern breakouts succeed with strong volume.
obv: float # On-Balance Volume cumulative total
obv_sma_20: float # 20-day SMA of OBV (trend confirmation)
# Volume Rate of Change
# Measures percentage change in volume. Spikes indicate increased interest.
# High positive values confirm price moves, negative values suggest weakness.
volume_roc_5: float # 5-day volume rate of change (%)
# ========================================================================
# SUPPORT/RESISTANCE INDICATORS (Key Price Levels)
# ========================================================================
# Identify potential price floors (support) and ceilings (resistance)
# where price may reverse or consolidate.
# Fibonacci Retracement Levels
# Based on Fibonacci ratios, commonly used to identify retracement targets.
# Performed well in ML models for price movement prediction.
# 23.6% = shallow retracement, 38.2% = moderate, 61.8% = deep (golden ratio)
fib_236: float # 23.6% Fibonacci retracement level
fib_382: float # 38.2% Fibonacci retracement level
fib_618: float # 61.8% Fibonacci retracement level (golden ratio)
# Pivot Points
# Classic support/resistance levels calculated from previous day's OHLC.
# Widely used by floor traders and algorithmic systems.
# Price above pivot = bullish bias, below = bearish bias.
pivot_point: float # Standard pivot point (High + Low + Close) / 3
resistance_1: float # First resistance level (R1)
support_1: float # First support level (S1)
# ========================================================================
# MARKET REGIME INDICATORS (Market Condition Classification)
# ========================================================================
# Help identify what type of market environment we're in
# (trending, ranging, volatile, calm, etc.)
# CCI (Commodity Channel Index)
# Identifies cyclical trends and extreme market conditions.
# Above +100 = overbought/strong uptrend, below -100 = oversold/strong downtrend.
# Particularly good at capturing short-term price movements.
cci_20: float # 20-period Commodity Channel Index
# Williams %R
# Momentum oscillator on -100 to 0 scale (inverted from Stochastic).
# Above -20 = overbought, below -80 = oversold.
# Complements RSI with different sensitivity and faster signals.
williams_r_14: float # 14-period Williams %R

219
paperone/taapi.py Normal file
View File

@ -0,0 +1,219 @@
import requests
import math
from dataclasses import dataclass
from typing import List
from enum import Enum
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from datetime import datetime, timedelta
@dataclass(frozen=True)
class Indicator:
endpoint: str
params: dict[str, int]
@dataclass
class QueryResult:
datetime: datetime
value: float
class IndicatorEnum(Enum):
# Momentum Indicators
RSI = Indicator(endpoint="rsi", params={"period": 20})
STOCH = Indicator(endpoint="stoch", params={"fast_k": 14, "slow_k": 3, "slow_d": 3})
CCI = Indicator(endpoint="cci", params={"period": 20})
# Trend Indicators
MACD = Indicator(
endpoint="macd",
params={"fast_period": 12, "slow_period": 26, "signal_period": 9},
)
EMA_20 = Indicator(endpoint="ema", params={"period": 20})
EMA_50 = Indicator(endpoint="ema", params={"period": 50})
SMA_200 = Indicator(endpoint="sma", params={"period": 200})
ADX = Indicator(endpoint="adx", params={"period": 14})
# Volatility Indicators
BBANDS = Indicator(endpoint="bbands", params={"period": 20, "stddev": 2})
ATR = Indicator(endpoint="atr", params={"period": 14})
# Volume Indicators
OBV = Indicator(endpoint="obv", params={})
VOLUME = Indicator(endpoint="volume", params={})
class TaapiClient:
def __init__(self, api_key: str) -> None:
self._api_key: str = api_key
self._base_url: str = "https://api.taapi.io"
self._session: requests.Session = self._create_session_with_retries()
def __build_indicator_url__(self, indicator: Indicator) -> str:
return f"{self._base_url}/{indicator.endpoint}"
@staticmethod
def _create_session_with_retries() -> requests.Session:
session: requests.Session = requests.Session()
retry_strategy: Retry = Retry(
total=5, # Maximum 5 retry attempts
backoff_factor=1, # Exponential backoff: 1s, 2s, 4s, 8s, 16s
status_forcelist=[429, 500, 502, 503, 504], # Retry on these HTTP codes
allowed_methods=["GET"], # Only retry GET requests
raise_on_status=False, # Don't raise exceptions, return response
)
adapter: HTTPAdapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
def _do_get(self, url: str, params: dict) -> requests.Response:
timeout = 5
return self._session.get(url, params=params, timeout=timeout)
def get_available_tickers(self) -> list[str] | None:
"""
Retrieves a list of supported stocks from the TAAPI API.
"""
target_url = f"{self._base_url}/exchange-symbols"
params: dict[str, str | int | bool] = {
"secret": self._api_key,
"type": "stocks",
}
response = self._do_get(target_url, params)
if response.status_code != 200:
return None
return list(response.json())
def query_indicator(
self,
ticker: str,
indicator: Indicator,
target_date: datetime,
interval: str = "1d",
results: int = 14,
) -> List[QueryResult] | None:
ret: List[QueryResult] = []
backtrack_candles: int = self.__candles_to_target_date__(target_date, interval)
target_url: str = self.__build_indicator_url__(indicator)
params: dict[str, str | int | bool] = {
"secret": self._api_key,
"symbol": ticker,
"interval": interval,
"type": "stocks",
"gaps": "false",
"addResultTimestamp": "true",
"backtrack": backtrack_candles,
"results": str(results),
}
if indicator.params:
params = params | indicator.params
response = self._do_get(target_url, params)
if response.status_code != 200:
return None
data: dict[str, list[float] | list[int]] = response.json()
for val, ts in zip(data["value"], data["timestamp"]):
dt: datetime = datetime.fromtimestamp(ts)
ret.append(QueryResult(dt, float(val)))
return ret
def query_price_on_day(
self,
ticker: str,
target_date: datetime,
) -> QueryResult | None:
backtrack_candles: int = self.__candles_to_target_date__(target_date, "1d")
target_url: str = f"{self._base_url}/price"
params: dict[str, str | int | bool] = {
"secret": self._api_key,
"symbol": ticker,
"interval": "1d",
"type": "stocks",
"gaps": "false",
"addResultTimestamp": "true",
"backtrack": backtrack_candles,
"results": "1",
}
response = self._do_get(target_url, params)
if response.status_code != 200:
return None
data = response.json()
dt: datetime = (
datetime.fromtimestamp(data["timestamp"][0])
if "timestamp" in data
else target_date
)
if "value" not in data:
raise Exception("Invalid value")
if len(data["value"]) != 1:
raise Exception("Multiple values returned")
return QueryResult(dt, float(data["value"][0]))
@staticmethod
def __candles_to_target_date__(
target_date: datetime,
interval: str = "1h",
current_time: datetime | None = None,
) -> int:
if current_time is None:
current_time = datetime.now()
# Calculate time difference
time_diff: timedelta = current_time - target_date
time_diff_seconds: float = time_diff.total_seconds()
# Parse interval to get candle duration in seconds
interval_map: dict[str, int] = {
"1m": 60,
"5m": 300,
"15m": 900,
"30m": 1800,
"1h": 3600,
"2h": 7200,
"4h": 14400,
"12h": 43200,
"1d": 86400,
"1w": 604800,
}
candle_duration_seconds: int = interval_map[interval]
# Calculate number of candles (round up)
num_candles: int = math.ceil(time_diff_seconds / candle_duration_seconds)
return num_candles
def close(self) -> None:
self._session.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.close()

72
paperone/utils.py Normal file
View File

@ -0,0 +1,72 @@
from datetime import datetime, timedelta
from typing import List
import pandas_market_calendars as mcal
NYSE = mcal.get_calendar("NYSE")
def is_trading_day(date: datetime) -> bool:
valid_days = NYSE.valid_days(
start_date=date.strftime("%Y-%m-%d"), end_date=date.strftime("%Y-%m-%d")
)
return len(valid_days) > 0
def get_trading_days(start_date: datetime, end_date: datetime) -> List[datetime]:
valid_days = NYSE.valid_days(
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d"),
)
return [day.to_pydatetime().replace(tzinfo=None) for day in valid_days]
def get_last_n_trading_days(date: datetime, n: int) -> List[datetime]:
"""
Get the last N trading days before (and including) the given date.
"""
# Add buffer for weekends/holidays (n trading days ≈ n * 1.5 calendar days)
buffer_days = int(n * 2)
start_date = date - timedelta(days=buffer_days)
# Get all trading days in the range
trading_days = get_trading_days(start_date, date)
# Return the last n trading days
return trading_days[-n:] if len(trading_days) >= n else trading_days
def get_next_trading_day(date: datetime) -> datetime:
next_day = date + timedelta(days=1)
# Search up to 10 days ahead (handles long holiday weekends)
for i in range(10):
check_date = next_day + timedelta(days=i)
if is_trading_day(check_date):
return check_date
raise ValueError(f"No trading day found within 10 days of {date}")
def get_previous_trading_day(date: datetime) -> datetime:
prev_day = date - timedelta(days=1)
# Search up to 10 days back
for i in range(10):
check_date = prev_day - timedelta(days=i)
if is_trading_day(check_date):
return check_date
raise ValueError(f"No trading day found within 10 days before {date}")
def parse_date_yyyymmdd(date_str: str) -> datetime:
"""Parse date string in YYYYMMDD format to datetime."""
return datetime.strptime(date_str, "%Y%m%d")
def format_date_readable(date: datetime) -> str:
"""Format datetime to readable string (e.g., 'October 15, 2025')."""
return date.strftime("%B %d, %Y")

1096
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,11 @@ requires-python = ">=3.13"
dependencies = [
"requests (>=2.32.5,<3.0.0)",
"python-dotenv (>=1.1.1,<2.0.0)",
"beartype (>=0.22.2,<0.23.0)"
"beartype (>=0.22.2,<0.23.0)",
"typer (>=0.19.2,<0.20.0)",
"yfinance (>=0.2.66,<0.3.0)",
"ipython (>=9.6.0,<10.0.0)",
"pandas-market-calendars (>=5.1.1,<6.0.0)"
]