aboutsummaryrefslogtreecommitdiff
path: root/server/services/process_bids.py
blob: 7e734f1eb180151f9e9f07fcc6ee7379b7cca26d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from db import SessionLocal
from models.bid import Bid
from models.auth import User
from gridstatus import ISONE
from zoneinfo import ZoneInfo

NEW_ENGLAND_TZ = ZoneInfo("America/New_York")
iso = ISONE()

def get_day_ahead_price(target_time: datetime) -> float:
    """Fetch the Day Ahead clearing price for the hour of target_time."""
    df = iso.get_lmp(date=target_time.date(), market="DAY_AHEAD_HOURLY", locations="ALL")
    df = df.groupby("Interval Start")["LMP"].mean().reset_index()

    for _, row in df.iterrows():
        if abs(row["Interval Start"] - target_time.replace(minute=0, second=0, microsecond=0)) < timedelta(minutes=30):
            return row["LMP"]

    raise ValueError(f"No day ahead price found for {target_time}")

def get_real_time_prices(target_time: datetime) -> list[float]:
    """Fetch the Real Time 5-min prices during the hour of target_time."""
    df = iso.get_lmp(date=target_time.date(), market="REAL_TIME_5_MIN", locations="ALL")
    df = df.groupby("Interval Start")["LMP"].mean().reset_index()

    start_of_hour = target_time.replace(minute=0, second=0, microsecond=0)
    end_of_hour = start_of_hour + timedelta(hours=1)

    prices = [
        row["LMP"]
        for _, row in df.iterrows()
        if start_of_hour <= row["Interval Start"] < end_of_hour
    ]

    return prices

def process_bids():
    db: Session = SessionLocal()

    now = datetime.now(NEW_ENGLAND_TZ)

    # Step 1: Process Submitted bids
    submitted_bids = db.query(Bid).filter(Bid.status == "Submitted").all()
    for bid in submitted_bids:
        try:
            target_time = bid.timestamp.astimezone(NEW_ENGLAND_TZ)

            # Only process bids whose timestamp is already in the past
            if target_time > now:
                print(f"Skipping future bid {bid.id} at {target_time}")
                continue

            print(f"Processing submitted bid {bid.id} for {target_time}")

            day_ahead_price = get_day_ahead_price(target_time)

            if bid.price >= day_ahead_price:
                # Bid was accepted
                bid.status = "Executed"
                bid.pnl = None  # Will be calculated later
                print(f"Bid {bid.id}: Executed (waiting for real-time PnL)")
            else:
                # Bid rejected
                bid.status = "Fail"
                bid.pnl = None
                print(f"Bid {bid.id}: Failed (price too low)")

            db.commit()

        except Exception as e:
            print(f"Error processing submitted bid {bid.id}: {e}")

    # Step 2: Process Executed bids (calculate PnL)
    executed_bids = db.query(Bid).filter(Bid.status == "Executed").all()
    for bid in executed_bids:
        try:
            target_time = bid.timestamp.astimezone(NEW_ENGLAND_TZ)

            # Only settle if the whole real-time period is over
            if now < target_time + timedelta(hours=1):
                print(f"Skipping bid {bid.id}, real-time window not complete yet")
                continue

            print(f"Settling executed bid {bid.id} for {target_time}")

            real_time_prices = get_real_time_prices(target_time)

            if not real_time_prices:
                print(f"No real-time prices available for bid {bid.id} yet")
                continue  # Retry later

            avg_real_time_price = sum(real_time_prices) / len(real_time_prices)

            day_ahead_price = get_day_ahead_price(target_time)

            pnl = (day_ahead_price - avg_real_time_price) * bid.quantity

            bid.status = "Settled"
            bid.pnl = pnl
            print(f"Bid {bid.id} Settled | PnL: ${pnl:.2f}")

            db.commit()

        except Exception as e:
            print(f"Error settling executed bid {bid.id}: {e}")

    db.close()

if __name__ == "__main__":
    process_bids()