├── .gitignore ├── Makefile ├── README.md ├── SampleBot.sln ├── SampleBot ├── Program.fs └── SampleBot.fsproj ├── TradingLib ├── Adapters │ ├── Alpaca.fs │ └── Gemini.fs ├── Base.fs ├── Buffer.fs ├── Common.fs ├── Data.fs ├── Execution.fs ├── Numbers.fs ├── Socket.fs ├── TradingLib.fsproj └── Utils.fs └── build.bat /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artificats 2 | bin/ 3 | obj/ 4 | [Dd]ebug/ 5 | [Rr]elease/ 6 | *.bin 7 | *.exe 8 | *.pdb 9 | x86/ 10 | x64/ 11 | 12 | 13 | # IDE files 14 | .vs/ 15 | .idea/ 16 | 17 | 18 | # Other non-useful files 19 | *.log 20 | *.jpg 21 | *.ps 22 | *.pdf 23 | Temp/ 24 | *.pkl 25 | 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | app=SampleBot 2 | version=net8.0 3 | os=$(shell uname -s) 4 | sources=$(wildcard */*.fs */*/*.fs */*.fsproj *.sln) 5 | 6 | ifeq ($(os), Linux) 7 | runtime=linux-x64 8 | inp= 9 | out=.bin 10 | endif 11 | 12 | ifeq ($(os), Darwin) 13 | runtime=osx-x64 14 | inp= 15 | out=.bin 16 | endif 17 | 18 | ifeq ($(shell uname -o), Cygwin) 19 | runtime=win-x64 20 | inp=.exe 21 | out=.exe 22 | endif 23 | 24 | 25 | debug: ${sources} 26 | dotnet publish -c Debug -r ${runtime} -p:PublishSingleFile=true \ 27 | --self-contained true ${app}/${app}.fsproj 28 | cp ./${app}/bin/Debug/${version}/${runtime}/publish/${app}${inp} ${app}${out} 29 | 30 | release: ${sources} 31 | dotnet publish -c Release -r ${runtime} -p:PublishSingleFile=true \ 32 | --self-contained true ${app}/${app}.fsproj 33 | cp ./${app}/bin/Release/${version}/${runtime}/publish/${app}${inp} ${app}${out} 34 | 35 | print.ps: ${sources} 36 | a2ps -o $@ --font-size=10 -R --columns=1 $^ 37 | 38 | print.pdf: print.ps 39 | ps2pdf -o $@ $^ 40 | 41 | clean: 42 | rm -rf */bin */obj *.bin *.exe *.log print.pdf print.ps 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is Open Source project which aims to produce a framework for Trading Bots for F#. The slides describing this project is available at [here](https://www.nikhilbarthwal.com/Trading.pdf). 2 | 3 | Contributions are welcome! 4 | 5 | 6 | 7 | Nikhil Barthwal 8 | 9 | [nikhilbarthwal@hotmail.com](mailto:nikhilbarthwal@hotmail.com) 10 | -------------------------------------------------------------------------------- /SampleBot.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "SampleBot", "SampleBot\SampleBot.fsproj", "{BB3A095D-44D8-4D58-92A6-FC2BB8BAEF21}" 3 | EndProject 4 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TradingLib", "TradingLib\TradingLib.fsproj", "{C3102B36-7156-4EF1-ADA0-BA0193C75C20}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Release|Any CPU = Release|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {BB3A095D-44D8-4D58-92A6-FC2BB8BAEF21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {BB3A095D-44D8-4D58-92A6-FC2BB8BAEF21}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {BB3A095D-44D8-4D58-92A6-FC2BB8BAEF21}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {BB3A095D-44D8-4D58-92A6-FC2BB8BAEF21}.Release|Any CPU.Build.0 = Release|Any CPU 16 | {C3102B36-7156-4EF1-ADA0-BA0193C75C20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {C3102B36-7156-4EF1-ADA0-BA0193C75C20}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {C3102B36-7156-4EF1-ADA0-BA0193C75C20}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {C3102B36-7156-4EF1-ADA0-BA0193C75C20}.Release|Any CPU.Build.0 = Release|Any CPU 20 | EndGlobalSection 21 | EndGlobal 22 | -------------------------------------------------------------------------------- /SampleBot/Program.fs: -------------------------------------------------------------------------------- 1 | // This example is for illustration purposes only. 2 | 3 | namespace SampleBot 4 | 5 | open TradingLib 6 | open System 7 | 8 | type DoubleMovingAverage(short: int, long: int, quantity: uint) = 9 | do assert (long > short) 10 | member this.Size = long 11 | interface Strategy with 12 | member this.Execute(ticker, data): Maybe = 13 | assert (data.Size >= long) 14 | let price = data[0].Price 15 | let sum (s: float) (i:int): float = s + data[i-1].Price 16 | let longAvg = List.fold sum 0.0 [1 .. long] 17 | let shortAvg = List.fold sum 0.0 [1 .. short] 18 | if shortAvg <= longAvg * 1.01 then No else Yes <| Order.Entry({| 19 | Ticker = ticker 20 | Quantity = quantity 21 | Price = price 22 | Profit = price * 1.01 23 | Loss = price * 0.97 |}) 24 | 25 | module Program = 26 | 27 | type demo() = 28 | let size = 100 29 | let apiKey = Environment.GetEnvironmentVariable("ALPACA_API_KEY") 30 | let apiSecret = Environment.GetEnvironmentVariable("ALPACA_API_SECRET") 31 | let strategy = DoubleMovingAverage(size/2, size, 10u) 32 | let alpaca = Alpaca(apiKey, apiSecret) :> Client 33 | let buffer = Buffer.Linear(5, 5) 34 | let source = Gemini.Source({| Tickers = [Crypto("BTC") ; Crypto("ETH")] 35 | Size = size 36 | Buffer = buffer 37 | AskBidDifference = 0.5 38 | Timeout = 15 |}) 39 | interface Execution with 40 | member this.Welcome: string = "This is a Sample" 41 | member this.StartTime = No 42 | member this.EndTime = No 43 | member this.InitialCapital = 100.0 * 1000.0 44 | member this.TargetProfit = 50.0 * 1000.0 45 | member this.StopLoss = 20.0 * 1000.0 46 | member this.Client(): Client = alpaca 47 | member this.Strategy() = strategy 48 | member this.Source() = source 49 | member this.Delay = 5 50 | member this.Execute(order) = alpaca.PlaceOrder(order) |> ignore 51 | Utils.Wait(15) 52 | 53 | [] 54 | let main _: int = if (let x = demo() in Execution.Run(x)) then 1 else 0 55 | -------------------------------------------------------------------------------- /SampleBot/SampleBot.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | SampleBot 7 | SampleBot 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /TradingLib/Adapters/Alpaca.fs: -------------------------------------------------------------------------------- 1 | namespace TradingLib 2 | 3 | open Alpaca.Markets 4 | 5 | 6 | type Alpaca(apiKey: string, apiSecret: string) = 7 | let secret = SecretKey(apiKey, apiSecret) 8 | let client = Environments.Paper.GetAlpacaTradingClient(secret) 9 | let error (ticker: Ticker) = 10 | $"Currently, only Crypto is supported for Alpaca, not {ticker}" 11 | 12 | let accountInfo() = 13 | let result = client.GetAccountAsync().Result 14 | if result.IsTradingBlocked then 15 | Log.Error("Alpaca", "Trading for this account is blocked") 16 | 17 | let total = if result.BuyingPower.HasValue then 18 | float <| result.BuyingPower.Value 19 | else 20 | Log.Error("Alpaca", "Unable to get total account value") 21 | 22 | let profit = if result.Equity.HasValue then 23 | float <| result.Equity.Value - result.LastEquity 24 | else 25 | Log.Error("Alpaca", "Unable to get account equity value") 26 | 27 | { Total = total ; Profit = profit } 28 | 29 | let placeOrder(order: Order.Entry): IOrder = 30 | try 31 | let symbol = match order.Ticker with 32 | | Crypto(sym) -> sym + "USD" 33 | | ticker -> Log.Error("Alpaca", error ticker) 34 | 35 | let quantity: OrderQuantity = OrderQuantity.FromInt64(order.Quantity) 36 | let takeProfitLimitPrice = decimal <| order.Profit 37 | let stopLossStopPrice = decimal <| order.Loss 38 | let task = client.PostOrderAsync(LimitOrder.Buy(symbol, quantity, 39 | decimal <| order.Price).WithDuration( 40 | TimeInForce.Day).Bracket(takeProfitLimitPrice, stopLossStopPrice)) 41 | task.RunSynchronously() 42 | task.Result 43 | with e -> Log.Exception("Alpaca", e.Message) e 44 | 45 | let cancelOrder(id: System.Guid): bool = 46 | try 47 | let task = client.CancelOrderAsync(id) 48 | task.RunSynchronously() 49 | task.Result 50 | with e -> Log.Exception("Alpaca", e.Message) e 51 | 52 | let orderStatus(id: System.Guid): Order.Status = 53 | try 54 | let task = client.GetOrderAsync(id) 55 | task.RunSynchronously() 56 | let iOrder = task.Result 57 | if iOrder.CancelledAtUtc.HasValue then Order.Status.Cancelled else 58 | if iOrder.FilledAtUtc.HasValue then Order.Status.Executed else 59 | if iOrder.FilledQuantity > 0.0m then Order.Status.Triggered else 60 | Order.Status.Placed 61 | with e -> Log.Exception("Alpaca", e.Message) e 62 | 63 | interface Client with 64 | member this.AccountInfo(): AccountInfo = accountInfo() 65 | member this.PlaceOrder(order): System.Guid = placeOrder(order).OrderId 66 | member this.CancelOrder(id): bool = cancelOrder(id) 67 | member this.OrderStatus(id): Order.Status = orderStatus(id) 68 | -------------------------------------------------------------------------------- /TradingLib/Adapters/Gemini.fs: -------------------------------------------------------------------------------- 1 | namespace TradingLib 2 | 3 | open System.Text.Json 4 | 5 | 6 | module Gemini = 7 | 8 | type private Parser(tag: string, difference: float) = 9 | 10 | let mutable bestAsk: Maybe = No 11 | let mutable bestBid: Maybe = No 12 | 13 | let processEvent (event: JsonElement): unit = 14 | if (event.GetProperty("type").GetString() = "change" && 15 | event.GetProperty("reason").GetString() = "place") then 16 | 17 | let price: float = float <| event.GetProperty("price").GetString() 18 | let side: string = event.GetProperty("side").GetString() 19 | 20 | if side = "ask" then 21 | match bestAsk with 22 | | Yes(ask) -> if price < ask then bestAsk <- Yes(price) 23 | | No -> bestAsk <- Yes(price) 24 | 25 | if side = "bid" then 26 | match bestBid with 27 | | Yes(bid) -> if price > bid then bestBid <- Yes(price) 28 | | No -> bestBid <- Yes(price) 29 | 30 | let processMessage (json: JsonElement): unit = 31 | if json.GetProperty("socket_sequence").GetInt64() > 0 then 32 | for k in [1 .. json.GetProperty("events").GetArrayLength()] do 33 | processEvent <| json.GetProperty("events").Item(k - 1) 34 | 35 | let insert (ask: float) (bid: float) (ingest: Bar -> unit) 36 | (bar: float -> Bar): unit = 37 | 38 | if bid >= ask then (ingest <| bar bid) else 39 | if ((100.0 * (ask - bid)) / bid) < difference then 40 | ingest <| bar ((ask + bid) / 2.0) 41 | 42 | let getBar (json: JsonElement) (price : float): Bar = 43 | bestAsk <- No ; bestBid <- No 44 | Bar {| Open = price 45 | High = price 46 | Low = price 47 | Close = price 48 | Time = json.GetProperty("timestamp").GetInt64() 49 | Volume = -1 |} 50 | 51 | let parse(message: string) (ingest: Bar -> unit): unit = 52 | let json: JsonElement = JsonDocument.Parse(message).RootElement 53 | processMessage json 54 | match bestAsk, bestBid with 55 | | Yes(ask), Yes(bid) -> insert ask bid ingest <| getBar json 56 | | _ -> () 57 | 58 | member this.Parse(message: string, ingest: Bar -> unit): unit = 59 | try (parse message ingest) with e -> 60 | Log.Error(tag, $"Unable to parse {message} -> {e.Message}") 61 | 62 | 63 | let Source(z: {| 64 | Tickers: Ticker list 65 | Size: int 66 | Buffer: Buffer 67 | AskBidDifference: float 68 | Timeout: int |}) = 69 | 70 | let symbol (ticker: Ticker): string = 71 | match ticker with 72 | | Crypto(symbol) -> symbol 73 | | _ -> Log.Error("Gemini", $"Gemini only supports Crypto, not {ticker}") 74 | 75 | let symbols = Utils.CreateDictionary(z.Tickers, symbol) 76 | let url ticker = $"wss://api.gemini.com/v1/marketdata/{symbols[ticker]}USD" 77 | let tags = Utils.CreateDictionary(z.Tickers, 78 | fun ticker -> $"Gemini[{symbols[ticker]}]") 79 | let parser ticker = Parser(tags[ticker], z.AskBidDifference) 80 | let parsers = Utils.CreateDictionary(z.Tickers, parser) 81 | 82 | let reconnect(ticker: Ticker, msg: string) = 83 | Log.Warning(tags[ticker], $"Reconnecting for {symbols[ticker]} -> {msg}") 84 | 85 | Socket.Source.Multi({new Socket.Adapter.Multi with 86 | member this.Tickers = z.Tickers 87 | member this.Timeout = z.Timeout 88 | member this.Start _ = () 89 | member this.Buffer = z.Buffer 90 | member this.Size = z.Size 91 | member this.Url(ticker) = url(ticker) 92 | member this.Tag(ticker) = tags[ticker] 93 | member this.Reconnect(ticker, msg) = reconnect(ticker, msg) 94 | member this.Send(_, _) = () 95 | member this.Dispose() = () 96 | member this.Receive(ticker, msg, insert) = 97 | parsers[ticker].Parse(msg, insert) }) 98 | -------------------------------------------------------------------------------- /TradingLib/Base.fs: -------------------------------------------------------------------------------- 1 | namespace TradingLib 2 | 3 | open System 4 | 5 | 6 | [] type OptionType = Call | Put 7 | with override this.ToString() = match this with Call -> "Call" | Put -> "Put" 8 | 9 | 10 | type Ticker = 11 | | Stock of Symbol: string 12 | | Option of Symbol: string * Strike: float * Expiry: DateTime * Type: OptionType 13 | | Crypto of Symbol: string 14 | with 15 | override this.ToString() = 16 | match this with 17 | | Stock(symbol) -> 18 | $"Symbol: {symbol}" 19 | | Option(symbol, strike, expiry, direction) -> 20 | let str = expiry.ToString("yyyy-MM-dd") 21 | $"Symbol: {direction} {symbol} / Strike: {strike} / Expiry: {str}" 22 | | Crypto(symbol) -> 23 | $"Symbol: {symbol}" 24 | 25 | member this.Symbol = 26 | match this with 27 | | Stock(symbol) -> symbol 28 | | Option(symbol, _, _, _) -> symbol 29 | | Crypto(symbol) -> symbol 30 | 31 | 32 | [] type AccountInfo = { Total: float ; Profit: float } 33 | 34 | 35 | module Order = 36 | 37 | [] 38 | type Entry (param: struct {| Ticker: Ticker; Quantity: uint; Price: float 39 | Profit: float; Loss: float |}) = 40 | 41 | member this.Ticker = param.Ticker 42 | member this.Quantity: int = int param.Quantity 43 | member this.Price = assert (param.Price > 0) ; Utils.Normalize(param.Price) 44 | member this.Profit = Utils.Normalize(param.Profit) 45 | member this.Loss = Utils.Normalize(param.Loss) 46 | member this.ProfitPercent() = (100.0 * (this.Profit - this.Price))/this.Price 47 | member this.LossPercent() = (100.0 * (this.Price - this.Loss))/this.Price 48 | with override this.ToString() = 49 | $"Ticker: {this.Ticker} / Quantity: {this.Quantity} / Price: " + 50 | $"{this.Price} / ProfitPrice: {this.Profit} / LossPrice: {this.Loss}" 51 | 52 | [] type Status = Placed | Triggered | Executed | Cancelled 53 | 54 | 55 | [] //TODO: This should be heap based, not struct 56 | type Bar (param: struct {| Open: float; High: float; Low: float 57 | Close: float; Time: time; Volume: int64 |}) = 58 | 59 | member this.Open = assert (param.Low <= param.Open) 60 | assert (param.High >= param.Open) 61 | Utils.Normalize(param.Open) 62 | 63 | member this.Close = assert (param.Low <= param.Close) 64 | assert (param.High >= param.Close) 65 | Utils.Normalize(param.Open) 66 | 67 | member this.High = assert (param.Low <= param.High) ; Utils.Normalize(param.High) 68 | member this.Low = assert (param.Low <= param.High) ; Utils.Normalize(param.Low) 69 | member this.Epoch = param.Time 70 | member this.Volume = param.Volume 71 | member this.Timestamp = Utils.ToDateTime(param.Time) 72 | member this.Price = Utils.Normalize((this.High + this.Low) / 2.0) 73 | override this.ToString() = 74 | let ts = Utils.Ascii <| this.Timestamp.ToString("F") 75 | $"Open: {this.Open} / High: {this.High} / Close: {this.Close} / Low: " + 76 | $"{this.Low} / Timestamp: {ts} / Epoch: {this.Epoch} / Volume: {this.Volume}" 77 | 78 | 79 | type Client<'T> = 80 | abstract AccountInfo: unit -> AccountInfo 81 | abstract CancelOrder: 'T -> bool 82 | abstract OrderStatus: 'T -> Order.Status 83 | abstract PlaceOrder: Order.Entry -> 'T 84 | 85 | 86 | type Strategy = abstract Execute: Ticker * Vector -> Maybe 87 | -------------------------------------------------------------------------------- /TradingLib/Buffer.fs: -------------------------------------------------------------------------------- 1 | namespace TradingLib 2 | 3 | 4 | module Buffer = 5 | 6 | type Ingest = abstract member Append: Bar -> bool 7 | 8 | type private Bucket() = 9 | let mutable data = Bar() 10 | let mutable count = 0 11 | let merge (b: Bar) = 12 | let pF = float count 13 | let tF = float <| count + 1 14 | let pL = int64 count 15 | let tL = int64 <| count + 1 16 | let avgFloat (p, c) = (pF * p + c) / tF 17 | let avgLong (p, c) = (pL * p + c) / tL 18 | Bar <| {| Open = avgFloat(data.Open, b.Open) 19 | Close = avgFloat(data.Close, b.Close) 20 | High = max data.High b.High 21 | Low = min data.Low b.Low 22 | Time = avgLong(data.Epoch, b.Epoch) 23 | Volume = avgLong(data.Volume, b.Volume) |} 24 | 25 | member this.Data = assert (count > 0) ; data 26 | member this.Reset() = data <-Bar() ; count <- 0 27 | member this.Count = count 28 | member this.Add(x: Bar) = 29 | if count > 0 then (data <- merge x) else (data <- x) 30 | count <- count + 1 31 | 32 | type private Buckets(size: int) = 33 | let mutable pos = 0 34 | let buckets = [| for _ in [1 .. size] do Bucket() |] 35 | let index k = (pos + k) % size 36 | do assert (size > 1) 37 | 38 | member this.Item with get(k: int) = buckets[index(k)] 39 | member this.Previous() = buckets[pos].Data.Epoch 40 | member this.Shift(k) = 41 | assert (k > 0) 42 | for i in [1 .. k] do buckets[index <| i-1].Reset() 43 | pos <- (pos + k) % size 44 | 45 | member this.Reset() = 46 | pos <- 0 ; for i in [0 .. size - 1] do buckets[i].Reset() 47 | 48 | type private LinearBuffer(ticker: Ticker, interval: time, 49 | size: int, output: Bar -> unit) = 50 | 51 | let buckets = Buckets(size) 52 | let mutable previous: time = 0L 53 | let floor (t:time) = t - (t % (int64 interval)) 54 | 55 | let extrapolate (diff: int): unit = 56 | #if DEBUG 57 | assert (diff > 0) 58 | assert (buckets[0].Count > 0) 59 | assert (buckets[diff].Count > 0) 60 | if diff > 1 then 61 | for k in [ 1 .. (diff - 1)] do assert(buckets[k].Count = 0) 62 | #endif 63 | let prev = buckets[0].Data 64 | let curr = buckets[diff].Data 65 | let f (p: float, c: float) (r: float) = p + (c - p) * r 66 | 67 | for k in [1 .. diff] do 68 | let r = (float <| k-1) / (float diff) 69 | let dv = int64 <| (float <| (curr.Volume - prev.Volume)) * r 70 | let bar = Bar({| Open = f(prev.Open, curr.Open) r 71 | Close = f(prev.Close, curr.Close) r 72 | High = f(prev.High, curr.High) r 73 | Low = f(prev.Low, curr.Low) r 74 | Time = previous + interval * (int64 <| k - 1) 75 | Volume = prev.Volume + dv |}) 76 | #if DEBUG 77 | Log.Info("Data", $"Price for {ticker} is {bar.Price} @ {bar.Epoch}") 78 | #endif 79 | output <| bar 80 | 81 | interface Ingest with 82 | member this.Append(input: Bar): bool = 83 | let current = floor input.Epoch 84 | if buckets[0].Count = 0 then 85 | buckets[0].Add input 86 | previous <- current 87 | true 88 | else 89 | assert (input.Epoch >= buckets.Previous()) 90 | let diff = int <| (current - previous) / interval 91 | if diff >= size then 92 | buckets.Reset() 93 | false 94 | else 95 | buckets[diff].Add input 96 | if diff > 0 then 97 | extrapolate diff 98 | buckets.Shift(diff) 99 | previous <- current ; true 100 | 101 | let Linear(interval: time, size: int) ticker output: Ingest = 102 | LinearBuffer(ticker, interval, size, output) 103 | 104 | 105 | type Buffer = Ticker -> (Bar -> unit) -> Buffer.Ingest 106 | -------------------------------------------------------------------------------- /TradingLib/Common.fs: -------------------------------------------------------------------------------- 1 | namespace TradingLib 2 | 3 | 4 | [] type Maybe<'T> = Yes of 'T | No 5 | 6 | [] type Pair<'T> = { Left: 'T ; Right: 'T } 7 | 8 | 9 | type Vector<'T> = 10 | abstract Size: int 11 | abstract Item : int -> 'T with get 12 | 13 | module Vector = 14 | 15 | type Buffer<'T> (size: int, f: int -> 'T) = 16 | let data: 'T[] = [| for i in 1 .. size -> f (i - 1) |] 17 | member this.Item with get(i: int) = data[i] and set(i: int) x = data[i] <- x 18 | member this.Size: int = size 19 | 20 | member this.Overwrite(f: int -> 'T): unit = 21 | for i in 0 .. (size-1) do data[i] <- (f i) 22 | 23 | interface Vector<'T> with 24 | member this.Item with get(i: int) = data[i] 25 | member this.Size = size 26 | 27 | let Create<'T>(size: int) (f: int -> 'T): Vector<'T> = Buffer(size, f) 28 | 29 | let Sequence (n: int) = Create n (fun i -> i) 30 | 31 | let inline Convolve<'T when ^T: (static member (*) : 'T * 'T -> 'T) 32 | and ^T: (static member (+) : 'T * 'T -> 'T)> 33 | (l: int) (f1: int -> 'T, f2: int -> 'T): 'T = 34 | let f (sum: 'T) (k: int): 'T = sum + (f1 k) * (f2 k) 35 | let init = (f1 0) * (f2 0) in (List.fold f init [1 .. l - 1]) 36 | 37 | type Circular<'T>(size: int, f: int -> 'T) = 38 | let mutable pos: int = 0 39 | let mutable count: int = 0 40 | let data = [| for i in 1 .. size -> f(i - 1) |] 41 | let get (index: int) = data[(size + pos - index - 1) % size] 42 | 43 | member this.Insert(x: 'T) = data[pos] <- x ; count <- count + 1 44 | pos <- pos + 1 ; if pos = size then pos <- 0 45 | 46 | member this.Reset() = count <- 0 ; pos <- 0 47 | 48 | member this.Get(buffer: Buffer<'T>): bool = 49 | if count < size then false else (buffer.Overwrite(get) ; true) 50 | 51 | type Matrix<'V, 'T when 'T :> Vector<'V>> private(rows: int, f: int -> 'T) = 52 | 53 | let data:Vector<'T> = Vector.Create rows f 54 | member this.Item with get(i: int):'T = data[i] 55 | member this.Row i: int -> 'V = fun j -> data[i][j] 56 | member this.Column j: int -> 'V = fun i -> data[i][j] 57 | member this.Rows: int = data.Size 58 | member this.Columns: int = this[0].Size 59 | 60 | static member Var<'N>((rows, columns): int * int) (gen: int * int -> 'N) = 61 | let row i = Vector.Buffer(columns, fun j -> gen (i, j)) in Matrix(rows, row) 62 | 63 | static member Const<'N>((rows, columns): int * int) (gen: int * int -> 'N) = 64 | let row i = Vector.Create columns (fun j -> gen (i, j)) in Matrix(rows, row) 65 | 66 | static member Vandermonde(order: int) (inp: Vector) = 67 | let row (_: int) = Vector.Buffer(inp.Size, fun _ -> Fraction(1)) 68 | let data = Vector.Create (order + 1) row 69 | let write (i: int) (j:int) = data[i-1][j] * inp[j] 70 | for i in 1 .. order do data[i].Overwrite(write i) 71 | Matrix(data.Size, fun i -> data[i] :> Vector) 72 | 73 | type MatrixConst<'T> = Matrix<'T, Vector<'T>> 74 | type MatrixVar<'T> = Matrix<'T, Vector.Buffer<'T>> 75 | -------------------------------------------------------------------------------- /TradingLib/Data.fs: -------------------------------------------------------------------------------- 1 | namespace TradingLib 2 | 3 | 4 | type Data = abstract Get: Vector.Buffer -> bool 5 | 6 | module Data = 7 | 8 | type Store = abstract member Insert: Bar -> unit 9 | abstract member Reset: unit -> unit 10 | 11 | type private Vault(ticker: Ticker, size: int, buffer: Buffer) = 12 | let object = System.Object() 13 | let data = Vector.Circular(size, fun _ -> Bar()) 14 | let insert bar = lock object (fun _ -> data.Insert(bar)) 15 | let reset() = lock object (fun _ -> data.Reset()) 16 | let ingest = buffer ticker insert 17 | interface Store with 18 | member this.Insert(bar: Bar) = if not (ingest.Append bar) then reset() 19 | member this.Reset() = reset() 20 | interface Data with member this.Get(l) = lock object (fun _ -> data.Get(l)) 21 | //TODO: Add this check (for i in l do (assert not(i.Empty))) ; r 22 | 23 | 24 | type Exchange(tickers: Ticker list, size: int, buffer: Buffer) = 25 | let vault ticker = Vault(ticker, size, buffer) 26 | let reader, writer = 27 | let map = Utils.CreateDictionary(tickers, vault) 28 | (Utils.CreateDictionary(tickers, fun ticker -> map[ticker] :> Data), 29 | Utils.CreateDictionary(tickers, fun ticker -> map[ticker] :> Store)) 30 | 31 | member this.Item with get(ticker: Ticker): Store = writer[ticker] 32 | member this.Data = reader 33 | member this.Tickers: Ticker list = tickers 34 | member this.BufferSize: int = size 35 | 36 | type Source = inherit System.IDisposable 37 | abstract Data: Dictionary 38 | abstract Tickers: Ticker list 39 | abstract Size: int 40 | -------------------------------------------------------------------------------- /TradingLib/Execution.fs: -------------------------------------------------------------------------------- 1 | namespace TradingLib 2 | 3 | open TradingLib 4 | 5 | 6 | type Execution<'T> = 7 | abstract Welcome: string 8 | abstract StartTime: Maybe 9 | abstract EndTime: Maybe 10 | abstract InitialCapital: float 11 | abstract TargetProfit: float 12 | abstract StopLoss: float 13 | abstract Strategy: unit -> Strategy 14 | abstract Client: unit -> Client<'T> 15 | abstract Source: unit -> Data.Source 16 | abstract Delay: int 17 | abstract Execute: Order.Entry -> unit 18 | 19 | module Execution = 20 | 21 | type private State(source: Data, strategy: Strategy, size: int, ticker: Ticker) = 22 | let mutable previous: time = 0L 23 | let data = Vector.Buffer(size, fun _ -> Bar()) 24 | let get(): Maybe = 25 | if not (source.Get(data)) then No else 26 | let current = data[0].Epoch 27 | assert (current >= previous) 28 | if current = previous then No else previous <- current 29 | strategy.Execute(ticker, data) 30 | 31 | let best: Maybe * Maybe -> _ = function 32 | | No, No -> No 33 | | No, Yes(o) -> Yes(o) 34 | | Yes(o), No -> Yes(o) 35 | | Yes(o1), Yes(o2) -> 36 | if o1.ProfitPercent() > o2.ProfitPercent() then Yes(o1) else Yes(o2) 37 | 38 | member this.Get(oldOrder: Maybe) = let newOrder = get() in 39 | best(newOrder, oldOrder) 40 | 41 | type Orders(source: Data.Source, strategy: Strategy) = 42 | let state (t: Ticker) = State(source.Data[t], strategy, source.Size, t) 43 | let map = Utils.CreateDictionary(source.Tickers, state) 44 | let get (order: Maybe) (ticker: Ticker) = map[ticker].Get(order) 45 | member this.Get() = source.Tickers |> List.fold get No 46 | 47 | let private wait (start: System.DateTime) (delay: int) = 48 | let str = start.ToString("F") 49 | Log.Info("Execute", $"Waiting for Start time of {str}") 50 | while not(Utils.Elapsed start) do (Utils.Wait delay) 51 | 52 | let stop (execution: Execution<'T>) (client: Client<'T>): Maybe = 53 | let info = client.AccountInfo() 54 | match info.Profit with 55 | | profit when profit >= execution.TargetProfit -> Yes(true) 56 | | profit when -1.0 * profit >= execution.StopLoss -> Yes(false) 57 | | _ -> match execution.EndTime with 58 | | Yes(t) -> if t >= System.DateTime.Now then Yes(false) else No 59 | | No -> No 60 | 61 | [] 62 | let rec loop (execution: Execution<'T>) (orders: Orders) 63 | (client: Client<'T>): bool = 64 | match (stop execution client) with 65 | | Yes(b) -> b 66 | | No -> let order = orders.Get() 67 | match order with 68 | | Yes(o) -> execution.Execute(o) 69 | | No -> Utils.Wait execution.Delay 70 | loop execution orders client 71 | 72 | let private run (execution: Execution<'T>) (source: Data.Source): bool = 73 | Log.Info("Main", "Initializing client ...") 74 | let client = execution.Client() 75 | let info = client.AccountInfo() 76 | Log.Info("Main", "Initializing strategy ...") 77 | let strategy = execution.Strategy() 78 | if info.Total < execution.InitialCapital then 79 | Log.Error("Main", $"Account doesn't have initial capital = {info.Total}") 80 | else 81 | match execution.StartTime with No -> () 82 | | Yes(start) -> wait start execution.Delay 83 | let orders = Orders(source, strategy) in (loop execution orders client) 84 | 85 | let Run(execution: Execution<'T>): bool = 86 | Log.Info("Main", $" ****** {execution.Welcome} ***** ") 87 | Log.Info("Main", $"Trading bot starting at {Utils.CurrentTime()}") 88 | let source = execution.Source() 89 | let result: bool = try (run execution source) finally source.Dispose() 90 | Log.Info("Execute", $"Trading bot ended at {Utils.CurrentTime()}!") 91 | if result then true else false 92 | -------------------------------------------------------------------------------- /TradingLib/Numbers.fs: -------------------------------------------------------------------------------- 1 | namespace TradingLib 2 | 3 | open System 4 | 5 | 6 | type Fraction private (n0: bigint, d0: bigint) = 7 | let n, d = 8 | assert (d0 <> 0I) 9 | let n1, d1 = if (d0 < 0I) then ((-1I * n0), (-1I * d0)) else (n0, d0) 10 | let g = System.Numerics.BigInteger.GreatestCommonDivisor(n1, d1) 11 | n1/g, d1/g 12 | 13 | member this.N = n 14 | member this.D = d 15 | 16 | new (ni: int) = Fraction(bigint(ni), 1I) 17 | new (ni: int, di: int) = Fraction(bigint(ni), bigint(di)) 18 | new (db: float) = 19 | Fraction(bigint (db * 1000.0 * 1000.0 * 1000.0), 1000I * 1000I * 1000I) 20 | 21 | member this.ToFloat() = 22 | let db = float((n * 1000I * 1000I * 1000I) / d) 23 | db / (1000.0 * 1000.0 * 1000.0) 24 | 25 | override this.ToString() = let db = this.ToFloat() in $"%.6f{db}" 26 | 27 | static member ToFloat(f: Fraction) = f.ToFloat() 28 | 29 | static member (+) (n1: Fraction, n2: Fraction) = 30 | Fraction(n1.N * n2.D + n1.D * n2.N, n1.D * n2.D) 31 | 32 | static member (-) (n1: Fraction, n2: Fraction) = 33 | Fraction(n1.N * n2.D - n1.D * n2.N, n1.D * n2.D) 34 | 35 | static member (*) (n1: Fraction, n2: Fraction) = 36 | Fraction(n1.N * n2.N, n1.D * n2.D) 37 | 38 | static member (/) (n1: Fraction, n2: Fraction) = 39 | Fraction(n1.N * n2.D, n1.D * n2.N) 40 | 41 | static member (==)(n1: Fraction, n2: Fraction) = (n1.N * n2.D = n2.N * n1.D) 42 | 43 | static member Reciprocal (f: Fraction) = Fraction(f.D, f.N) 44 | 45 | 46 | type Complex(real: float, imaginary: float) = 47 | member this.Real = real 48 | member this.Imaginary = imaginary 49 | member this.Magnitude() = real * real + imaginary * imaginary 50 | 51 | static member Arc (p: int, q: int): Complex = 52 | let pi = 3.141593 53 | let phase = 2.0 * pi * (float p) / (float q) 54 | Complex(Math.Cos phase, Math.Sin phase) 55 | 56 | static member (+) (c1: Complex, c2: Complex) = 57 | Complex(c1.Real + c2.Real, c1.Imaginary + c2.Imaginary) 58 | 59 | static member (-) (c1: Complex, c2: Complex) = 60 | Complex(c1.Real - c2.Real, c1.Imaginary - c2.Imaginary) 61 | 62 | static member (*) (c1: Complex, c2: Complex) = 63 | let real = c1.Real * c2.Real - c1.Imaginary * c2.Imaginary 64 | let imaginary = c1.Real * c2.Imaginary + c1.Imaginary * c2.Real 65 | Complex(real, imaginary) 66 | 67 | static member (/) (c: Complex, f: float) = Complex(c.Real/f, c.Imaginary/f) 68 | 69 | override this.ToString() = $"{this.Real} + {this.Imaginary}i" 70 | -------------------------------------------------------------------------------- /TradingLib/Socket.fs: -------------------------------------------------------------------------------- 1 | namespace TradingLib 2 | 3 | open Websocket.Client 4 | 5 | 6 | module Socket = 7 | 8 | type private parameters = {| 9 | Url: string 10 | Timeout: int 11 | Receive: string -> unit 12 | Reconnect: string -> unit 13 | Send: string -> unit 14 | Tag: string |} 15 | 16 | type private Socket (p: parameters) = 17 | 18 | let client, reconnect, receive, task = 19 | try 20 | let cl = new WebsocketClient(System.Uri(p.Url)) 21 | cl.ReconnectTimeout <- System.TimeSpan.FromSeconds(p.Timeout) 22 | let rc = System.ObservableExtensions.Subscribe( 23 | cl.ReconnectionHappened, 24 | fun info -> p.Reconnect(info.Type.ToString())) 25 | let rv = System.ObservableExtensions.Subscribe( 26 | cl.MessageReceived, fun msg -> p.Receive(msg.Text)) 27 | (cl, rc, rv, cl.Start()) 28 | with ex -> 29 | Log.Exception(p.Tag, $"Exception in {p.Url} socket connection") ex 30 | 31 | member this.Send (msg:string) = client.Send msg 32 | 33 | interface System.IDisposable with 34 | member this.Dispose() = 35 | receive.Dispose() ; reconnect.Dispose() ; client.Dispose() 36 | Utils.Wait(p.Timeout) 37 | if task.IsCompleted then 38 | Log.Info(p.Tag, $"Closed Socket connection for {p.Url}") 39 | else 40 | Log.Warning(p.Tag, 41 | $"Unable to close socket connection for {p.Url}") 42 | 43 | module Adapter = 44 | 45 | type Common = 46 | inherit System.IDisposable 47 | abstract Tickers: Ticker list 48 | abstract Timeout: int 49 | abstract Start: Ticker -> unit 50 | abstract Buffer: Buffer 51 | abstract Size: int 52 | 53 | type Single = 54 | inherit Common 55 | abstract Url: string 56 | abstract Receive: string * (Ticker -> Bar -> unit) -> unit 57 | abstract Reconnect: string -> unit 58 | abstract Send: string -> unit 59 | abstract Initialize: unit -> unit 60 | abstract Tag: string 61 | 62 | type Multi = 63 | inherit Common 64 | abstract Url: Ticker -> string 65 | abstract Receive: Ticker * string * (Bar -> unit) -> unit 66 | abstract Reconnect: Ticker * string -> unit 67 | abstract Send: Ticker * string -> unit 68 | abstract Tag: Ticker -> string 69 | 70 | module Source = 71 | 72 | type private SingleSource(z: Adapter.Single) = 73 | let exchange = Data.Exchange(z.Tickers, z.Size, z.Buffer) 74 | let insert ticker = exchange[ticker].Insert 75 | let connection: System.IDisposable = new Socket {| 76 | Url = z.Url 77 | Timeout = z.Timeout 78 | Receive = fun msg -> z.Receive(msg, insert) 79 | Reconnect = z.Reconnect 80 | Send = z.Send 81 | Tag = z.Tag |} 82 | 83 | interface Data.Source with 84 | member this.Data = exchange.Data 85 | member this.Tickers = z.Tickers 86 | member this.Size = z.Size 87 | member this.Dispose() = z.Dispose() ; connection.Dispose() 88 | 89 | type private MultiSource(z: Adapter.Multi) = 90 | 91 | let exchange = Data.Exchange(z.Tickers, z.Size, z.Buffer) 92 | let adapter ticker: parameters ={| 93 | Url = z.Url ticker 94 | Timeout = z.Timeout 95 | Receive = fun msg -> z.Receive(ticker, msg, exchange[ticker].Insert) 96 | Reconnect = fun msg -> z.Reconnect(ticker, msg) 97 | Send = fun msg -> z.Send(ticker, msg) 98 | Tag = z.Tag ticker |} 99 | let socket ticker = new Socket(adapter ticker) :> System.IDisposable 100 | let connections = Utils.CreateDictionary(z.Tickers, socket) 101 | 102 | interface Data.Source with 103 | member this.Size = z.Size 104 | member this.Data = exchange.Data 105 | member this.Tickers = z.Tickers 106 | member this.Dispose() = 107 | z.Dispose() 108 | for connection in connections.Values do connection.Dispose() 109 | 110 | let Single(z: Adapter.Single): Data.Source = new SingleSource(z) 111 | let Multi(z: Adapter.Multi): Data.Source = new MultiSource(z) 112 | -------------------------------------------------------------------------------- /TradingLib/TradingLib.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /TradingLib/Utils.fs: -------------------------------------------------------------------------------- 1 | namespace TradingLib 2 | 3 | open System 4 | open System.Diagnostics 5 | open System.Collections.Immutable 6 | 7 | 8 | type time = int64 9 | type Dictionary<'K,'V> = System.Collections.Immutable.ImmutableDictionary<'K,'V> 10 | 11 | 12 | module Utils = 13 | 14 | let Ascii (inp: string): string = 15 | let bytes = System.Text.Encoding.ASCII.GetBytes(inp) 16 | System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length).Replace("?", " ") 17 | 18 | let ToDateTime(epoch: int64): DateTime = 19 | let dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(epoch) 20 | let estZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time") 21 | TimeZoneInfo.ConvertTimeFromUtc(dateTimeOffset.DateTime, estZone) 22 | 23 | let inline Normalize(x: float) = Math.Round(x, 3) 24 | let inline CurrentTime() = DateTime.Now.ToString("F") 25 | let inline Diff (a: float, b: float) = Normalize(100.0 * (a-b) / b) 26 | 27 | let CreateDictionary<'V, 'K when 'K: equality>(l: 'K list, f: 'K -> 'V) = 28 | let data = System.Collections.Generic.Dictionary<'K, 'V>(l.Length) 29 | (for x in l do data.Add(x, f x)) ; data.ToImmutableDictionary() 30 | 31 | let inline Wait (timeout: int) = 32 | assert (timeout > 0) ; Threading.Thread.Sleep(timeout * 1000) 33 | 34 | let inline Elapsed (time: DateTime) = DateTime.Now > time 35 | 36 | 37 | module Log = 38 | 39 | type private log() = 40 | do Trace.Listeners.Add(new ConsoleTraceListener(true)) |> ignore 41 | 42 | member this.Entry (header: string) (tag: string, msg: string): unit = 43 | let timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") 44 | let tagStr = if tag = "" then "" else $" {tag}" 45 | Trace.WriteLine($"[{timestamp}] {header}{tagStr}: {msg}") 46 | 47 | let private logger = log() 48 | let Warning = logger.Entry "WARNING" 49 | let Info = logger.Entry "INFO" 50 | let Debug = logger.Entry "Debug" 51 | let Error(tag, msg) = 52 | logger.Entry "ERROR" (tag, msg) ; raise (Exception(msg)) 53 | let Exception(tag, msg) (ex: exn) = 54 | logger.Entry "ERROR" (tag, msg) ; raise (Exception(msg, ex)) 55 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true --self-contained true SampleBot\SampleBot.fsproj 2 | copy .\SampleBot\bin\Release\net8.0\win-x64\publish\SampleBot.exe . 3 | --------------------------------------------------------------------------------