Get Notified when a Public Company cuts their Dividend using Go + Polygon.io
We are going to write a basic script that detects when a company cuts a dividend. We will be using Go and a Polygon.io for financial data. You will need an account (Free to sign up). You can find this same lesson, but for Python here.
Polygon.io offers a Go client, we won’t be using it. We will just call REST API. For our script, we will take two arguments:
- Ticker (required)
- After Date (optional)
The Go call will look like:
POLYGON_API_KEY="YOUR_SECRET_KEY" go run ./ AAPL 2020-01-01
or,
POLYGON_API_KEY="YOUR_SECRET_KEY" go run ./ AAPL
Start by running:
go mod init
go mod tidy
Now, the script (below, split into parts and explained):
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
dateFormat = "2006-01-02"
)
type Dividend struct {
CashAmount float64 `json:"cash_amount"`
DeclarationDate string `json:"declaration_date"`
DividendType string `json:"dividend_type"`
ExDividendDate string `json:"ex_dividend_date"`
Frequency int `json:"frequency"`
PayDate string `json:"pay_date"`
RecordDate string `json:"record_date"`
Ticker string `json:"ticker"`
}
type DividendResponse struct {
Results []Dividend `json:"results"`
NextUrl string `json:"next_url,omitempty"`
Status string `json:"status"`
}
func getAllDividends(ticker string, initialDate time.Time) []Dividend {
previousMinute := -1
currentCount := 0
results := []Dividend{}
url := buildUrl(ticker, initialDate)
for true {
currentMinute := time.Now().Minute()
if currentMinute != previousMinute {
// Reset the rate limit counter
previousMinute = currentMinute
currentCount = 0
}
if currentCount >= 5 {
// Wait until the next minute
time.Sleep(time.Second * 10)
continue
}
currentCount++ // Increase the rate limiter counter
pageOfResults, nextURL, rateLimited := getPageOfResults(url)
if rateLimited {
currentCount = 5 // Force a wait
time.Sleep(time.Second * 1)
continue
}
url = nextURL
results = append(results, pageOfResults...)
if len(pageOfResults) == 0 {
break
}
}
return results
}
func buildUrl(ticker string, initialDate time.Time) string {
url := "https://api.polygon.io/v3/reference/dividends?ticker=" + ticker
url += "&apiKey=" + getPolygonApiKey()
url += "&limit=1000"
url += "&ex_dividend_date.gte=" + initialDate.Format(dateFormat)
url += "&sort=ex_dividend_date"
url += "&order=asc"
return url
}
func getPageOfResults(url string) ([]Dividend, string, bool) {
if url == "" {
// The next url came back empty, so we're done
return []Dividend{}, "", false
}
// Fetch the data
client := &http.Client{
Timeout: time.Second * 30,
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Printf("Error creating request: %v\n", err)
os.Exit(1)
}
resp, err := client.Do(req)
defer resp.Body.Close()
if resp.StatusCode == 429 {
// Rate limited
return []Dividend{}, "", true
}
if resp.StatusCode != 200 {
fmt.Printf("Error fetching data: %v :: %+v\n", resp.Status, err)
os.Exit(1)
}
response, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response: %v\n", err)
os.Exit(1)
}
var data DividendResponse
err = json.Unmarshal(response, &data)
if err != nil {
fmt.Printf("Error parsing response: %v\n", err)
os.Exit(1)
}
return data.Results, data.NextUrl, false
}
func getPolygonApiKey() string {
apiKey := os.Getenv("POLYGON_API_KEY")
if apiKey == "" {
fmt.Printf("POLYGON_API_KEY environment variable not set\n")
os.Exit(1)
}
return apiKey
}
func main() {
// Get the arguments
if len(os.Args) < 2 {
fmt.Printf("Usage: go run main.go <ticker>\n")
os.Exit(1)
}
ticker := os.Args[1]
ticker = strings.ToUpper(ticker)
fmt.Printf("Ticker : %s\n", ticker)
// Get the second argument if it exists
initialDate := time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC)
if len(os.Args) > 2 {
parseDate, err := time.Parse(dateFormat, os.Args[2])
if err == nil {
initialDate = parseDate
}
}
fmt.Printf("Initial Date: %s\n", initialDate.Format(dateFormat))
// Get the dividends
allDividends := getAllDividends(ticker, initialDate)
previousCashAmount := 0.0
cuts := 0
for _, dividend := range allDividends {
if dividend.DividendType != "CD" {
// Not a consistent dividend
continue
}
if dividend.CashAmount < previousCashAmount {
// This dividend is less than the previous one
fmt.Printf("Dividend cut detected: %s\n", dividend.ExDividendDate)
cuts++
}
previousCashAmount = dividend.CashAmount
}
fmt.Printf("Done, total cuts :: %d\n", cuts)
}
Above is the entire script, and below I’ve split it into parts. Let’s start with the imports:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
The majority is standard packages. Next, the constants:
const (
dateFormat = "2006-01-02"
)
As mentioned, the second optional argument is the initial date. If not provided, we go as far as January 1, 1980. I did this because it decreases the amount of data being pulled. I think 40 years is enough.
Next, the models for the response on the API.
type Dividend struct {
CashAmount float64 `json:"cash_amount"`
DeclarationDate string `json:"declaration_date"`
DividendType string `json:"dividend_type"`
ExDividendDate string `json:"ex_dividend_date"`
Frequency int `json:"frequency"`
PayDate string `json:"pay_date"`
RecordDate string `json:"record_date"`
Ticker string `json:"ticker"`
}
type DividendResponse struct {
Results []Dividend `json:"results"`
NextUrl string `json:"next_url,omitempty"`
Status string `json:"status"`
}
Let’s look at the code to make the request against Polygon. We are calling the dividends endpoint.
func getAllDividends(ticker string, initialDate time.Time) []Dividend {
previousMinute := -1
currentCount := 0
results := []Dividend{}
url := buildUrl(ticker, initialDate)
for true {
currentMinute := time.Now().Minute()
if currentMinute != previousMinute {
// Reset the rate limit counter
previousMinute = currentMinute
currentCount = 0
}
if currentCount >= 5 {
// Wait until the next minute
time.Sleep(time.Second * 10)
continue
}
currentCount++ // Increase the rate limiter counter
pageOfResults, nextURL, rateLimited := getPageOfResults(url)
if rateLimited {
currentCount = 5 // Force a wait
time.Sleep(time.Second * 1)
continue
}
url = nextURL
results = append(results, pageOfResults...)
if len(pageOfResults) == 0 {
break
}
}
return results
}
func buildUrl(ticker string, initialDate time.Time) string {
url := "https://api.polygon.io/v3/reference/dividends?ticker=" + ticker
url += "&apiKey=" + getPolygonApiKey()
url += "&limit=1000"
url += "&ex_dividend_date.gte=" + initialDate.Format(dateFormat)
url += "&sort=ex_dividend_date"
url += "&order=asc"
return url
}
func getPageOfResults(url string) ([]Dividend, string, bool) {
if url == "" {
// The next url came back empty, so we're done
return []Dividend{}, "", false
}
// Fetch the data
client := &http.Client{
Timeout: time.Second * 30,
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Printf("Error creating request: %v\n", err)
os.Exit(1)
}
resp, err := client.Do(req)
defer resp.Body.Close()
if resp.StatusCode == 429 {
// Rate limited
return []Dividend{}, "", true
}
if resp.StatusCode != 200 {
fmt.Printf("Error fetching data: %v :: %+v\n", resp.Status, err)
os.Exit(1)
}
response, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response: %v\n", err)
os.Exit(1)
}
var data DividendResponse
err = json.Unmarshal(response, &data)
if err != nil {
fmt.Printf("Error parsing response: %v\n", err)
os.Exit(1)
}
return data.Results, data.NextUrl, false
}
func getPolygonApiKey() string {
apiKey := os.Getenv("POLYGON_API_KEY")
if apiKey == "" {
fmt.Printf("POLYGON_API_KEY environment variable not set\n")
os.Exit(1)
}
return apiKey
}
The first aspect of this is the rate limiting. I have a free account and I’m limited to 5 requests per minute. The code starts by checking if the current minute is different than the one in previousMinute. If so, it resets the count. Otherwise, we check the count. If count is greater than or equal to 5, wait 10 seconds. This could be less, but most companies will require one call.
We then make the request. We get three arguments back, the results (list of dividend data), the next url if there’s more pages, or we were rate limited (and need to wait).
Why the rate limit check again? Technically, we could use our rate limit in multiple spots and be throttled. As of right now, this code could scale horizontally across multiple computer but we are storing the count in shared spot.
We move into the next function: buildUrl . This is only called for the initial url. Polygon returns a full URL in the response to use in the next API call. I determined the majority of these values by looking at the API docs. The sort matters because we are looking if dividends have been cut (reversing would mess up the logic).
Finally, getPageOfResults function. This is making an API call, extracting some response values or checking if the HTTP status code returned a non-200.
This would set us up with all dividend information. Now, let’s look at the main function.
func main() {
// Get the arguments
if len(os.Args) < 2 {
fmt.Printf("Usage: go run main.go <ticker>\n")
os.Exit(1)
}
ticker := os.Args[1]
ticker = strings.ToUpper(ticker)
fmt.Printf("Ticker : %s\n", ticker)
// Get the second argument if it exists
initialDate := time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC)
if len(os.Args) > 2 {
parseDate, err := time.Parse(dateFormat, os.Args[2])
if err == nil {
initialDate = parseDate
}
}
fmt.Printf("Initial Date: %s\n", initialDate.Format(dateFormat))
// Get the dividends
allDividends := getAllDividends(ticker, initialDate)
previousCashAmount := 0.0
cuts := 0
for _, dividend := range allDividends {
if dividend.DividendType != "CD" {
// Not a consistent dividend
continue
}
if dividend.CashAmount < previousCashAmount {
// This dividend is less than the previous one
fmt.Printf("Dividend cut detected: %s\n", dividend.ExDividendDate)
cuts++
}
previousCashAmount = dividend.CashAmount
}
fmt.Printf("Done, total cuts :: %d\n", cuts)
}
As you can see, we just iterate through the dividends checking or a cut on the cash amount.
Below is a screenshot of me running the script:

In terms of notifying you, I won’t demonstrate that part. You can setup a simple Slackbot via webhook, or email with Sendgrid.
Thanks for reading! You can make this code much more efficient, but I chose to keep it simple.