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
[ ]: