added Adv Historical Module2 Test
This commit is contained in:
@@ -0,0 +1,223 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import logging
|
||||||
|
import configparser
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
import pytz
|
||||||
|
import threading
|
||||||
|
import csv
|
||||||
|
|
||||||
|
from ibapi.client import EClient
|
||||||
|
from ibapi.wrapper import EWrapper
|
||||||
|
from ibapi.contract import Contract
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
# Load config
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
if not os.path.exists('config.ini'):
|
||||||
|
print("config.ini not found. Please create one.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
config.read('config.ini')
|
||||||
|
HOST = config.get('API', 'host', fallback='127.0.0.1')
|
||||||
|
PORT = config.getint('API', 'port', fallback=4002)
|
||||||
|
CLIENT_ID = config.getint('API', 'clientId', fallback=1)
|
||||||
|
DATA_DIR = config.get('Directories', 'data_dir', fallback='./data')
|
||||||
|
TICKER_FILE = config.get('General', 'ticker_file', fallback='ticker_ids.csv')
|
||||||
|
MIN_VOLUME = config.getint('Thresholds', 'min_volume', fallback=1000000)
|
||||||
|
|
||||||
|
if not os.path.exists(DATA_DIR):
|
||||||
|
os.makedirs(DATA_DIR)
|
||||||
|
|
||||||
|
if not os.path.exists(TICKER_FILE):
|
||||||
|
print(f"{TICKER_FILE} not found. Please create one with a list of symbols.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
class IBWrapper(EWrapper):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.current_time = None
|
||||||
|
self.current_time_received = threading.Event()
|
||||||
|
|
||||||
|
self.historical_data_reqId = None
|
||||||
|
self.historical_bars = []
|
||||||
|
self.historical_data_received = threading.Event()
|
||||||
|
|
||||||
|
def error(self, reqId, errorCode, errorString, advancedOrderRejectJson=""):
|
||||||
|
logging.error(f"Error. ReqId: {reqId}, Code: {errorCode}, Msg: {errorString}")
|
||||||
|
|
||||||
|
def currentTime(self, time_):
|
||||||
|
self.current_time = time_
|
||||||
|
self.current_time_received.set()
|
||||||
|
|
||||||
|
def historicalData(self, reqId, bar):
|
||||||
|
if reqId == self.historical_data_reqId:
|
||||||
|
self.historical_bars.append(bar)
|
||||||
|
|
||||||
|
def historicalDataEnd(self, reqId, start, end):
|
||||||
|
if reqId == self.historical_data_reqId:
|
||||||
|
self.historical_data_received.set()
|
||||||
|
|
||||||
|
class IBClient(EClient):
|
||||||
|
def __init__(self, wrapper):
|
||||||
|
EClient.__init__(self, wrapper)
|
||||||
|
|
||||||
|
|
||||||
|
class IBApp(IBWrapper, IBClient):
|
||||||
|
def __init__(self):
|
||||||
|
IBWrapper.__init__(self)
|
||||||
|
IBClient.__init__(self, wrapper=self)
|
||||||
|
self.connect_error = None
|
||||||
|
|
||||||
|
def connect_app(self):
|
||||||
|
try:
|
||||||
|
self.connect(HOST, PORT, CLIENT_ID)
|
||||||
|
except Exception as e:
|
||||||
|
self.connect_error = e
|
||||||
|
|
||||||
|
thread = threading.Thread(target=self.run, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
# Wait a bit for connection
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def disconnect_app(self):
|
||||||
|
if self.isConnected():
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
print("Interrupt received, shutting down...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
|
def is_trading_hours(now_eastern):
|
||||||
|
# Trading hours: Monday-Friday, 9:30 - 16:00 Eastern
|
||||||
|
if now_eastern.weekday() > 4: # Saturday=5, Sunday=6
|
||||||
|
return False
|
||||||
|
open_time = now_eastern.replace(hour=9, minute=30, second=0, microsecond=0)
|
||||||
|
close_time = now_eastern.replace(hour=16, minute=0, second=0, microsecond=0)
|
||||||
|
return open_time <= now_eastern <= close_time
|
||||||
|
|
||||||
|
def get_symbols_from_file(file_path):
|
||||||
|
symbols = []
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
csv_reader = csv.reader(f)
|
||||||
|
for row in csv_reader:
|
||||||
|
# Extend the symbols list with all symbols in the current row
|
||||||
|
symbols.extend([symbol.strip() for symbol in row if symbol.strip()])
|
||||||
|
return symbols
|
||||||
|
|
||||||
|
|
||||||
|
def request_historical_data(app, symbol):
|
||||||
|
contract = Contract()
|
||||||
|
contract.symbol = symbol
|
||||||
|
contract.secType = "STK"
|
||||||
|
contract.exchange = "SMART"
|
||||||
|
contract.currency = "USD"
|
||||||
|
|
||||||
|
app.historical_bars = []
|
||||||
|
app.historical_data_received.clear()
|
||||||
|
app.historical_data_reqId = 10000 # arbitrary reqId
|
||||||
|
|
||||||
|
endDateTime = datetime.now().strftime("%Y%m%d %H:%M:%S") + " UTC"
|
||||||
|
try:
|
||||||
|
app.reqHistoricalData(
|
||||||
|
reqId=app.historical_data_reqId,
|
||||||
|
contract=contract,
|
||||||
|
endDateTime=endDateTime,
|
||||||
|
durationStr="2 D",
|
||||||
|
barSizeSetting="1 day",
|
||||||
|
whatToShow="TRADES",
|
||||||
|
useRTH=1,
|
||||||
|
formatDate=1,
|
||||||
|
keepUpToDate=False,
|
||||||
|
chartOptions=[]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error requesting historical data for {symbol}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not app.historical_data_received.wait(timeout=10):
|
||||||
|
logging.warning(f"Timeout waiting for historical data for {symbol}.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return app.historical_bars
|
||||||
|
|
||||||
|
def serialize_decimal(obj):
|
||||||
|
if isinstance(obj, Decimal):
|
||||||
|
return float(obj)
|
||||||
|
raise TypeError(f"Type {type(obj)} not serializable")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = IBApp()
|
||||||
|
app.connect_app()
|
||||||
|
|
||||||
|
if app.connect_error:
|
||||||
|
logging.error(f"Failed to connect to IB API: {app.connect_error}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
app.reqCurrentTime()
|
||||||
|
if not app.current_time_received.wait(timeout=10):
|
||||||
|
logging.error("Timeout waiting for current time.")
|
||||||
|
app.disconnect_app()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
ib_server_time = datetime.utcfromtimestamp(app.current_time)
|
||||||
|
eastern = pytz.timezone("US/Eastern")
|
||||||
|
ib_server_time_eastern = ib_server_time.replace(tzinfo=pytz.utc).astimezone(eastern)
|
||||||
|
|
||||||
|
if is_trading_hours(ib_server_time_eastern):
|
||||||
|
logging.info("It's currently within trading hours. No historical retrieval needed.")
|
||||||
|
app.disconnect_app()
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
logging.info("Outside trading hours. Proceeding with historical data retrieval.")
|
||||||
|
|
||||||
|
symbols = get_symbols_from_file(TICKER_FILE)
|
||||||
|
logging.info(f"Loaded {len(symbols)} symbols from {TICKER_FILE}.")
|
||||||
|
|
||||||
|
raw_data = []
|
||||||
|
for symbol in symbols:
|
||||||
|
bars = request_historical_data(app, symbol)
|
||||||
|
if bars is None or len(bars) < 2:
|
||||||
|
logging.info(f"Skipping {symbol}: not enough data.")
|
||||||
|
else:
|
||||||
|
last_bar = bars[-1]
|
||||||
|
prev_bar = bars[-2]
|
||||||
|
net_change = last_bar.close - prev_bar.close
|
||||||
|
percent_change = (net_change / prev_bar.close) * 100 if prev_bar.close != 0 else 0.0
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
"symbol": symbol,
|
||||||
|
"date": last_bar.date,
|
||||||
|
"open": float(last_bar.open),
|
||||||
|
"high": float(last_bar.high),
|
||||||
|
"low": float(last_bar.low),
|
||||||
|
"close": float(last_bar.close),
|
||||||
|
"volume": int(last_bar.volume),
|
||||||
|
"net_change": float(net_change),
|
||||||
|
"percent_change": float(percent_change)
|
||||||
|
}
|
||||||
|
raw_data.append(entry)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
now_str = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
raw_filename = os.path.join(DATA_DIR, f"raw_stock_info_{now_str}.json")
|
||||||
|
with open(raw_filename, 'w') as f:
|
||||||
|
json.dump(raw_data, f, indent=4, default=serialize_decimal)
|
||||||
|
logging.info(f"Raw data saved to {raw_filename}")
|
||||||
|
|
||||||
|
app.disconnect_app()
|
||||||
|
logging.info("All done.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
70
src/MidasV1/tests/AdvHistoricalModule2Test/clean_tickes.py
Normal file
70
src/MidasV1/tests/AdvHistoricalModule2Test/clean_tickes.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
def clean_tickers(data_dir):
|
||||||
|
# Define paths
|
||||||
|
input_file = os.path.join(data_dir, "us_tickers.csv")
|
||||||
|
output_dir = os.path.join(data_dir, "csv")
|
||||||
|
output_cleaned = os.path.join(output_dir, "cleaned_us_tickers.csv")
|
||||||
|
output_refined = os.path.join(output_dir, "refined_us_tickers.csv")
|
||||||
|
|
||||||
|
# Check if input file exists
|
||||||
|
if not os.path.exists(input_file):
|
||||||
|
print(f"Error: {input_file} not found in {data_dir}. Please ensure the file is in the correct directory.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create output directory if it doesn't exist
|
||||||
|
if not os.path.exists(output_dir):
|
||||||
|
os.makedirs(output_dir)
|
||||||
|
|
||||||
|
# Load the CSV file
|
||||||
|
try:
|
||||||
|
tickers_data = pd.read_csv(input_file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading the file: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Step 1: Initial Cleanup
|
||||||
|
relevant_columns = ['Ticker', 'Name', 'Primary Exchange', 'Type']
|
||||||
|
cleaned_data = tickers_data[relevant_columns]
|
||||||
|
|
||||||
|
# Filter rows with specific types (e.g., CS for common stock, ETF for exchange-traded fund)
|
||||||
|
valid_types = ['CS', 'ETF']
|
||||||
|
cleaned_data = cleaned_data[cleaned_data['Type'].isin(valid_types)]
|
||||||
|
|
||||||
|
# Drop rows with missing values in critical columns
|
||||||
|
cleaned_data = cleaned_data.dropna(subset=['Ticker', 'Name', 'Primary Exchange'])
|
||||||
|
|
||||||
|
# Save the cleaned data to a new file
|
||||||
|
cleaned_data.to_csv(output_cleaned, index=False)
|
||||||
|
print(f"Cleaned data saved to {output_cleaned}.")
|
||||||
|
|
||||||
|
# Step 2: Ask for further refinement
|
||||||
|
refine = input("Do you want to refine further to keep only Ticker IDs (formatted as CSV)? (yes/no): ").strip().lower()
|
||||||
|
if refine == "yes":
|
||||||
|
# Keep only the Ticker column and format as a single line CSV
|
||||||
|
refined_data = cleaned_data[['Ticker']]
|
||||||
|
ticker_list = refined_data['Ticker'].tolist()
|
||||||
|
|
||||||
|
# Save as a single line CSV
|
||||||
|
with open(output_refined, 'w') as f:
|
||||||
|
f.write(','.join(ticker_list))
|
||||||
|
|
||||||
|
print(f"Refined data saved to {output_refined} (formatted as a single-line CSV).")
|
||||||
|
else:
|
||||||
|
print("No further refinement done. Exiting.")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Define the data directory
|
||||||
|
data_dir = "data" # Relative path to the data directory
|
||||||
|
|
||||||
|
# Ensure the data directory exists
|
||||||
|
if not os.path.exists(data_dir):
|
||||||
|
os.makedirs(data_dir)
|
||||||
|
|
||||||
|
# Run the cleaning process
|
||||||
|
clean_tickers(data_dir)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
17
src/MidasV1/tests/AdvHistoricalModule2Test/config.ini
Normal file
17
src/MidasV1/tests/AdvHistoricalModule2Test/config.ini
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[API]
|
||||||
|
host=127.0.0.1
|
||||||
|
port=4002
|
||||||
|
clientId=1
|
||||||
|
|
||||||
|
[Directories]
|
||||||
|
data_dir=./data
|
||||||
|
|
||||||
|
[General]
|
||||||
|
ticker_file=ticker_ids.csv
|
||||||
|
good_runtime_threshold=120
|
||||||
|
|
||||||
|
[Thresholds]
|
||||||
|
min_volume = 1000000
|
||||||
|
min_percent_change = 0.2
|
||||||
|
min_net_change = 0.5
|
||||||
|
max_share_price = 1000.0
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,76 @@
|
|||||||
|
import requests
|
||||||
|
import csv
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
# Polygon API configuration
|
||||||
|
API_KEY = "RumpAnkjWlY69jKygFzMyDm5Chc3luDr" # Replace with your Polygon.io API key
|
||||||
|
BASE_URL = "https://api.polygon.io/v3/reference/tickers"
|
||||||
|
OUTPUT_FILE = "data/us_tickers.csv"
|
||||||
|
|
||||||
|
# API parameters
|
||||||
|
params = {
|
||||||
|
"market": "stocks", # US stock market
|
||||||
|
"active": "true", # Only active tickers
|
||||||
|
"limit": 1000, # Max limit per request
|
||||||
|
"apiKey": API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
def fetch_tickers():
|
||||||
|
tickers = []
|
||||||
|
next_url = BASE_URL
|
||||||
|
|
||||||
|
while next_url:
|
||||||
|
logging.info(f"Fetching data from {next_url}")
|
||||||
|
response = requests.get(next_url, params=params)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
results = data.get("results", [])
|
||||||
|
tickers.extend(results)
|
||||||
|
|
||||||
|
# Check if there are more pages
|
||||||
|
next_url = data.get("next_url", None)
|
||||||
|
time.sleep(1) # Rate limit handling (1 second delay)
|
||||||
|
else:
|
||||||
|
logging.error(f"Failed to fetch data: {response.status_code} - {response.text}")
|
||||||
|
break
|
||||||
|
|
||||||
|
return tickers
|
||||||
|
|
||||||
|
def save_to_csv(tickers):
|
||||||
|
# Save the tickers to a CSV file
|
||||||
|
with open(OUTPUT_FILE, mode="w", newline="", encoding="utf-8") as file:
|
||||||
|
writer = csv.writer(file)
|
||||||
|
# Write header
|
||||||
|
writer.writerow(["Ticker", "Name", "Market", "Locale", "Primary Exchange", "Type", "Currency"])
|
||||||
|
# Write rows
|
||||||
|
for ticker in tickers:
|
||||||
|
writer.writerow([
|
||||||
|
ticker.get("ticker"),
|
||||||
|
ticker.get("name"),
|
||||||
|
ticker.get("market"),
|
||||||
|
ticker.get("locale"),
|
||||||
|
ticker.get("primary_exchange"),
|
||||||
|
ticker.get("type"),
|
||||||
|
ticker.get("currency")
|
||||||
|
])
|
||||||
|
logging.info(f"Saved {len(tickers)} tickers to {OUTPUT_FILE}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logging.info("Starting ticker retrieval program...")
|
||||||
|
|
||||||
|
# Fetch tickers
|
||||||
|
tickers = fetch_tickers()
|
||||||
|
if tickers:
|
||||||
|
logging.info(f"Fetched {len(tickers)} tickers.")
|
||||||
|
save_to_csv(tickers)
|
||||||
|
else:
|
||||||
|
logging.warning("No tickers fetched. Exiting program.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import configparser
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import csv
|
||||||
|
from decimal import Decimal
|
||||||
|
import pytz
|
||||||
|
import threading
|
||||||
|
import yfinance as yf
|
||||||
|
import signal
|
||||||
|
|
||||||
|
from ibapi.client import EClient
|
||||||
|
from ibapi.wrapper import EWrapper
|
||||||
|
from ibapi.contract import Contract
|
||||||
|
|
||||||
|
now_str = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
|
# Color definitions for terminal output
|
||||||
|
class Colors:
|
||||||
|
RESET = "\033[0m"
|
||||||
|
RED = "\033[91m"
|
||||||
|
GREEN = "\033[92m"
|
||||||
|
YELLOW = "\033[93m"
|
||||||
|
BLUE = "\033[94m"
|
||||||
|
MAGENTA = "\033[95m"
|
||||||
|
|
||||||
|
# Configure logging with colors
|
||||||
|
def color_log(level, message, color):
|
||||||
|
print(f"{color}[{level.upper()}]{Colors.RESET} {message}")
|
||||||
|
|
||||||
|
# Load config
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
if not os.path.exists('config.ini'):
|
||||||
|
color_log("error", "config.ini not found. Please create one.", Colors.RED)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
config.read('config.ini')
|
||||||
|
HOST = config.get('API', 'host', fallback='127.0.0.1')
|
||||||
|
PORT = config.getint('API', 'port', fallback=4002)
|
||||||
|
CLIENT_ID = config.getint('API', 'clientId', fallback=1)
|
||||||
|
DATA_DIR = config.get('Directories', 'data_dir', fallback='./data')
|
||||||
|
TICKER_FILE = config.get('General', 'ticker_file', fallback='ticker_ids.csv')
|
||||||
|
MIN_VOLUME = config.getint('Thresholds', 'min_volume', fallback=1000000)
|
||||||
|
MIN_NET_CHANGE = config.getfloat('Thresholds', 'min_net_change', fallback=0.1)
|
||||||
|
MIN_PERCENT_CHANGE = config.getfloat('Thresholds', 'min_percent_change', fallback=0.5)
|
||||||
|
MAX_SHARE_PRICE = config.getfloat('Thresholds', 'max_share_price', fallback=500.0)
|
||||||
|
GOOD_RUNTIME_THRESHOLD = config.getfloat('Performance', 'good_runtime_threshold', fallback=10.0)
|
||||||
|
|
||||||
|
if not os.path.exists(DATA_DIR):
|
||||||
|
os.makedirs(DATA_DIR)
|
||||||
|
|
||||||
|
if not os.path.exists(TICKER_FILE):
|
||||||
|
color_log("error", f"{TICKER_FILE} not found in {DATA_DIR}. Please create one with a list of symbols.", Colors.RED)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Debug loaded configuration
|
||||||
|
def debug_config():
|
||||||
|
color_log("debug", "Configuration loaded:", Colors.BLUE)
|
||||||
|
for section in config.sections():
|
||||||
|
for key, value in config[section].items():
|
||||||
|
color_log("debug", f"{section.upper()} - {key}: {value}", Colors.BLUE)
|
||||||
|
|
||||||
|
debug_config()
|
||||||
|
|
||||||
|
class IBWrapper(EWrapper):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.current_time = None
|
||||||
|
self.current_time_received = threading.Event()
|
||||||
|
|
||||||
|
self.historical_data_reqId = None
|
||||||
|
self.historical_bars = []
|
||||||
|
self.historical_data_received = threading.Event()
|
||||||
|
|
||||||
|
def error(self, reqId, errorCode, errorString, advancedOrderRejectJson=""):
|
||||||
|
if errorCode in [2104, 2106, 2158]:
|
||||||
|
color_log("info", f"System Check: ReqId {reqId}, Code {errorCode}, Msg: {errorString}", Colors.BLUE)
|
||||||
|
else:
|
||||||
|
color_log("error", f"Error. ReqId: {reqId}, Code: {errorCode}, Msg: {errorString}", Colors.RED)
|
||||||
|
|
||||||
|
def currentTime(self, time_):
|
||||||
|
self.current_time = time_
|
||||||
|
self.current_time_received.set()
|
||||||
|
|
||||||
|
def historicalData(self, reqId, bar):
|
||||||
|
if reqId == self.historical_data_reqId:
|
||||||
|
self.historical_bars.append(bar)
|
||||||
|
|
||||||
|
def historicalDataEnd(self, reqId, start, end):
|
||||||
|
if reqId == self.historical_data_reqId:
|
||||||
|
self.historical_data_received.set()
|
||||||
|
|
||||||
|
class IBClient(EClient):
|
||||||
|
def __init__(self, wrapper):
|
||||||
|
EClient.__init__(self, wrapper)
|
||||||
|
|
||||||
|
class IBApp(IBWrapper, IBClient):
|
||||||
|
def __init__(self):
|
||||||
|
IBWrapper.__init__(self)
|
||||||
|
IBClient.__init__(self, wrapper=self)
|
||||||
|
self.connect_error = None
|
||||||
|
|
||||||
|
def connect_app(self):
|
||||||
|
try:
|
||||||
|
self.connect(HOST, PORT, CLIENT_ID)
|
||||||
|
except Exception as e:
|
||||||
|
self.connect_error = e
|
||||||
|
|
||||||
|
thread = threading.Thread(target=self.run, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def disconnect_app(self):
|
||||||
|
if self.isConnected():
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
def calculate_start_date():
|
||||||
|
"""Calculate the start date for historical data (2 trading days ago)."""
|
||||||
|
current_date = datetime.now()
|
||||||
|
delta_days = 2
|
||||||
|
while delta_days > 0:
|
||||||
|
current_date -= timedelta(days=1)
|
||||||
|
if current_date.weekday() < 5: # Skip weekends
|
||||||
|
delta_days -= 1
|
||||||
|
return current_date.strftime("%Y%m%d")
|
||||||
|
|
||||||
|
def get_symbols_from_file(file_path):
|
||||||
|
symbols = []
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
csv_reader = csv.reader(f)
|
||||||
|
for row in csv_reader:
|
||||||
|
symbols.extend([symbol.strip() for symbol in row if symbol.strip()])
|
||||||
|
return symbols
|
||||||
|
|
||||||
|
def request_historical_data(app, symbol, start_date):
|
||||||
|
"""Request historical data for a given symbol."""
|
||||||
|
contract = Contract()
|
||||||
|
contract.symbol = symbol
|
||||||
|
contract.secType = "STK"
|
||||||
|
contract.exchange = "SMART"
|
||||||
|
contract.currency = "USD"
|
||||||
|
|
||||||
|
app.historical_bars = []
|
||||||
|
app.historical_data_received.clear()
|
||||||
|
app.historical_data_reqId = 10000 # arbitrary reqId
|
||||||
|
|
||||||
|
endDateTime = f"{start_date} 09:30:00 UTC"
|
||||||
|
try:
|
||||||
|
app.reqHistoricalData(
|
||||||
|
reqId=app.historical_data_reqId,
|
||||||
|
contract=contract,
|
||||||
|
endDateTime=endDateTime,
|
||||||
|
durationStr="2 D",
|
||||||
|
barSizeSetting="1 day",
|
||||||
|
whatToShow="TRADES",
|
||||||
|
useRTH=1,
|
||||||
|
formatDate=1,
|
||||||
|
keepUpToDate=False,
|
||||||
|
chartOptions=[]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
color_log("error", f"Error requesting historical data for {symbol}: {e}", Colors.RED)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not app.historical_data_received.wait(timeout=10):
|
||||||
|
color_log("warning", f"Timeout waiting for historical data for {symbol}.", Colors.YELLOW)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return app.historical_bars
|
||||||
|
|
||||||
|
def filter_data(raw_data):
|
||||||
|
"""Filter raw data based on thresholds."""
|
||||||
|
filtered_data = [
|
||||||
|
entry for entry in raw_data
|
||||||
|
if entry['volume'] >= MIN_VOLUME and
|
||||||
|
entry['net_change'] >= MIN_NET_CHANGE and
|
||||||
|
entry['percent_change'] >= MIN_PERCENT_CHANGE and
|
||||||
|
entry['close'] <= MAX_SHARE_PRICE
|
||||||
|
]
|
||||||
|
if not filtered_data:
|
||||||
|
color_log("warning", "No data passed filtering. Check your config or provide a larger data pool.", Colors.YELLOW)
|
||||||
|
return filtered_data
|
||||||
|
|
||||||
|
def refine_with_options(processed_data, app):
|
||||||
|
"""
|
||||||
|
Refine processed data to include only stocks with associated option contracts.
|
||||||
|
Attempts to use the IBKR API first, then falls back to Yahoo Finance.
|
||||||
|
If IBKR fails persistently, it switches entirely to Yahoo Finance for subsequent checks.
|
||||||
|
"""
|
||||||
|
refined_data = []
|
||||||
|
ibkr_persistent_fail = False # Flag to skip IBKR checks after persistent failures
|
||||||
|
|
||||||
|
def check_with_ibkr(symbol):
|
||||||
|
"""Check if a stock has options contracts using IBKR."""
|
||||||
|
nonlocal ibkr_persistent_fail
|
||||||
|
|
||||||
|
if ibkr_persistent_fail:
|
||||||
|
color_log("info", f"Skipping IBKR check for {symbol} due to persistent failures.", Colors.YELLOW)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
contract = Contract()
|
||||||
|
contract.symbol = symbol
|
||||||
|
contract.secType = "STK" # Stock type for the underlying security
|
||||||
|
contract.exchange = "SMART"
|
||||||
|
contract.currency = "USD"
|
||||||
|
|
||||||
|
color_log("info", f"IBKR: Requesting options contract for {symbol} with contract parameters: {contract}", Colors.MAGENTA)
|
||||||
|
|
||||||
|
app.historical_data_received.clear()
|
||||||
|
app.reqSecDefOptParams(
|
||||||
|
reqId=1,
|
||||||
|
underlyingSymbol=symbol,
|
||||||
|
futFopExchange="",
|
||||||
|
underlyingSecType="STK",
|
||||||
|
underlyingConId=0
|
||||||
|
)
|
||||||
|
|
||||||
|
if not app.historical_data_received.wait(timeout=5): # Reduced timeout
|
||||||
|
color_log("warning", f"IBKR: Timeout while querying options for {symbol}.", Colors.YELLOW)
|
||||||
|
return False
|
||||||
|
|
||||||
|
color_log("info", f"IBKR: Successfully queried options contract for {symbol}.", Colors.GREEN)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
color_log("error", f"IBKR option check failed for {symbol}: {e}", Colors.RED)
|
||||||
|
if "Invalid contract id" in str(e): # Check for specific persistent error
|
||||||
|
ibkr_persistent_fail = True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_with_yfinance(symbol):
|
||||||
|
"""Check if a stock has options contracts using Yahoo Finance."""
|
||||||
|
try:
|
||||||
|
stock = yf.Ticker(symbol)
|
||||||
|
if stock.options:
|
||||||
|
color_log("info", f"Yahoo Finance: Options contract found for {symbol}.", Colors.GREEN)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
color_log("info", f"Yahoo Finance: No options contract found for {symbol}.", Colors.YELLOW)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
color_log("warning", f"Yahoo Finance option check failed for {symbol}: {e}", Colors.YELLOW)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Process each stock in the data
|
||||||
|
for entry in processed_data:
|
||||||
|
symbol = entry['symbol']
|
||||||
|
color_log("info", f"Checking options contracts for {symbol}...", Colors.MAGENTA)
|
||||||
|
|
||||||
|
# Try IBKR first unless persistent failures are detected
|
||||||
|
if check_with_ibkr(symbol):
|
||||||
|
refined_data.append(entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fallback to Yahoo Finance
|
||||||
|
color_log("info", f"Falling back to Yahoo Finance for {symbol}...", Colors.BLUE)
|
||||||
|
if check_with_yfinance(symbol):
|
||||||
|
refined_data.append(entry)
|
||||||
|
|
||||||
|
if not refined_data:
|
||||||
|
color_log("warning", "No stocks with associated options contracts found.", Colors.YELLOW)
|
||||||
|
|
||||||
|
return refined_data
|
||||||
|
|
||||||
|
def save_to_csv(ticker_ids, filename):
|
||||||
|
"""Save ticker IDs to a CSV file."""
|
||||||
|
with open(filename, 'w', newline='') as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerow(ticker_ids)
|
||||||
|
color_log("info", f"Ticker IDs saved to {filename}.", Colors.GREEN)
|
||||||
|
|
||||||
|
def save_data(data, filename):
|
||||||
|
"""Save data to a JSON file."""
|
||||||
|
if not data:
|
||||||
|
color_log("warning", f"No data to save in {filename}.", Colors.YELLOW)
|
||||||
|
else:
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(data, f, indent=4)
|
||||||
|
color_log("info", f"Data saved to {filename}.", Colors.GREEN)
|
||||||
|
|
||||||
|
def handle_sigint(signal_received, frame):
|
||||||
|
"""
|
||||||
|
Handle SIGINT (Ctrl+C) to perform cleanup before exiting.
|
||||||
|
Deletes incomplete data files.
|
||||||
|
"""
|
||||||
|
color_log("error", "SIGINT received. Cleaning up and exiting...", Colors.RED)
|
||||||
|
|
||||||
|
# List of files to clean up
|
||||||
|
temp_files = [
|
||||||
|
os.path.join(DATA_DIR, f"raw_stock_info_{now_str}.json"),
|
||||||
|
os.path.join(DATA_DIR, f"processed_data_{now_str}.json"),
|
||||||
|
os.path.join(DATA_DIR, f"contract_option_stock_info_{now_str}.json")
|
||||||
|
]
|
||||||
|
|
||||||
|
for file in temp_files:
|
||||||
|
if os.path.exists(file):
|
||||||
|
os.remove(file)
|
||||||
|
color_log("info", f"Deleted incomplete file: {file}", Colors.YELLOW)
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
signal.signal(signal.SIGINT, handle_sigint)
|
||||||
|
start_time = time.time()
|
||||||
|
app = IBApp()
|
||||||
|
app.connect_app()
|
||||||
|
|
||||||
|
if app.connect_error:
|
||||||
|
color_log("error", f"Failed to connect to IB API: {app.connect_error}", Colors.RED)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
start_date = calculate_start_date()
|
||||||
|
color_log("info", f"Start date for data retrieval: {start_date}", Colors.BLUE)
|
||||||
|
|
||||||
|
symbols = get_symbols_from_file(TICKER_FILE)
|
||||||
|
color_log("info", f"Loaded {len(symbols)} symbols from {TICKER_FILE}.", Colors.BLUE)
|
||||||
|
|
||||||
|
raw_data = []
|
||||||
|
ibkr_checks = 0
|
||||||
|
yahoo_checks = 0
|
||||||
|
|
||||||
|
for symbol in symbols:
|
||||||
|
color_log("info", f"Retrieving data for {symbol}...", Colors.MAGENTA)
|
||||||
|
bars = request_historical_data(app, symbol, start_date)
|
||||||
|
if bars is None or len(bars) < 2:
|
||||||
|
color_log("warning", f"Skipping {symbol}: not enough data.", Colors.YELLOW)
|
||||||
|
else:
|
||||||
|
last_bar = bars[-1]
|
||||||
|
prev_bar = bars[-2]
|
||||||
|
net_change = last_bar.close - prev_bar.close
|
||||||
|
percent_change = (net_change / prev_bar.close) * 100 if prev_bar.close != 0 else 0.0
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
"symbol": symbol,
|
||||||
|
"date": last_bar.date,
|
||||||
|
"open": float(last_bar.open),
|
||||||
|
"high": float(last_bar.high),
|
||||||
|
"low": float(last_bar.low),
|
||||||
|
"close": float(last_bar.close),
|
||||||
|
"volume": int(last_bar.volume),
|
||||||
|
"net_change": float(net_change),
|
||||||
|
"percent_change": float(percent_change)
|
||||||
|
}
|
||||||
|
raw_data.append(entry)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
now_str = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
raw_filename = os.path.join(DATA_DIR, f"raw_stock_info_{now_str}.json")
|
||||||
|
save_data(raw_data, raw_filename)
|
||||||
|
|
||||||
|
# Filter and save processed data
|
||||||
|
processed_data = filter_data(raw_data)
|
||||||
|
processed_filename = os.path.join(DATA_DIR, f"processed_data_{now_str}.json")
|
||||||
|
save_data(processed_data, processed_filename)
|
||||||
|
|
||||||
|
# Track fallback performance metrics
|
||||||
|
color_log("info", "Starting refinement with options contracts...", Colors.BLUE)
|
||||||
|
refined_data = refine_with_options(processed_data, app)
|
||||||
|
refined_filename = os.path.join(DATA_DIR, f"contract_option_stock_info_{now_str}.json")
|
||||||
|
save_data(refined_data, refined_filename)
|
||||||
|
|
||||||
|
# Extract ticker IDs and save to CSV
|
||||||
|
ticker_ids = [entry['symbol'] for entry in refined_data]
|
||||||
|
csv_filename = os.path.join(DATA_DIR, "ticker_ids_with_match.csv")
|
||||||
|
save_to_csv(ticker_ids, csv_filename)
|
||||||
|
|
||||||
|
app.disconnect_app()
|
||||||
|
end_time = time.time()
|
||||||
|
runtime = end_time - start_time
|
||||||
|
|
||||||
|
# Determine the proportion of IBKR and Yahoo Finance checks
|
||||||
|
total_checks = len(processed_data)
|
||||||
|
yahoo_checks = total_checks - ibkr_checks
|
||||||
|
|
||||||
|
# Log detailed performance analysis
|
||||||
|
if runtime < GOOD_RUNTIME_THRESHOLD:
|
||||||
|
color_log("info", f"Program completed in {runtime:.2f} seconds (Good runtime).", Colors.GREEN)
|
||||||
|
elif runtime == GOOD_RUNTIME_THRESHOLD:
|
||||||
|
color_log("warning", f"Program completed in {runtime:.2f} seconds (Runtime threshold met).", Colors.YELLOW)
|
||||||
|
else:
|
||||||
|
color_log("error", f"Program completed in {runtime:.2f} seconds (Bad runtime).", Colors.RED)
|
||||||
|
|
||||||
|
color_log("info", f"Refinement breakdown: IBKR checks = {ibkr_checks}, Yahoo Finance checks = {yahoo_checks}", Colors.MAGENTA)
|
||||||
|
color_log(
|
||||||
|
"info",
|
||||||
|
"Time Complexity: O(k × t_ibkr + (n - k) × t_yahoo), where:\n"
|
||||||
|
f" k = {ibkr_checks} (IBKR checks)\n"
|
||||||
|
f" n = {total_checks} (Total symbols processed)\n"
|
||||||
|
f" t_ibkr = avg. IBKR query time\n"
|
||||||
|
f" t_yahoo = avg. Yahoo Finance query time",
|
||||||
|
Colors.BLUE
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user