We show how to create a PriceSeries object by downloading prices from Yahoo! Finance while giving the basis for further data sources developments.

PriceSeries.fs

namespace Trading.Data

open System

type PriceSeries =
    { Date : DateTime[]
      Open : float[]
      High : float[]
      Low : float[]
      Close : float[]
      Volume : float[]
    }
      static member create n =
        { Date = Array.zeroCreate n
          Open = Array.zeroCreate n
          High = Array.zeroCreate n
          Low = Array.zeroCreate n
          Close = Array.zeroCreate n
          Volume = Array.zeroCreate n
        }

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module PriceSeries =

  let create n =
    { Date = Array.zeroCreate n
      Open = Array.zeroCreate n
      High = Array.zeroCreate n
      Low = Array.zeroCreate n
      Close = Array.zeroCreate n
      Volume = Array.zeroCreate n
    }

  let rec private areDatesInvalidAux dates i found =
    if found then true
    elif i >= Array.length dates then found
    else
      areDatesInvalidAux dates (i+1) (dates.[i] < dates.[i-1])

  let checkDates dates =
    if Array.length dates > 1 && areDatesInvalidAux dates 1 false then
      invalidArg "dates" "dates are not sorted"

  let checkSameLength a b =
    if Array.length a <> Array.length b then
      invalidArg "input arrays" "input arrays have different lengths"

  let checkHighLow high low =
    checkSameLength high low
    for i in 0 .. high.Length - 1 do
      if high.[i] < low.[i] then
        failwithf "high < low at index %d" i

  let checkHighLowClose high low c =
    checkSameLength high low
    checkSameLength high c
    for i in 0 .. Array.length high - 1 do
      if c.[i] > high.[i] then
        failwithf "open > high at index %d" i
      if low.[i] > c.[i] then
        failwithf "low > open at index %d" i

  let checkOpenHighLowClose o high low c =
    checkSameLength high low
    checkSameLength high o
    checkSameLength high c
    for i in 0 .. Array.length high - 1 do
      if o.[i] > high.[i] then
        failwithf "open > high at index %d" i
      if low.[i] > o.[i] then
        failwithf "low > open at index %d" i
      if c.[i] > high.[i] then
        failwithf "close > high at index %d" i
      if low.[i] > c.[i] then
        failwithf "low > close at index %d" i

DataLoader.fs

For the Excel DataLoader, we use the FileHelpers library by Marcos Meli.

namespace Trading.Data

open System
open System.Net
open Microsoft.FSharp.Control.WebExtensions

open FileHelpers
open FileHelpers.DataLink

open Trading.Data

type DataLoader =
  interface
    abstract AsyncDownload : string -> DateTime -> DateTime -> Async<PriceSeries>
  end

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module DataLoader =

  module Excel =

    [<DelimitedRecord("|")>]
    type PriceSample () =
      [<DefaultValue>]
      val mutable Date : DateTime
      [<DefaultValue>]
      val mutable Open : float
      [<DefaultValue>]
      val mutable High : float
      [<DefaultValue>]
      val mutable Low : float
      [<DefaultValue>]
      val mutable Close : float
      [<DefaultValue>]
      val mutable Volume : float

    let create tickerDir ext startRow startColumn =
      {new DataLoader with
        override x.AsyncDownload ticker first last =
          async {
            let fullPath = System.IO.Path.Combine(tickerDir, ticker + ext)
            let provider =
              new ExcelStorage(
                typeof<PriceSample>,
                StartRow=startRow,
                StartColumn=startColumn,
                FileName=fullPath
              )
            let res =
              provider.ExtractRecords()
                |> unbox<PriceSample[]>
                |> Array.filter (fun sample -> sample.Date >= first && sample.Date <= last)
                |> Array.sortBy (fun sample -> sample.Date)

            let series = PriceSeries.create res.Length

            res |> Array.iteri (fun i sample ->
              series.Date.[i] <- sample.Date
              series.Open.[i] <- sample.Open
              series.High.[i] <- sample.High
              series.Low.[i] <- sample.Low
              series.Close.[i] <- sample.Close
              series.Volume.[i] <- sample.Volume
            )

            PriceSeries.checkDates series.Date
            PriceSeries.checkOpenHighLowClose series.Open series.High series.Low series.Close

            return series
          }
      }

  module Yahoo =

    let createUrl ticker (first:DateTime) (last:DateTime) =
        sprintf
          @"http://ichart.finance.yahoo.com/table.csv?s=%s&a=%d&b=%d&c=%d&d=%d&e=%d&f=%d&g=d&ignore=.csv"
            ticker
            (first.Month - 1) first.Day first.Year
            (last.Month - 1) last.Day last.Year 

    let asyncDownload url =
      async {
        let client = new WebClient()
        return! client.AsyncDownloadString(url)
      }

    let create () =
      {new DataLoader with
        override x.AsyncDownload ticker first last =
          async {
            let url =  Uri(createUrl ticker first last)
            let! data = asyncDownload url
            let lines = data.Split( [|'\n'|]).[1..]
            let series =  PriceSeries.create lines.Length
            let firstOk = ref -1
            let counter = ref 0
            for line in 0 .. lines.Length - 1 do
              let columns = lines.[line].Split([|','|])
              if Array.length columns = 7 then
                let date = DateTime.Parse columns.[0]
                if date >= first && date <= last then
                  if !firstOk < 0 then firstOk := line
                  incr counter
                  series.Date.[line] <- date
                  series.Open.[line] <- float columns.[1]
                  series.High.[line] <- float columns.[2]
                  series.Low.[line] <- float columns.[3]
                  //adjusted close
                  //normal close would be the 4th column
                  series.Close.[line] <- float columns.[6]
                  series.Volume.[line] <- float columns.[5]
            //Delete invalid dates
            if !counter <> lines.Length then
              let newSeries =  PriceSeries.create !counter
              Array.blit series.Date !firstOk newSeries.Date 0 !counter
              Array.blit series.Open !firstOk newSeries.Open 0 !counter
              Array.blit series.High !firstOk newSeries.High 0 !counter
              Array.blit series.Low !firstOk newSeries.Low 0 !counter
              Array.blit series.Close !firstOk newSeries.Close 0 !counter
              Array.blit series.Volume !firstOk newSeries.Volume 0 !counter
              return newSeries
            else
              return series
          }
      }

Comments are closed.