Get News for a Ticker with the IBKR API

This example shows how to get News items for a stock given its ticker symbol using the Interactive Brokers (IBKR) API in Python.

The API distinguishes Historical News from streaming, real-time news.
This post is about historical news, i.e. a list of dates sorted by time.
This is different from subscribing to News ticks and listening for new news events.

Note that we need to get the Contract ID for the ticker symbol first.
The Contract ID (conId) is stored inside the IBapi object for later use.

This is done in contractDetails().

The default, freely available news sources in the API are:

  • Briefing.com General Market Columns (BRFG)
  • Briefing.com Analyst Actions (BRFUPDN)
  • Dow Jones Newsletters (DJNL)

If you are subscribed to more news sources, they can be included in the API results given the proper codes.

Some other things to note:

  • The API returns headlines with extra metadata such as language inside braces ({…}). We filter these out using a regular expression as there does not seem to be any way to omit them from the results.
  • IBKR Trader Workstation (TWS) is assumed to be running on localhost.

Complete script:

import re
import threading
import time

from datetime import datetime, timedelta

from ibapi.client import EClient, Contract
from ibapi.contract import ContractDetails
from ibapi.wrapper import EWrapper

# Extend IBKR scanner base class.
class IBapi(EClient, EWrapper):
  def __init__(self):
    EClient.__init__(self, self)

  def contractDetails(self, reqId: int, contractDetails: ContractDetails):
    print(f"Ticker: {contractDetails.contract.symbol}, conId: {contractDetails.contract.conId}")
    self.conId = contractDetails.contract.conId

  def historicalNews(self, requestId, timeStamp, providerCode, articleId, headline):
    print(timeStamp + " " + self.cleanHeadline(headline))

  # Remove metadata often included in the headline within "{...}", sometimes with "!".
  def cleanHeadline(self, headline):
    return re.sub("\{.*?}!?", "", headline)

# Main script.
TWS_HOST = "127.0.0.1"
TWS_PORT = 7497
CLIENT_ID = 12345

ticker = input("Ticker symbol: ")

ibApi = IBapi()
ibApi.connect(TWS_HOST, TWS_PORT, CLIENT_ID)
threading.Thread(target=ibApi.run).start()
time.sleep(5) # Allow time for subscription.

# Contract object for the stock.
contract = Contract()
contract.symbol = ticker
contract.secType = "STK" # Stocks.
contract.exchange = "SMART"
contract.currency = "USD"

ibApi.reqContractDetails(
  reqId=101,
  contract=contract
)

time.sleep(3) # Allow time to get contract details.

PROVIDERS = "BRFG+BRFUPDN+DJNL" # Briefing.com, Dow Jones Newsletters.
TOTAL_RESULTS = 15
LOOKBACK_DAYS = 10 # Num days from now to oldest news.
DATE_FORMAT = "%Y%m%d %H:%M:%S"

currentDatetime = datetime.now()
currentDatetimeFormatted = currentDatetime.strftime(DATE_FORMAT)

tenDaysAgo = currentDatetime - timedelta(days=LOOKBACK_DAYS)
tenDaysAgoFormatted = tenDaysAgo.strftime(DATE_FORMAT)

ibApi.reqHistoricalNews(
  reqId=102,
  conId=ibApi.conId, # Contract ID for ticker.
  providerCodes=PROVIDERS,
  startDateTime=tenDaysAgoFormatted,
  endDateTime=currentDatetimeFormatted,
  totalResults=TOTAL_RESULTS,
  historicalNewsOptions=[]
)

time.sleep(1)
ibApi.disconnect()

Example run:

Ticker symbol: TSLA
Ticker: TSLA, conId: 76792991
2025-10-02 14:19:54.0 Tesla cruises past Q3 delivery estimates, but tax credit pull-forward clouds Q4 outlook
2025-09-30 14:59:17.0 Canaccord Genuity reiterated Tesla (TSLA) coverage with Buy and target $490
2025-09-26 11:49:39.0 Deutsche Bank reiterated Tesla (TSLA) coverage with Buy and target $435
2025-09-23 11:51:14.0 Mizuho reiterated Tesla (TSLA) coverage with Outperform and target $450
2025-09-22 12:57:41.0 Piper Sandler reiterated Tesla (TSLA) coverage with Overweight and target $500
2025-09-19 12:20:40.0 Robert W. Baird upgraded Tesla (TSLA) to Outperform with target $548
2025-09-18 17:11:04.0 Goldman reiterated Tesla (TSLA) coverage with Neutral and target $395
2025-09-15 14:03:38.0 Tesla's Musk makes bold $1 bln stock bet amid EV headwinds and AI ambitions
2025-07-29 12:14:24.0 RBC Capital Mkts reiterated Tesla (TSLA) coverage with Outperform and target $325
2025-07-25 13:00:32.0 China Renaissance downgraded Tesla (TSLA) to Hold with target $349
2025-07-24 14:20:22.0 Tesla's better-than-feared Q2 results overshadowed by Musk's cautious outlook
2025-07-24 11:54:12.0 Canaccord Genuity reiterated Tesla (TSLA) coverage with Buy and target $333
2025-07-07 12:14:59.0 William Blair downgraded Tesla (TSLA) to Mkt Perform
2025-07-01 14:10:42.0 Tesla shares slide as Trump targets Musk's EV subsidies in public spat
2025-06-26 12:32:13.0 The Benchmark Company reiterated Tesla (TSLA) coverage with Buy and target $475

Note:

It is also possible to get entire articles with reqNewsArticle().

References

https://interactivebrokers.github.io/tws-api/news.html

 

Iterate over a Range of Characters in Perl

To easily iterate over a range of characters in Perl, we can use the range operator to define an array of chars.
Then, we can iterate over each element with a for-each loop.
This can be useful when iterating over objects named alphabetically instead of numerically, for example.

The code is below.

use strict;

my @chars = ('a'..'z');

foreach my $c (@chars) {
  print("Current char: $c \n");
}

Truncated output:

Current char: a
Current char: b
Current char: c
Current char: d
Current char: e
Current char: f
...

Current char: z

 

Get Short Share Borrow Availability using the IBKR API

The following script shows how to retrieve a value indicating if a stock has shares available to borrow to sell short in the Interactive Brokers (IBKR) system.

First, make sure the IBKR API for Python is installed:

$ pip install ibapi

We assume Trader Workstation (TWS) is running locally and allows connections from EClients.

The script takes the ticker symbol as input.

The class extending EClient and EWrapper allows us to implement an event handler: tickGeneric(). We will subscribe to the tick indicating short share availability and the data will be received via this callback.

Note the value for genericTickList is “236” in the call to reqMktData() to subscribe to the short shares available tick.

Note that there will be a delay before the generic tick event fires and future updates are not frequent.

Use CTRL-C to exit.

Complete script:

import threading
import time

from ibapi.client import EClient, Contract
from ibapi.wrapper import EWrapper

# Extend IBKR scanner base class.
class IBapi(EClient, EWrapper):
  def __init__(self):
    EClient.__init__(self, self)

  def tickGeneric(self, reqId, tickType, value):
    if tickType == 46:
      print("Value (short shares available indicator): " + str(value))
      # Indicator value interpreted according to the docs:
      # https://interactivebrokers.github.io/tws-api/tick_types.html
      if value <= 1.5:
        print("Not available for short sale.")
      if value > 1.5 and value <= 2.5:
        print("Hard to borrow. Locate required.")
      if value > 2.5:
        print("Easy to borrow. At least 1000 shares available.")

# Main script.
TWS_HOST = "127.0.0.1"
TWS_PORT = 7497
CLIENT_ID = 12345

ticker = input("Ticker symbol: ")

app = IBapi()
app.connect(TWS_HOST, TWS_PORT, CLIENT_ID)
threading.Thread(target=app.run).start()
time.sleep(5) # Allow time for subscription.

contract = Contract()
contract.symbol = ticker
contract.secType = "STK" # Stocks.
contract.exchange = "SMART"
contract.currency = "USD"

app.reqMarketDataType(3) # Delayed streaming data.
app.reqMktData(reqId=100, # Arbitrary ID.
  contract=contract,
  genericTickList="236", # Includes borrow availability.
  snapshot=False,
  regulatorySnapshot=False,
  mktDataOptions=[]
)

Example input and output:

Ticker symbol: TRIB
Value (short shares available indicator): 3.0
Easy to borrow. At least 1000 shares available.

References

https://interactivebrokers.github.io/tws-api/tick_types.html

 

Validate XML Syntax in the Command Line

This example shows how to parse and validate XML in the terminal using the command xmllint.
Note that this is testing if the document is well formed, i.e. has correct syntax, not validity according to a specific schema.

Input file: items.xml

<items>
  <item>
    <name>A</name>
  </item>
  <item>
    <name>B</name>
  </item>
</items>

Test the file:

$ xmllint items.xml

For a valid file, the output will be a copy of the entire file, without error messages:

<?xml version="1.0"?>
<items>
  <item>
    <name>A</name>
  </item>
  <item>
    <name>B</name>
  </item>
</items>

If there is a syntax error, as below, the line number will be reported.

Input file with error: items.xml

<items>
  <item>
    <name>A<name> <!-- improper close tag -->
  </item>
  <item>
    <name>B</name>
  </item>
</items>

The error output will look like this:

items.xml:4: parser error : 
Opening and ending tag mismatch: name line 3 and item
</item>
^
items.xml:8: parser error : 
Opening and ending tag mismatch: name line 3 and items
</items>
^
items.xml:11: parser error : 
Premature end of data in tag item line 2

^

 

Get the Top Percentage Gainers using the IBKR API in Python

This example shows how to get a list of the top gainers on the day in US stocks using the Interactive Brokers (IBKR) API for Python.

The API works by connecting to a locally running Trader Workstation (TWS) instance. Make sure TWS is up and running; both live trading a paper trading logins will work.

In TWS, we need to enable EClients in TWS under Global Configuration / API. This will allow clients from localhost to connect to TWS over HTTP.

Install the IBKR API package using:

$ pip install ibapi

We extend both EClient and EWrapper to inherit the functionality of reading the scanner feed.
We call the scannerData() method of the superclass to get the actual data. We can ignore most of the parameters to scannerData(), but need to include them to match the signature.

Note that we need to launch a listener in a thread in parallel to the main script.
We connect to the data feed, read some items and disconnect.

The following script will get the first ten of the top percentage gainers at the time it is run:

import time
import threading

from ibapi.client import EClient
from ibapi.scanner import ScannerSubscription
from ibapi.wrapper import EWrapper

# Class extending IBKR scanner base classes.
class IBapi(EWrapper, EClient):
  def __init__(self):
    EClient.__init__(self, self)

  def scannerData(self, reqId, rank, contractDetails, distance, benchmark, projection, legsStr):
    super().scannerData(reqId, rank, contractDetails, distance, benchmark, projection, legsStr)
    position = rank + 1 # Zero based indexing.
    print(f"{position}: {contractDetails.contract.symbol}")

  def scannerDataEnd(self, reqId):
    print("End of scanner data.")
    self.disconnect()

# Function to run continuously.
def ibApiRunLoop(ibApiApp):
  ibApiApp.run()

# Main script.

TWS_HOST = '127.0.0.1'
TWS_PORT = 7497
CLIENT_ID = 12345

ibApi = IBapi()
ibApi.connect(TWS_HOST, TWS_PORT, CLIENT_ID)

thread = threading.Thread(target=ibApiRunLoop, args=(ibApi,), daemon=True)
thread.start()

time.sleep(1) # Allow time for connection.

scannerSub = ScannerSubscription()
scannerSub.numberOfRows = 10
scannerSub.instrument='STK' # Stocks.
scannerSub.locationCode='STK.US.MAJOR' # US major exchanges.
scannerSub.scanCode='TOP_PERC_GAIN' # Top percentage gainers.

requestId = 1000 # An arbitrary request ID.
filters = [] # No special filters needed.

ibApi.reqScannerSubscription(requestId, scannerSub, filters, filters)

time.sleep(5) # Allow time to receive data.

ibApi.disconnect()

Example output:

1: LUCY
2: TNMG
3: DEVS
4: SRXH
5: SMCX
6: SMCL
7: LOBO
8: LGMK
9: HIMZ
10: SMCI
End of scanner data.

Note that to get more details, e.g. the last price of the tickers, we need to query data specific to each ticker.

References

https://www.interactivebrokers.com/campus/ibkr-quant-news/interactive-brokers-python-api-native-a-step-by-step-guide/

https://www.interactivebrokers.com/campus/ibkr-quant-news/implementing-market-scanners-using-tws-api-part-ii/

 

Get the Float Size of a Stock with Yahoo Finance Data

The following example shows how to retrieve the value of the float of a stock in Python using the Yahoo Finance (yfinance) library.

Note that there is no value returned specifically for Free Float, but Float Size should be equal or close enough for many purposes.

The following script retrieves the float size for any ticker symbol provided as input.

The float is under the Key Statistics (keyStats).

import yfinance as yf

ticker = input("Ticker symbol: ")

stock = yf.Ticker(ticker)

keyStats = stock.info

floatShares = keyStats.get("floatShares")

print("Float size: " + str(floatShares))

Example run:

Ticker symbol: AREB
Float size: 1026592

 

Scraping a Web Page with Python and BeautifulSoup

The following is a simple example showing how to scrape a web page in Python using the BeautifulSoup library.

The example web page we use is Hacker News:

https://news.ycombinator.com/

The goal of the example is to print just the headlines from the page.

Scraping and parsing a web page requires first understanding the HTML document structure of the target page.
In this case, the relevant portion of the HTML structure is roughly as follows:

<table>
 <tbody>
  <tr>
   ...
   <td>
    <span class="titleline">
     <a href="https://...">Headline</a>
    </span>
   </td>
   ...

Crucially, BeautifulSoup can find elements by many different criteria, regardless of depth of nesting.

One of the most useful techniques is searching by CSS class. In this case, we need to find anchor elements inside spans with the class “titleline”, inside table cells.

First, we create a BeautifulSoup object and use the find methods to drill down into the HTML elements in the DOM.

Note that findAll() finds a list of elements, while find() gets a single element.

The entire script is below.

scrape-example.py
from bs4 import BeautifulSoup
from urllib import request

url = "https://news.ycombinator.com/"

htmlPage = request.urlopen(url)

soup = BeautifulSoup(htmlPage, "html.parser")

# Find list of elements by CSS class.
spanList = soup.findAll(
  "span", 
  attrs={"class": "titleline"}
)

for span in spanList:
  anchor = span.find("a")
  headline = anchor.text
  print(headline)

Running the script:

$ python scrape-example.py

Example output of headlines:

The subtle art of designing physical controls for cars
WASM will replace containers
Backblaze Drive Stats for 2024

...

 

LLM Tool Use with LangChain and a Local Llama

Tool Use for LLMs is the idea of giving a language model the ability to take actions by executing external code.

LangChain is a framework which uses Chain-of-Thought (COT) prompting in order to generate steps for a plan of action and then actually carry out those steps.

A tool is defined as software functionality which can be triggered through matching a Tool class, which is associated with a function. The function code specifies the exact tool functionality.
In the simplest case the return value from a function reachable through a tool is a string.

In this example we use a local LLM running through Ollama: Llama 3.
One simple tool is provided: a function that gets the current time. Normally, an LLM will say it does not have access to a clock when asked the time in a prompt. This tool gives the model that ability.

Overall, the code does the following:

  • Define a function that returns the current time
  • Define time_tool as a Tool class with a good description of what the tool does and links to the time function

Note that matching the description is probabilistic and not 100% guaranteed. Hence, we make the description as complete and explicit as possible to increase the likelihood of matching. Note also that some models are better than others; LlaMa 3 performs quite well.

  • We link the LLM and the tools list together using LangChain’s initialize_agent()
  • Run a prompt asking for the current time. The model answering will trigger the use of the time tool, and the return value is used in the final result

The output is set to verbose, so we can see the actions planned and executed.

Before running the code, make sure Ollama is running and Llama 3 is available with:

$ ollama run llama3

Make sure the LangChain libraries required are installed:

$ pip install langchain

$ pip install langchain-community

$ pip install langchain-ollama

Note: at the time of writing using LangChain >= 0.3.x works well.
A specific version install may be required, for example:

$ pip install langchain==0.3.14

Here is the complete code:

from datetime import datetime
from langchain.agents import initialize_agent, AgentType
from langchain.tools import Tool
from langchain_community.chat_models import ChatOllama


def get_time(query: str):
  # So we can see when the tool code is reached:
  print("get_time() called.")
  return "The time is: " + datetime.now().strftime("%H:%M:%S") + "."

time_tool = Tool(
  name="TimeTool",
  description="Fetch time information. Able to get the current time.",
  func=get_time
)

tools = [time_tool]

llm = ChatOllama(model="llama3")

agent = initialize_agent(
  tools,
  llm,
  agent_type=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION,
  verbose=True,
  handle_parsing_errors=True
)

query = "What is the time?"

response = agent.invoke(query)

print(response)

Note that the output will not necessarily be consistent between runs.
The following is an example run:

> Entering new AgentExecutor chain...
Let's get started!

Question: What is the time?

Thought: Let's get started!

Thought: Since I need to find out what the current time is, 
I should use the TimeTool.

Action: TimeTool

Action Input: None needed for this tool, it just provides 
the current time.
get_time() called.

Observation: The time is: 22:42:02.

Thought:Final Answer: The time is: 22:41:59.

> Finished chain.
{'input': 'What is the time?', 
 'output': 'The time is: 22:41:59.'}

 

Determine if a Number is a Factorion in Python

A factorion is a number which equals the sum of the factorials of each of its digits.

For example: 145 is a factorion, because:

1! + 4! + 5! = 1 + 24 + 120 = 145

The following Python code will determine if a given number is a factorion.

import math

def isFactorion(n):
  digits = getDigits(n)

  digitSum = 0

  for digit in digits:
    digitSum += math.factorial(digit)

  return n == digitSum


# Helper function.
# Get a list of digits as integers for a number n.
def getDigits(n): 
  return [int(digitChar) for digitChar in list(str(n))]


# Examples.
if isFactorion(145):
  print("145 is a factorion")
else:
  print("145 is not a factorion")

if isFactorion(185):
  print("185 is a factorion")
else:
  print("185 is not a factorion")

Output for the two examples:

145 is a factorion
185 is not a factorion

 

Range of Characters in Python

Suppose we want to generate a range of characters, similar to how the range() function returns a list of numbers between a given range.
This can be easily achieved with special constants in the string module.

To get the range between ‘a’ and ‘z’, we can use string.ascii_lowercase.

For example:

import string

for char in string.ascii_lowercase:
  print(char)

Output:

a
b
c
...
z

To generate the range between ‘A’ and ‘Z’, we can use string.ascii_uppercase.

For example:

import string

for char in string.ascii_uppercase:
  print(char)

Output:

A
B
C
...
Z

The constants can be used like any list, e.g. for iteration or testing membership of an element.