Add market data handling, battery state updates, and unit tests
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .market import MarketState
|
||||||
|
from .battery import BatteryState
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--market1", type=Path, help="Market state file")
|
||||||
|
parser.add_argument("--market2", type=Path, help="Market state file")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
market1 = MarketState().load_market_data_from_csv(args.market1)
|
||||||
|
market2 = MarketState().load_market_data_from_csv(args.market2)
|
||||||
|
|
||||||
|
battery = BatteryState()
|
||||||
|
|
||||||
|
increment = datetime.timedelta(minutes=10)
|
||||||
|
market_1_start = market1.get_start_time()
|
||||||
|
market2_start = market2.get_start_time()
|
||||||
|
start_time = min(market_1_start, market2_start)
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from batterymodel.models.market import MarketDataIncrement
|
||||||
|
|
||||||
|
|
||||||
class BatteryState(Enum):
|
class BatteryState(Enum):
|
||||||
IDLE = 0
|
IDLE = 0
|
||||||
@@ -19,12 +21,59 @@ class BatteryModel:
|
|||||||
self._storage_volume_degradation_rate = 0.001 # %/cycle
|
self._storage_volume_degradation_rate = 0.001 # %/cycle
|
||||||
self._fixed_operational_costs = 50000 # £/year
|
self._fixed_operational_costs = 50000 # £/year
|
||||||
self._runtime = datetime.timedelta(0)
|
self._runtime = datetime.timedelta(0)
|
||||||
|
self._charge_discharge_cycles = 0
|
||||||
self._decision_log_file_handle = open("battery_log.txt", "w+")
|
self._decision_log_file_handle = open("battery_log.txt", "w+")
|
||||||
|
|
||||||
self._battery_state = BatteryState.IDLE
|
self._battery_state = BatteryState.IDLE
|
||||||
|
self._current_charge_rate = 0.0
|
||||||
|
self._current_discharge_rate = 0.0
|
||||||
self._current_market_rate = 0.0
|
self._current_market_rate = 0.0
|
||||||
self._current_charge_value = 0.0
|
self._current_charge_value = 0.0
|
||||||
|
self._current_charge = 0.0
|
||||||
self._total_profit = 0.0
|
self._total_profit = 0.0
|
||||||
|
|
||||||
def update(self, increment: datetime.timedelta):
|
def update(self, increment: datetime.timedelta, previous_state: "BatteryModel", market_data_increment: MarketDataIncrement):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def handle_charge_or_discharge(self, increment: datetime.timedelta, previous_state: "BatteryModel"):
|
||||||
|
if self._battery_state != BatteryState.CHARGING:
|
||||||
|
total_charge_this_increment = self._current_charge_rate * (increment.total_seconds()/60.0)
|
||||||
|
self._current_charge = self._current_charge + total_charge_this_increment
|
||||||
|
|
||||||
|
# We filled the battery this increment
|
||||||
|
if self._current_charge > self._battery_capacity:
|
||||||
|
self._current_charge = self._battery_capacity
|
||||||
|
total_charge_this_increment = self._battery_capacity - previous_state._current_charge
|
||||||
|
self._battery_state = BatteryState.IDLE
|
||||||
|
time_of_charge = datetime.timedelta(seconds=total_charge_this_increment / self._current_charge_rate)
|
||||||
|
|
||||||
|
elif self._battery_state == BatteryState.DISCHARGING:
|
||||||
|
|
||||||
|
total_discharge_this_increment = self._current_discharge_rate * (increment.total_seconds() / 60.0)
|
||||||
|
self._current_charge = self._current_charge - total_discharge_this_increment
|
||||||
|
|
||||||
|
# emptied the battery this increment
|
||||||
|
if self._current_charge <= 0:
|
||||||
|
self._battery_state = BatteryState.IDLE
|
||||||
|
# might have to deal with penalties here for not providing enough?
|
||||||
|
time_of_charge = datetime.timedelta(seconds=previous_state._current_charge / self._current_discharge_rate)
|
||||||
|
|
||||||
|
if self._battery_state != BatteryState.IDLE:
|
||||||
|
self._current_charge_value = self._current_charge_value + (self._current_market_rate * time_of_charge.total_seconds())
|
||||||
|
self._total_profit += self._current_charge_value
|
||||||
|
self.log_state_change("handle_charge_or_discharge_charge", previous_state._current_charge, self._current_charge)
|
||||||
|
self.log_state_change("handle_charge_or_discharge_value", previous_state._current_charge_value,
|
||||||
|
self._current_charge_value)
|
||||||
|
|
||||||
|
def log_state_change(self, fn, original_state, new_state):
|
||||||
|
self._decision_log_file_handle.write(f"{self._runtime},{fn},{original_state},{new_state}\n")
|
||||||
|
|
||||||
|
def handle_storage_capacity_check(self, previous_state: "BatteryModel"):
|
||||||
|
if self._charge_discharge_cycles > previous_state._charge_discharge_cycles and self._charge_discharge_cycles / int(self._charge_discharge_cycles) == 1:
|
||||||
|
storage_volume_lost = (self._max_storage_volume/100) * self._storage_volume_degradation_rate
|
||||||
|
self._max_storage_volume -= storage_volume_lost
|
||||||
|
self.log_state_change("handle_storage_capacity_check_cycles_check", previous_state._max_storage_volume, self._max_storage_volume)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import csv
|
||||||
|
import datetime
|
||||||
|
from collections import OrderedDict
|
||||||
|
from typing import List, Tuple
|
||||||
|
from xmlrpc.client import DateTime
|
||||||
|
|
||||||
|
|
||||||
|
class MarketState:
|
||||||
|
def __init__(self):
|
||||||
|
self._market_data = OrderedDict()
|
||||||
|
|
||||||
|
def load_market_data_from_csv(self, csv_file):
|
||||||
|
with open(csv_file, 'r') as file:
|
||||||
|
reader = csv.reader(file)
|
||||||
|
for row in reader:
|
||||||
|
self._market_data[datetime.datetime.strptime(row[0], '%d-%m-%Y %H:%M')] = float(row[1])
|
||||||
|
|
||||||
|
def get_start_time(self):
|
||||||
|
return next(iter(self._market_data))
|
||||||
|
|
||||||
|
def get_market_rate_for_timestamp(self, timestamp: datetime.datetime):
|
||||||
|
last_timestamp = None
|
||||||
|
for key in self._market_data:
|
||||||
|
if last_timestamp is not None:
|
||||||
|
delta = timestamp - last_timestamp
|
||||||
|
key_delta = key - last_timestamp
|
||||||
|
if key == timestamp:
|
||||||
|
return self._market_data[key]
|
||||||
|
elif last_timestamp is not None and last_timestamp < key and key_delta > delta:
|
||||||
|
|
||||||
|
return self._market_data[last_timestamp]
|
||||||
|
else:
|
||||||
|
last_timestamp = key
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class MarketDataIncrement:
|
||||||
|
def __init__(self, timestamp: datetime.datetime, markets: List[Tuple[datetime.datetime, float]]):
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.markets = markets
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
from batterymodel import BatteryModel
|
from batterymodel import BatteryModel
|
||||||
|
from batterymodel.models.battery import BatteryState
|
||||||
|
|
||||||
|
|
||||||
def test_battery_stats():
|
def test_battery_stats():
|
||||||
assert BatteryModel()._max_charge_rate == 2000000 # Watts
|
assert BatteryModel()._max_charge_rate == 2000000 # Watts
|
||||||
@@ -10,3 +12,7 @@ def test_battery_stats():
|
|||||||
assert BatteryModel()._lifetime_cycles == 50000
|
assert BatteryModel()._lifetime_cycles == 50000
|
||||||
assert BatteryModel()._storage_volume_degradation_rate == 0.001 # %/cycle
|
assert BatteryModel()._storage_volume_degradation_rate == 0.001 # %/cycle
|
||||||
assert BatteryModel()._fixed_operational_costs == 50000 # £/year
|
assert BatteryModel()._fixed_operational_costs == 50000 # £/year
|
||||||
|
|
||||||
|
def test_battery_update():
|
||||||
|
battery = BatteryModel()
|
||||||
|
assert battery._battery_state == BatteryState.IDLE
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from batterymodel.models.market import MarketState
|
||||||
|
|
||||||
|
|
||||||
|
def test_market():
|
||||||
|
this_market = MarketState()
|
||||||
|
this_market._market_data[datetime.datetime.strptime('01-01-2022 00:00', '%d-%m-%Y %H:%M')] = 11
|
||||||
|
this_market._market_data[datetime.datetime.strptime('01-01-2022 00:01', '%d-%m-%Y %H:%M')] = 12
|
||||||
|
this_market._market_data[datetime.datetime.strptime('01-01-2022 00:02', '%d-%m-%Y %H:%M')] = 13
|
||||||
|
this_market._market_data[datetime.datetime.strptime('01-01-2022 00:05', '%d-%m-%Y %H:%M')] = 14
|
||||||
|
|
||||||
|
assert this_market.get_market_rate_for_timestamp(datetime.datetime.strptime('01-01-2022 00:02', '%d-%m-%Y %H:%M')) == 13
|
||||||
|
assert this_market.get_market_rate_for_timestamp(datetime.datetime.strptime('01-01-2022 00:03', '%d-%m-%Y %H:%M')) == 13
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user