Skip to content

Instantly share code, notes, and snippets.

@SIRHAMY
Created October 21, 2025 11:45
Show Gist options
  • Select an option

  • Save SIRHAMY/0983f5b46c5505df8864e739f4d0bd76 to your computer and use it in GitHub Desktop.

Select an option

Save SIRHAMY/0983f5b46c5505df8864e739f4d0bd76 to your computer and use it in GitHub Desktop.
open FsHttp
open System
open System.Collections
open System.Collections.Generic
open System.Linq
open System.Text.Json
// For more information see https://aka.ms/fsharp-console-apps
printfn "Hello from F#"
let marketDataToken = "TODO_YOUR_TOKEN_HERE"
(*
Strategy:
* Companies I see have great reps in TECH
* Good products
* Good business strategy
* Work long-term
*)
let stockTickers =
[
// "AAPL" // Free one for MarketData
// "TEAM" // Atlassian
// "NET" // CloudFlare
// "DDOG" // DataDog
// "DOCN" // Digital Ocean
// "GOOG" // Google
// "MSFT" // Microsoft
// "VOO" // Vanguard SP500
"JOBY"
"WBD"
]
let expirationYearsToConsider =
[
2024
2025
]
let createDate
(year: int)
(month: int)
(day: int)
=
new DateTimeOffset(DateTime(year, month, day), TimeSpan())
let findFirstFriday
(year: int)
(month: int)
: DateTimeOffset
=
let firstFridayDay =
seq {1 .. 10} // By def, friday will be in first 10 days
|> Seq.find (
fun dayIndex ->
let date = createDate year month dayIndex
date.DayOfWeek = DayOfWeek.Friday
)
createDate year month firstFridayDay
let findAllOptionExpirationDaysInYear
(year: int)
: DateTimeOffset seq
=
(*
Options expiration is third friday of month
*)
let allThirdFridays =
seq { 1 .. 12 }
|> Seq.map (
fun month -> findFirstFriday year month
)
|> Seq.map (
fun firstFriday -> firstFriday.AddDays(14)
)
allThirdFridays
let expirationDates =
let nowish = DateTimeOffset.UtcNow.AddDays(5)
expirationYearsToConsider
|> List.map (
fun expirationYear -> findAllOptionExpirationDaysInYear expirationYear
)
|> List.collect (
fun expirationDates ->
expirationDates
|> Seq.toList
)
|> List.filter (
fun expirationDate ->
expirationDate > nowish
)
printfn "All ExpirationDates: %A" expirationDates
type MarketDataOptionChainApiResult =
{
optionSymbol : string List
underlying : string List
expiration : int List
strike: float List
dte: int List
bid: float List
bidSize: float List
underlyingPrice: float List // asset price
updated: int List // Last updated time in unix seconds
volume: int List
}
type MarketDataOptionChainResult =
{
optionSymbol : string
underlying : string
expiration : int
strike: float
dte: int
bid: float
bidSize: float
underlyingPrice: float // asset price
Volume: int
LastUpdated: DateTimeOffset
}
type StockPick<'a> =
{
ReturnOnRiskPerYearPercent : float
AbsoluteReturn : float
EffectivePrice: float
EffectiveDiscount: float
Expiration : DateTimeOffset
Bid : float
Strike : float
Data : 'a
}
let createMarketDataOptionChainUrl
(stockSymbol : string)
(expirationDate: DateTimeOffset)
=
let usableDate = expirationDate.ToString("yyyy-MM-dd")
$"https://api.marketdata.app/v1/options/chain/{stockSymbol}/?side=put&range=otm&minVolume=1&expiration={usableDate}"
let fetchMarketDataOptionChainApi
(stockSymbol : string)
(expirationDate: DateTimeOffset)
: MarketDataOptionChainApiResult option
=
let serializerOptions: JsonSerializerOptions =
(JsonSerializerOptions())
serializerOptions.PropertyNameCaseInsensitive <- true
http {
GET (createMarketDataOptionChainUrl stockSymbol expirationDate)
AuthorizationBearer marketDataToken
}
|> Request.send
|> Response.toJson
|> fun json ->
try
let deserializedJson =
JsonSerializer.Deserialize<MarketDataOptionChainApiResult>(json, serializerOptions)
// ham: Necessary cause sometimes we get bad returns w unexpected null values
match deserializedJson.bid with
| null ->
printfn $"No data available for stock:{stockSymbol} expiration:{expirationDate}"
None
| _ -> Some deserializedJson
with ex ->
printfn $"An error occurred fetching for stock:{stockSymbol} expiration:{expirationDate}"
None
let convertMarketDataOptionChainApiResultToResult
(apiResult: MarketDataOptionChainApiResult option)
: MarketDataOptionChainResult seq
=
if apiResult.IsNone
then Seq.empty
else
let apiResultValue = apiResult.Value
let length = apiResultValue.bid.Count
// printfn "apiResultValue: %A" apiResultValue
// printfn "length: %A" length
Enumerable.Range(0, length - 1)
|> Seq.map (
fun i ->
{
optionSymbol = apiResultValue.optionSymbol.[i]
underlying = apiResultValue.underlying.[i]
expiration = apiResultValue.expiration.[i]
strike = apiResultValue.strike.[i]
dte = apiResultValue.dte.[i]
bid = apiResultValue.bid.[i]
bidSize = apiResultValue.bidSize.[i]
underlyingPrice = apiResultValue.underlyingPrice.[i]
Volume = apiResultValue.volume.[i]
LastUpdated = DateTimeOffset.FromUnixTimeSeconds(apiResultValue.updated.[i])
}
)
let calculateOptionsRewards
(optionsChains: MarketDataOptionChainResult seq)
: StockPick<MarketDataOptionChainResult> seq
=
let calculateRiskOverRewardYearly
(bid: float)
(stockPrice: float)
(daysToExpire: int)
=
let expiryMonths = ((float)daysToExpire) / 30.0
let monthsToYear = 12.0 / expiryMonths
let percentEarned = (bid / stockPrice)
percentEarned * monthsToYear
optionsChains
|> Seq.choose (
fun chain ->
let riskOverRewardYearly =
calculateRiskOverRewardYearly
chain.bid
chain.strike
chain.dte
let effectivePrice = (chain.strike - chain.bid)
Some (
{
ReturnOnRiskPerYearPercent = riskOverRewardYearly
AbsoluteReturn = (chain.bid / chain.strike)
EffectiveDiscount = (effectivePrice / chain.underlyingPrice) - 1.0
EffectivePrice = effectivePrice
Expiration = DateTimeOffset.FromUnixTimeSeconds(chain.expiration)
Bid = chain.bid
Strike = chain.strike
Data = chain
}
)
)
printfn "# Update: Fetching Options Chains"
let results =
stockTickers
|> List.map (
fun stock ->
expirationDates
|> List.map (
fun y -> fetchMarketDataOptionChainApi stock y
)
)
|> List.collect (fun l -> l)
|> List.map (
fun apiResult -> convertMarketDataOptionChainApiResultToResult apiResult
)
|> List.toSeq
|> Seq.collect (fun s -> s)
printfn "# Update: Calculating Results"
let allResults =
results
|> fun s -> calculateOptionsRewards s
printfn "## All Results"
allResults
|> Seq.iter (fun r -> printfn "* %A" r)
|> ignore
printfn "# Update: Choosing Results"
allResults
|> Seq.filter (
fun r ->
(*
Looking for long-term holds:
* Big discount (30%)
* Good return on money (~10%)
*)
(
// At least 5p return on money
(r.ReturnOnRiskPerYearPercent > 0.05)
// At least 5p return on money to be worth risk (I get half after tax)
&& r.AbsoluteReturn > 0.05
&& r.EffectiveDiscount < -0.25)
(*
Strategy: Cash-good returns:
* High return on risk
* Decent absolute return
*)
|| (
// At least 20p return on year to be good deal
(r.ReturnOnRiskPerYearPercent > 0.25)
// At least 5p return on money to be worth risk (I get half after tax)
&& r.AbsoluteReturn > 0.05)
)
|> Seq.toList
|> List.sortByDescending (fun s -> s.ReturnOnRiskPerYearPercent)
|> Seq.iter (
fun r -> printfn "GoodPick: %A" r
)
|> ignore
let now = DateTimeOffset.UtcNow
let oldestData =
allResults
|> Seq.map (fun r -> r.Data.LastUpdated)
|> Seq.min
printfn "OldestData: %A" oldestData
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment