Node Search Price Pull Example

Notebook Overview

This notebook illustrates how to use the “node search” API endpoint. This endpoint allows users to search by location, node name, iso, or a combination. When searching by location, a list of the nearest nodes is returned. Once the user has identified the node they want to use, they can then pull historical prices for this node using the “get prices” API endpoint.

Imports

First, let’s import software packages, the Client API and your PAT.

[1]:
import time
import os
import pandas as pd
from tyba_client.client import Client, Market

PAT = os.environ["TYBA_PAT"]
client = Client(PAT)

# Relevant API services
services = client.services
lmp = services.lmp
ancillary = services.ancillary

Node Search Functions

Next, let’s use the lmp.search_nodes function to search for a node. There are multiple ways a user can search for a node. To confirm successful search, status_code = 200. The function will return the node location, node ISO, node name, node ID, and node distance to the search location (if specified).

1. Search by location / address

Let’s search by location and extract the list of nodes into a dataframe.

[2]:
res = lmp.search_nodes(location="dallas, texas")
res.raise_for_status()
df = pd.DataFrame(res.json()["nodes"])
[3]:
df
[3]:
node/lng node/iso node/name node/lat node/id node/distance-meters
0 -96.862329 ERCOT EMPCT_RN 32.825858 10018267018 8198.673431
1 -96.666783 ERCOT HB_NORTH 32.759108 10000697078 12354.211404
2 -96.666783 ERCOT LZ_NORTH 32.759108 10000698392 12354.211404
3 -96.664116 ERCOT PRCRK_RN 32.776804 10018267016 12448.495097
4 -96.935367 ERCOT MCSES_UNIT6 32.724285 10000698679 14209.794577
5 -96.935367 ERCOT MCSES_UNIT7 32.724285 10000698680 14209.794577
6 -96.935367 ERCOT MCSES_UNIT8 32.724285 10000698681 14209.794577
7 -97.026005 ERCOT LIGSW_RN 32.813753 10018248462 21842.460551
8 -96.547759 ERCOT LHSES_UNIT1 32.836986 10000698659 24281.578207
9 -96.547759 ERCOT LHSES_UNIT2 32.836986 10000698660 24281.578207
10 -96.490168 ERCOT FRNYPP_1_CCU 32.756028 10000698581 28839.581651
11 -96.490168 ERCOT FRNYPP_2_CCU 32.756028 10000698582 28839.581651
12 -96.490168 ERCOT FRNYPP_CC1 32.756028 10000698343 28839.581651
13 -96.490168 ERCOT FRNYPP_CC2 32.756028 10000698344 28839.581651
14 -97.083319 ERCOT CNTRY_RN 32.679874 10017419312 28907.071045
15 -96.539784 ERCOT ANOL_ESS_RN 32.552020 10018287496 34680.822268
16 -97.216621 ERCOT HLSES_UNIT3 32.728377 10000698608 39688.083699
17 -97.216621 ERCOT HLSES_UNIT4 32.728377 10000698609 39688.083699
18 -97.216621 ERCOT HLSES_UNIT5 32.728377 10000698610 39688.083699
19 -97.038847 ERCOT ELS_RN_1 32.449070 10016962333 42838.585540
20 -96.428990 ERCOT LILY_RN 32.533273 10016498291 43822.820037
21 -97.077831 ERCOT DSWL_BES1 32.452541 10018287497 44573.640963
22 -97.156367 ERCOT NOBLESLR_ALL 33.045111 10016874016 44906.180346
23 -97.054843 ERCOT MDANP_CT1_2 32.427980 10000698682 45619.085651
24 -97.054843 ERCOT MDANP_CT3_4 32.427980 10000698683 45619.085651

We can also search by lat/lng or street address.

[4]:
res = lmp.search_nodes(location="29.760427, -95.369804")
res = lmp.search_nodes(location="3 NRG Pkwy, Houston, TX 77054")

2. Search by portion of node name

[5]:
res = lmp.search_nodes(node_name_filter="HB_")
res.raise_for_status()
df = pd.DataFrame(res.json()["nodes"])
[6]:
df
[6]:
node/id node/name node/iso node/lat node/lng
0 10000698380 HB_BUSAVG ERCOT 30.850714 -97.877628
1 10000697077 HB_HOUSTON ERCOT 29.852151 -95.477535
2 10000698382 HB_HUBAVG ERCOT 30.850714 -97.877628
3 10000697078 HB_NORTH ERCOT 32.759108 -96.666783
4 10015999590 HB_PAN ERCOT 35.370957 -101.385270
5 10000697079 HB_SOUTH ERCOT 28.870453 -98.513815
6 10000697080 HB_WEST ERCOT 32.099516 -100.984953
7 10016973502 IHB_7_IRONHORSE_BATGNODE CAISO 32.098504 -110.819117
8 10004176539 PGHB_6_PDRP01-APND CAISO 32.795429 -116.972406
9 10004074430 PGHB_6_PDRP02-APND CAISO 32.795429 -116.972406
10 10002701851 PGHB_6_PDRP03-APND CAISO 40.867043 -124.088263
11 10004262229 PGHB_6_PDRP04-APND CAISO 32.795429 -116.972406
12 10015847591 PGHB_6_PDRP05-APND CAISO 32.795429 -116.972406
13 10015853877 PGHB_6_PDRP06-APND CAISO 32.795429 -116.972406
14 10015880548 PGHB_6_PDRP07-APND CAISO 32.795429 -116.972406
15 10016481077 PGHB_6_PDRP08-APND CAISO 32.795429 -116.972406
16 10016481089 PGHB_6_PDRP09-APND CAISO 32.795429 -116.972406
17 10016019849 PGHB_6_PDRP10-APND CAISO 32.795429 -116.972406
18 10016240024 PGHB_6_PDRP11-APND CAISO 32.795429 -116.972406
19 10016240072 PGHB_6_PDRP12-APND CAISO 32.795429 -116.972406
20 10016481091 PGHB_6_PDRP14-APND CAISO 32.795429 -116.972406
21 10016481092 PGHB_6_PDRP15-APND CAISO 32.795429 -116.972406
22 10016481078 PGHB_6_PDRP16-APND CAISO 32.795429 -116.972406
23 10016495007 PGHB_6_PDRP17-APND CAISO 32.795429 -116.972406
24 10016656810 PGHB_6_PDRP18-APND CAISO 32.795429 -116.972406

3. Search by portion of node name and location / address

We can combine multiple search filters to narrow down further.

[7]:
res = lmp.search_nodes(node_name_filter="HB_", location="Houston, Texas")

4. Search for a location but with an ISO override to constrain search to the specified ISO

We can even search for a location but then specify an ISO within which to look for nodes.

[8]:
res = lmp.search_nodes(location="toledo, OH", iso_override="MISO")

Pull Energy Prices

Now that we’ve searched for the node(s) in question, let’s extract the relevant node directory information.

[9]:
df
[9]:
node/id node/name node/iso node/lat node/lng
0 10000698380 HB_BUSAVG ERCOT 30.850714 -97.877628
1 10000697077 HB_HOUSTON ERCOT 29.852151 -95.477535
2 10000698382 HB_HUBAVG ERCOT 30.850714 -97.877628
3 10000697078 HB_NORTH ERCOT 32.759108 -96.666783
4 10015999590 HB_PAN ERCOT 35.370957 -101.385270
5 10000697079 HB_SOUTH ERCOT 28.870453 -98.513815
6 10000697080 HB_WEST ERCOT 32.099516 -100.984953
7 10016973502 IHB_7_IRONHORSE_BATGNODE CAISO 32.098504 -110.819117
8 10004176539 PGHB_6_PDRP01-APND CAISO 32.795429 -116.972406
9 10004074430 PGHB_6_PDRP02-APND CAISO 32.795429 -116.972406
10 10002701851 PGHB_6_PDRP03-APND CAISO 40.867043 -124.088263
11 10004262229 PGHB_6_PDRP04-APND CAISO 32.795429 -116.972406
12 10015847591 PGHB_6_PDRP05-APND CAISO 32.795429 -116.972406
13 10015853877 PGHB_6_PDRP06-APND CAISO 32.795429 -116.972406
14 10015880548 PGHB_6_PDRP07-APND CAISO 32.795429 -116.972406
15 10016481077 PGHB_6_PDRP08-APND CAISO 32.795429 -116.972406
16 10016481089 PGHB_6_PDRP09-APND CAISO 32.795429 -116.972406
17 10016019849 PGHB_6_PDRP10-APND CAISO 32.795429 -116.972406
18 10016240024 PGHB_6_PDRP11-APND CAISO 32.795429 -116.972406
19 10016240072 PGHB_6_PDRP12-APND CAISO 32.795429 -116.972406
20 10016481091 PGHB_6_PDRP14-APND CAISO 32.795429 -116.972406
21 10016481092 PGHB_6_PDRP15-APND CAISO 32.795429 -116.972406
22 10016481078 PGHB_6_PDRP16-APND CAISO 32.795429 -116.972406
23 10016495007 PGHB_6_PDRP17-APND CAISO 32.795429 -116.972406
24 10016656810 PGHB_6_PDRP18-APND CAISO 32.795429 -116.972406

Pull all ISO nodes

Let’s first pull the ERCOT ISO directory. We’ll filter this directory to use in any energy price pulls.

[10]:
services.get_all_isos().json()
[10]:
['PJMISO', 'CAISO', 'NEISO', 'NYISO', 'ERCOT', 'SPPISO', 'MISO']
[11]:
price_nodes = lmp.get_all_nodes(iso='ERCOT').json()
node_directory = pd.DataFrame(price_nodes)
[12]:
node_directory
[12]:
da_start_year rt_end_year zone name type substation da_end_year id rt_start_year
0 2011.0 2025 SOUTH OXY_OXY_CCG1 GENERATOR OXYCHEM CC 2025.0 10000698721 2011
1 2022.0 2025 SOUTH SLCN_ESS_ALL GENERATOR NaN 2025.0 10016671726 2022
2 2024.0 2025 SOUTH ANEM_ESS_RN GENERATOR NaN 2025.0 10017318833 2024
3 2011.0 2025 NORTH WHCCS_CC1 GENERATOR WOLF HOLLOW GEN 2025.0 10000698379 2011
4 2011.0 2025 HOUSTON SCLP_PUN4 GENERATOR SWEENY COGEN 2025.0 10000698780 2011
... ... ... ... ... ... ... ... ... ...
1066 2022.0 2025 WEST QUEEN_BA_RN GENERATOR NaN 2025.0 10016874015 2022
1067 2024.0 2025 HOUSTON RBN_BESS1 LOAD NaN 2025.0 10017290064 2024
1068 2018.0 2025 SOUTH SANTACRU_ALL GENERATOR SANTACRU 2025.0 10004183883 2018
1069 2023.0 2025 WEST PALE_ESS_EN GENERATOR NaN 2025.0 10017177379 2023
1070 2011.0 2025 HOUSTON WAP_WAP_G2 GENERATOR WA PARISH 2025.0 10000698870 2011

1071 rows × 9 columns

Helper Functions

This takes the raw API payload and converts it into a dataframe with a datetime index:

[13]:
def reformat_df(result):
    df = pd.concat({node:pd.DataFrame(results['prices'],index=pd.to_datetime(results['datetimes'])) for node,results in result.items()},axis=1)
    return df.droplevel(axis=1,level=1).tz_localize(None)

This is a function to simplify pulling 1 or more nodes and or energy markets. At most 8 nodes are allowed in a single request.

[14]:
def lmp_pull(directory,start_year,end_year,markets):

    nodemap = dict(zip(directory.id, directory.name)) #takes the nodes dataframe and turns it into a dictionary
    market_options = {'RT':Market.RT,'DA':Market.DA} #takes the list of markets to include so we can pull from the relevant database

    results = {}
    for market in markets:
        print(f"Fetching LMP prices for market={market_options[market]}")
        prices_response = lmp.get_prices(
            node_ids=list(nodemap.keys()), #this is now a list of node ids (see node_directory)
            market=market_options[market], #this pulls the right market
            start_year=int(start_year),
            end_year=int(end_year),
        )

        if prices_response.status_code == 200:
            prices = prices_response.json()
            results[market] = reformat_df(prices).rename(nodemap,axis=1) #this renames the column with the nodename instead of ID
        else:
            print(f"Error while pulling LMP prices: {prices_response._content}")

    return pd.concat(results,axis=1)

Single Node Pull - RT & DA

Let’s pull the LMP data for the first node returned by our node search.

[15]:
single_node = node_directory.loc[node_directory.name== df.iloc[0]['node/name']]
single_node
[15]:
da_start_year rt_end_year zone name type substation da_end_year id rt_start_year
609 2011.0 2025 N/A HB_BUSAVG HUB 2025.0 10000698380 2011
[16]:
lmps = lmp_pull(single_node,2019,2023,['DA','RT'])
lmps
Fetching LMP prices for market=dayahead
Fetching LMP prices for market=realtime
[16]:
DA RT
HB_BUSAVG HB_BUSAVG
2019-01-01 00:00:00 20.49 13.7800
2019-01-01 01:00:00 19.77 14.8450
2019-01-01 02:00:00 19.56 15.4975
2019-01-01 03:00:00 19.21 16.1050
2019-01-01 04:00:00 21.22 16.0325
... ... ...
2023-12-31 19:00:00 19.88 14.3475
2023-12-31 20:00:00 17.01 12.5775
2023-12-31 21:00:00 14.83 10.6650
2023-12-31 22:00:00 15.79 10.2950
2023-12-31 23:00:00 15.86 12.9600

43800 rows × 2 columns

Multi Node Pull - RT & DA

Let’s pull the LMP data for multiple nodes returned by our node search.

[17]:
df['node/name']
[17]:
0                    HB_BUSAVG
1                   HB_HOUSTON
2                    HB_HUBAVG
3                     HB_NORTH
4                       HB_PAN
5                     HB_SOUTH
6                      HB_WEST
7     IHB_7_IRONHORSE_BATGNODE
8           PGHB_6_PDRP01-APND
9           PGHB_6_PDRP02-APND
10          PGHB_6_PDRP03-APND
11          PGHB_6_PDRP04-APND
12          PGHB_6_PDRP05-APND
13          PGHB_6_PDRP06-APND
14          PGHB_6_PDRP07-APND
15          PGHB_6_PDRP08-APND
16          PGHB_6_PDRP09-APND
17          PGHB_6_PDRP10-APND
18          PGHB_6_PDRP11-APND
19          PGHB_6_PDRP12-APND
20          PGHB_6_PDRP14-APND
21          PGHB_6_PDRP15-APND
22          PGHB_6_PDRP16-APND
23          PGHB_6_PDRP17-APND
24          PGHB_6_PDRP18-APND
Name: node/name, dtype: object

You can pull prices for at most 8 nodes in a single request. To pull prices for more than 8 nodes, split up the nodes into multiple requests.

[18]:
nodes_to_pull = df['node/name']

multiple_nodes = node_directory.loc[node_directory.name.isin(nodes_to_pull)]
multiple_nodes
[18]:
da_start_year rt_end_year zone name type substation da_end_year id rt_start_year
29 2011.0 2025 N/A HB_HUBAVG HUB 2025.0 10000698382 2011
94 2011.0 2025 WEST HB_WEST HUB 2025.0 10000697080 2011
176 2011.0 2025 NORTH HB_NORTH HUB 2025.0 10000697078 2011
282 2011.0 2025 SOUTH HB_SOUTH HUB 2025.0 10000697079 2011
609 2011.0 2025 N/A HB_BUSAVG HUB 2025.0 10000698380 2011
720 2011.0 2025 HOUSTON HB_HOUSTON HUB 2025.0 10000697077 2011
853 2020.0 2025 WEST HB_PAN HUB 2025.0 10015999590 2020
[19]:
multiple_nodes_filtered = multiple_nodes[multiple_nodes['rt_start_year']<2020]

multiple_nodes_filtered = multiple_nodes_filtered[:8]  # Limiting to 8 nodes
multiple_nodes_filtered
[19]:
da_start_year rt_end_year zone name type substation da_end_year id rt_start_year
29 2011.0 2025 N/A HB_HUBAVG HUB 2025.0 10000698382 2011
94 2011.0 2025 WEST HB_WEST HUB 2025.0 10000697080 2011
176 2011.0 2025 NORTH HB_NORTH HUB 2025.0 10000697078 2011
282 2011.0 2025 SOUTH HB_SOUTH HUB 2025.0 10000697079 2011
609 2011.0 2025 N/A HB_BUSAVG HUB 2025.0 10000698380 2011
720 2011.0 2025 HOUSTON HB_HOUSTON HUB 2025.0 10000697077 2011
[20]:
lmps_from_multiple = lmp_pull(multiple_nodes_filtered,2020,2024,['DA','RT'])
lmps_from_multiple
Fetching LMP prices for market=dayahead
Fetching LMP prices for market=realtime
[20]:
DA RT
HB_HUBAVG HB_WEST HB_NORTH HB_SOUTH HB_BUSAVG HB_HOUSTON HB_HUBAVG HB_WEST HB_NORTH HB_SOUTH HB_BUSAVG HB_HOUSTON
2020-01-01 00:00:00 11.23 11.03 11.29 11.34 11.27 11.27 13.7075 13.7075 13.7075 13.7075 13.7075 13.7075
2020-01-01 01:00:00 9.99 9.83 10.01 10.40 10.04 9.73 13.2725 13.2725 13.2725 13.2725 13.2725 13.2725
2020-01-01 02:00:00 9.52 9.28 9.64 9.58 9.58 9.58 11.3700 11.3700 11.3700 11.3700 11.3700 11.3700
2020-01-01 03:00:00 9.89 9.67 9.95 10.30 9.95 9.65 9.4300 9.4250 9.4300 9.4375 9.4300 9.4325
2020-01-01 04:00:00 10.13 9.71 10.30 10.31 10.22 10.18 8.6325 8.6325 8.6325 8.6325 8.6325 8.6325
... ... ... ... ... ... ... ... ... ... ... ... ...
2024-12-31 19:00:00 34.31 40.37 35.70 28.68 34.25 32.47 34.7075 46.9075 36.9375 24.5675 34.5850 30.4125
2024-12-31 20:00:00 31.16 34.27 31.97 28.05 31.14 30.35 26.5600 28.7125 26.8275 24.8950 26.4975 25.8050
2024-12-31 21:00:00 25.26 27.07 25.64 23.51 25.22 24.81 22.7175 23.4000 22.5675 22.4250 22.6250 22.4825
2024-12-31 22:00:00 20.98 22.06 21.09 20.09 20.93 20.70 22.2400 22.2775 22.2300 22.2225 22.2325 22.2250
2024-12-31 23:00:00 20.39 21.12 20.39 19.97 20.34 20.11 20.2875 20.2875 20.2875 20.2875 20.2875 20.2875

43800 rows × 12 columns

[ ]: