Parsing date in JSON key

I am trying to track the price of various funds described at Share Price History | The Thrift Savings Plan (TSP). These are not tracked on Yahoo Finance or anywhere standard as far as I can tell.

For historical data, I can do a one-time CSV import without issue.

For ongoing data, I found an API that will provide the latest closing price for the various funds at https://api.dailytsp.com/close, which provides the latest prices in the following JSON format:

{
  "2024-04-25": {
    "C Fund": 79.0421,
    "F Fund": 18.5777,
    "G Fund": 18.2053,
    "I Fund": 41.2685,
    "L 2025": 12.9831,
    "L 2030": 46.5431,
    "L 2035": 13.9625,
    "L 2040": 52.8197,
    "L 2045": 14.4487,
    "L 2050": 31.6545,
    "L 2055": 15.7637,
    "L 2060": 15.7615,
    "L 2065": 15.7591,
    "L Income": 25.3699,
    "S Fund": 77.6817
  }
}

I’m struggling to find the right JSONPath expression to use to parse the key name as date. The “Path to Close” I think I have configured correctly as $.*["L 2050"] For the date, I cannot figure out how to access a key name just using JSONPath. According to the tester at jsonpath.com, it appears I should be able to use $.*~:

but when I try this in PP, I get the following output:

so it seems the ~ operator is not supported. Any tips on how to accomplish this?

I had a similar issue; see JSON from Alpha Vantage - English - Portfolio Performance Forum (portfolio-performance.info)

Unfortunately, I didn’t find a solution.

Sorry to say but what you are expecting is impossible to achieve out of the box with PP. You cannot use a JSON key node in the same way as target value.

Indeed the current PP implementation makes the assumption, that the user provides two json path expressions which each yield an array. And then the imported price is constructed by taking the value from one array and the price from the other.

I see two problems:

  • First, the tilde operator does not seem to yield the expected result. Unfortunately, the Json specification is not final and different implementations behave differently (see https://cburgmer.github.io/json-path-comparison/). The Java implementation does not return the expected result (it returns the JSON objects, not the key). See #417 and #439
  • Second, one could use $.keys(). This statement returns a set as opposed to a list. PP cannot work with the set at the moment and a set is not ordered. I would not be able to match that to the list of prices.

The gist: PP does not support this kind of JSON structure at the moment.

I am not sure what to do about it. To contribute the tilde operator to jpath, I do not have the time or skills. Plus it would have to work on the mobile platform the same.

An alternative implementation also sounds like a special case: Define which a) a path that points to the object and b) the sub path within that object and c) define which is what (is the date always the key?).

Ideas welcome

As the previous speakers have already said, the JSON structure simply doesn’t fit.
As a workaround, you could use a proxy script that delivers the historical CSV data as JSON. As an example, I have created a Golang script here (the use of “curl --compressed” works for me on Linux, no idea how it is on other OSs).

tsp_share_price_history.go

package main

import (
   "log"
   "net/http"
   "fmt"
   "os/exec"
   "encoding/json"
   "strings"
   "regexp"
)

var nlines int = 0

type Record struct {
   Date  string `json:"date"`
   LIncome string `json:"LIncome"`
   L2025 string `json:"L2025"`
   L2030 string `json:"L2030"`
   L2035 string `json:"L2035"`
   L2040 string `json:"L2040"`
   L2045 string `json:"L2045"`
   L2050 string `json:"L2050"`
   L2055 string `json:"L2055"`
   L2060 string `json:"L2060"`
   L2065 string `json:"L2065"`
   GFund string `json:"GFund"`
   FFund string `json:"FFund"`
   CFund string `json:"CFund"`
   SFund string `json:"SFund"`
   IFund string `json:"IFund"`
}

func gethistory(w http.ResponseWriter, req *http.Request) {

   getjson := "curl --compressed -H \"User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/125.0\" -H \"Accept: */*\" -H \"Accept-Language: de-DE,en-US;q=0.7,en;q=0.3\" -H \"Accept-Encoding: gzip, deflate, br\" -H \"Referer: https://www.tsp.gov/share-price-history/\" -H \"X-Requested-With: XMLHttpRequest\" -H \"DNT: 1\" -H \"Sec-Fetch-Dest: empty\" -H \"Sec-Fetch-Mode: cors\" -H \"Sec-Fetch-Site: same-origin\" -H \"TE: trailers\" \"https://www.tsp.gov/data/fund-price-history.csv\""
   body, err := exec.Command("bash", "-c", getjson).Output()
   if err != nil {
      log.Fatalln(err)
   }
   csvstring := string(body)
   records := []Record{}
   lines := strings.Split(csvstring, "\n")
   for key, _ := range lines[0:] {
      nlines = key
   }
// L Income,L 2025,L 2030,L 2035,L 2040,L 2045,L 2050,L 2055,L 2060,L 2065,G Fund,F Fund,C Fund,S Fund,I Fund
   for _, line := range lines[1:nlines] {
      fields := strings.Split(line, ",")
      if fields[0] == "" {
      	continue
      }
      date := fields[0]
      LIncome := fields[1]
      L2025 := fields[2]
      L2030 := fields[3]
      L2035 := fields[4]
      L2040 := fields[5]
      L2045 := fields[6]
      L2050 := fields[7]
      L2055 := fields[8]
      L2060 := fields[9]
      L2065 := fields[10]
      GFund := fields[11]
      FFund := fields[12]
      CFund := fields[13]
      SFund := fields[14]
      IFund := fields[15]
      records = append(records, Record{
         Date: date,
	 LIncome: LIncome,
	 L2025: L2025,
         L2030: L2030,
         L2035: L2035,
         L2040: L2040,
         L2045: L2045,
         L2050: L2050,
         L2055: L2055,
         L2060: L2060,
         L2065: L2065,
         GFund: GFund,
         FFund: FFund,
         CFund: CFund,
         SFund: SFund,
         IFund: IFund,
      })
   }
   jsonData, err := json.Marshal(records)
   if err != nil {
      panic(err)
   }
   nonullspleaseRegexp := regexp.MustCompile(`:\"\"`)
   newbody := nonullspleaseRegexp.ReplaceAllString(string(jsonData), ":\"0\"")

   fmt.Fprintf(w, string(newbody))
}

func headers(w http.ResponseWriter, req *http.Request) {
    for name, headers := range req.Header {
        for _, h := range headers {
            fmt.Fprintf(w, "%v: %v\n", name, h)
        }
    }
}

func main() {
   http.HandleFunc("/", gethistory)
   http.HandleFunc("/headers", headers)
   http.ListenAndServe(":8083", nil)
}

Start the script with

go run tsp_share_price_history.go

and let it run in the background while PP is running.

You can access the script via a browser, or also via PP


ă…¤
ă…¤
ă…¤

TSP: F Fund for example

http://localhost:8083/gethistory
$[*].date
$[*].FFund


ă…¤
ă…¤
ă…¤
TSP: L Income for example

http://localhost:8083/gethistory
$[*].date
$[*].LIncome

I just cobbled the script together quickly. If you want to query more than three or four funds in PP, you should consider whether it would be better to prevent the csv from being fetched multiple times (buffer CSV data). I didn’t feel like doing that now.

If you also want to have the data in the mobile app, the script must of course run on an accessible server, otherwise the machine where PP is also running, or a NAS, or something like that, is sufficient.

3 Likes

Thanks for the reply.

I ended up doing something similar, writing a scraper for this security and a few others I couldn’t easily get into PP in Python, and then serving the JSON locally with Caddy, then finally importing that into PP using the JSON importer. It seems to be working pretty well so far.

That’s a fairly “advanced” solution, as in many users probably wouldn’t be able to implement something like that very easily. I wonder if there’s any appetite to enhance the generic “scraping” abilities in this area, or reduce the barrier to entry to programming a new “Provider”. I guess the base language of PP is Java, so I suppose it would have to be in that language.