org.powertac.auctioneer.AuctionService Maven / Gradle / Ivy
/*
* Copyright (c) 2011 - 2014 by the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.powertac.auctioneer;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.joda.time.Instant;
import org.powertac.common.Broker;
import org.powertac.common.ClearedTrade;
import org.powertac.common.Competition;
import org.powertac.common.MarketPosition;
import org.powertac.common.Order;
import org.powertac.common.Orderbook;
import org.powertac.common.OrderbookOrder;
import org.powertac.common.TimeService;
import org.powertac.common.Timeslot;
import org.powertac.common.config.ConfigurableValue;
import org.powertac.common.interfaces.Accounting;
import org.powertac.common.interfaces.BrokerProxy;
import org.powertac.common.interfaces.InitializationService;
import org.powertac.common.interfaces.ServerConfiguration;
import org.powertac.common.interfaces.TimeslotPhaseProcessor;
import org.powertac.common.msg.OrderStatus;
import org.powertac.common.repo.OrderbookRepo;
import org.powertac.common.repo.TimeslotRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
/**
* This is the wholesale day-ahead market. Energy is traded in future timeslots by
* submitting MarketOrders representing bids and asks. Each specifies a price (minimum price
* for asks, maximum (negative) price for bids) and a quantity in mWh. A bid is
* defined as an Order with a positive value for mWh; an ask is an Order with a
* negative mWh value. Once during each timeslot, the
* market is cleared by matching bids with asks such that the bid price is no lower than
* the ask price, and allocating quantities, until no matching bids or asks are
* available. In general, the last matched bid will have a higher price than the last
* matched ask. All trades are cleared at a price determined by splitting the difference
* between the last bid and the last ask according to the value of sellerSurplusRatio,
* which is a parameter set in the initialization process.
*
* Orders may be market orders (no specified price) as well as limit orders
* (the normal case). Market orders are considered to have a "more attractive"
* price than any limit order, so they are sorted first in the clearing process.
* In case the clearing process needs to set a price by matching a market order
* with a limit order, the clearing price is set by applying a "default margin"
* to the limit order. If there are no limit orders in the match, then the
* market clears at a fixed default clearing price. It's probably best if brokers
* do not allow this to happen.
* @author John Collins
*/
@Service
public class AuctionService
extends TimeslotPhaseProcessor
implements InitializationService
{
static private Logger log = LogManager.getLogger(AuctionService.class.getName());
//@Autowired
//private TimeService timeService;
@Autowired
private Accounting accountingService;
@Autowired
private BrokerProxy brokerProxyService;
@Autowired
private TimeService timeService;
@Autowired
private TimeslotRepo timeslotRepo;
@Autowired
private OrderbookRepo orderbookRepo;
@Autowired
private ServerConfiguration serverProps;
@ConfigurableValue(valueType = "Double",
publish = true,
description = "Default margin when matching market order with limit order")
private double defaultMargin = 0.05; // used when one side has no limit price
@ConfigurableValue(valueType = "Double",
publish = true,
description = "Default price/mwh when matching only market orders")
private double defaultClearingPrice = 40.00; // used when no limit prices
@ConfigurableValue(valueType = "Double",
publish = true,
description = "Proportion of market surplus allocated to the seller")
private double sellerSurplusRatio = 0.5;
@ConfigurableValue(valueType = "Double",
publish = true,
description = "maximum seller margin")
private double sellerMaxMargin = 0.05;
@ConfigurableValue(valueType = "Double",
publish = true,
description = "maximum market position at maximum leadtime")
private double mktPosnLimitInitial = 90.0;
@ConfigurableValue(valueType = "Double",
publish = true,
description = "maximum market position at minimum leadtime")
private double mktPosnLimitFinal = 143.0;
private double epsilon = 1e-6; // position balance less than this is ignored
private List incoming;
private HashMap> sortedBids;
private HashMap> sortedAsks;
private List enabledTimeslots = null;
public AuctionService ()
{
super();
incoming = new ArrayList();
}
@Override
public String initialize (Competition competition, List completedInits)
{
incoming.clear();
serverProps.configureMe(this);
brokerProxyService.registerBrokerMessageListener(this, Order.class);
super.init();
serverProps.publishConfiguration(this);
return "Auctioneer";
}
public double getSellerSurplusRatio ()
{
return sellerSurplusRatio;
}
public double getDefaultMargin ()
{
return defaultMargin;
}
public double getDefaultClearingPrice ()
{
return defaultClearingPrice;
}
List getIncoming ()
{
return incoming;
}
// ----------------- Broker message API --------------------
/**
* Receives, validates, and queues an incoming Order message. Processing the incoming
* marketOrders happens during Phase 2 in each timeslot.
*/
public void handleMessage (Order msg)
{
if (validateOrder(msg)) {
// queue incoming message
synchronized(incoming) {
incoming.add(msg);
}
log.info("Received " + msg.toString());
}
}
public boolean validateOrder (Order order)
{
if (order.getMWh().equals(Double.NaN) ||
order.getMWh().equals(Double.POSITIVE_INFINITY) ||
order.getMWh().equals(Double.NEGATIVE_INFINITY)) {
log.warn("Order from " + order.getBroker().getUsername()
+ " with invalid quantity " + order.getMWh());
return false;
}
double minQuantity = Competition.currentCompetition().getMinimumOrderQuantity();
if (Math.abs(order.getMWh()) < minQuantity) {
log.warn("Order from " + order.getBroker().getUsername()
+ " with quantity " + order.getMWh()
+ " < minimum quantity " + minQuantity);
return false;
}
if (!timeslotRepo.isTimeslotEnabled(order.getTimeslot())) {
OrderStatus status = new OrderStatus(order.getBroker(), order.getId());
brokerProxyService.sendMessage(order.getBroker(), status);
log.warn("Order from " + order.getBroker().getUsername()
+" for disabled timeslot " + order.getTimeslot());
return false;
}
return true;
}
// ------------------- Market clearing ------------------------
/**
* Processes incoming Order instances for each timeslot, generating the appropriate
* MarketTransactions, Orderbooks, and ClearedTrade instances.
*/
@Override
public void activate (Instant time, int phaseNumber)
{
log.info("Activate");
// Grab all the incoming marketOrders and sort them by price and timeslot
ArrayList orders;
synchronized(incoming) {
orders = new ArrayList();
for (Order order : incoming) {
OrderWrapper sw = new OrderWrapper(order);
if (sw.isValid()) {
// ignore invalid orders
orders.add(new OrderWrapper(order));
}
else {
log.info("Ignoring invalid order " + order.getId() +
" from " + order.getBroker().getUsername());
}
}
incoming.clear();
}
sortedAsks = new HashMap>();
sortedBids = new HashMap>();
// add bids and asks to the appropriate lists
for (OrderWrapper sw : orders) {
if (sw.isBuyOrder())
addBid(sw);
else
addAsk(sw);
}
// then sort the lists
for (ArrayList list : sortedAsks.values()) {
Collections.sort(list);
}
for (ArrayList list : sortedBids.values()) {
Collections.sort(list);
}
log.debug("activate: asks in " + sortedAsks.size() + " timeslots, bids in " +
sortedBids.size() + " timeslots");
// Iterate through the timeslots that were enabled at the end of the last
// timeslot, and clear each one individually
if (enabledTimeslots == null) {
enabledTimeslots = timeslotRepo.enabledTimeslots();
}
collectAskRanges();
for (Timeslot timeslot : enabledTimeslots) {
clearTimeslot(timeslot);
}
// save a copy of the current set of enabled timeslots for the next clearing
enabledTimeslots = new ArrayList(timeslotRepo.enabledTimeslots());
}
private void clearTimeslot (Timeslot timeslot)
{
List bids = sortedBids.get(timeslot);
List asks = sortedAsks.get(timeslot);
if (null != bids)
constrainMarketPositions(bids, timeslot.getSerialNumber());
if (null != bids || null != asks) {
// we have bids and/or asks to match up
if (bids != null && asks != null)
log.info("Timeslot " + timeslot.getSerialNumber() +
": Clearing " + asks.size() + " asks and " +
bids.size() + " bids");
Double bidPrice = 0.0;
Double askPrice = 0.0;
double totalMWh = 0.0;
ArrayList pendingTrades = new ArrayList();
while (bids != null && !bids.isEmpty() &&
asks != null && !asks.isEmpty() &&
(bids.get(0).isMarketOrder() ||
asks.get(0).isMarketOrder() ||
-bids.get(0).getLimitPrice() >= asks.get(0).getLimitPrice())) {
// transfer from ask to bid, keep track of qty
OrderWrapper bid = bids.get(0);
bidPrice = bid.getLimitPrice();
OrderWrapper ask = asks.get(0);
askPrice = ask.getLimitPrice();
// amount to transfer is minimum of remaining bid qty and remaining ask qty
log.debug("ask: " + ask.executionMWh + " used out of " + ask.getMWh() +
"; bid: " + bid.executionMWh + " used out of " + bid.getMWh());
double transfer = Math.min(bid.getMWh() - bid.executionMWh,
-ask.getMWh() + ask.executionMWh);
if (transfer > 0.0) {
log.debug("transfer " + transfer + " from " +
ask.getBroker().getUsername() + " at " + askPrice + " to " +
bid.getBroker().getUsername() + " at " + bidPrice);
totalMWh += transfer;
pendingTrades.add(new PendingTrade(ask.getBroker(), bid.getBroker(), transfer));
bid.executionMWh += transfer;
ask.executionMWh -= transfer;
}
log.debug("bid remaining=" + (bid.getMWh() - bid.executionMWh));
log.debug("ask remaining=" + (ask.getMWh() - ask.executionMWh));
if (Math.abs(bid.getMWh() - bid.executionMWh) <= epsilon)
bids.remove(bid);
if (Math.abs(ask.getMWh() - ask.executionMWh) <= epsilon)
asks.remove(ask);
}
double clearingPrice;
if (bidPrice != null) {
if (askPrice != null) {
clearingPrice =
askPrice + sellerSurplusRatio * (-bidPrice - askPrice);
clearingPrice =
Math.min(clearingPrice, askPrice * (1.0 + sellerMaxMargin));
}
else {
// ask price is null
clearingPrice = -bidPrice / (1.0 + defaultMargin);
log.info("market clears at " + clearingPrice + " with null ask price");
}
}
else {
// bid price is null
if (askPrice != null) {
clearingPrice = askPrice * (1.0 + defaultMargin);
log.info("market clears at " + clearingPrice + " with null bid price");
}
else {
// both bid and ask are null
clearingPrice = defaultClearingPrice;
log.info("market clears at default clearing price" + clearingPrice);
}
}
for (PendingTrade trade : pendingTrades) {
accountingService.addMarketTransaction(trade.from, timeslot,
-trade.mWh, clearingPrice);
accountingService.addMarketTransaction(trade.to, timeslot,
trade.mWh, -clearingPrice);
}
// create the orderbook and cleared-trade, send to brokers
Orderbook orderbook =
orderbookRepo.makeOrderbook(timeslot,
(pendingTrades.size() > 0
? clearingPrice : null));
if (bids != null) {
for (OrderWrapper bid : bids) {
orderbook.addBid(new OrderbookOrder(bid.getMWh() - bid.executionMWh,
bid.getLimitPrice()));
}
}
if (asks != null) {
for (OrderWrapper ask : asks) {
orderbook.addAsk(new OrderbookOrder(ask.getMWh() - ask.executionMWh,
ask.getLimitPrice()));
}
}
brokerProxyService.broadcastMessage(orderbook);
if (totalMWh > 0.0) {
ClearedTrade trade = new ClearedTrade(timeslot, totalMWh, clearingPrice,
timeService.getCurrentTime());
log.info(trade.toString());
brokerProxyService.broadcastMessage(trade);
}
}
}
// Walks through a sorted list of bids, modifying quantities as necessary
// to impose market position limits.
private void constrainMarketPositions (List bids, int ts)
{
HashMapremainingPosn = new HashMap<>();
for (OrderWrapper bid: bids) {
if (bid.getBroker().isWholesale())
// Don't limit wholesale entities
continue;
double remaining = getRemaining(bid.getBroker(), remainingPosn, ts);
remaining -= bid.getMWh();
if (remaining < 0.0) {
// adjust bid
double qty = bid.getMWh() + remaining;
remaining = 0.0;
log.info("Adjusting bid of {} from {} to {}",
bid.getBroker().getUsername(),
bid.getMWh(), qty);
bid.setMWh(qty);
}
remainingPosn.put(bid.getBroker(), remaining);
}
}
// Returns remaining position for broker/timeslot
private double getRemaining (Broker broker,
HashMap posns,
int ts)
{
Double result = posns.get(broker);
if (null == result) {
MarketPosition posn = broker.findMarketPositionByTimeslot(ts);
// offset is zero for final ts
int offset = ts - timeslotRepo.currentSerialNumber();
double limit = mktPosnLimitFinal;
if (enabledTimeslots.size() > 1) {
limit -= (offset * (mktPosnLimitFinal - mktPosnLimitInitial)
/ (enabledTimeslots.size() - 1));
}
result = Math.max(0.0, limit - posn.getOverallBalance());
posns.put(broker, result);
}
return result;
}
private void addAsk (OrderWrapper marketOrder)
{
Timeslot timeslot = marketOrder.getTimeslot();
if (sortedAsks.get(timeslot) == null) {
sortedAsks.put(timeslot, new ArrayList());
}
sortedAsks.get(timeslot).add(marketOrder);
}
private void addBid (OrderWrapper marketOrder)
{
Timeslot timeslot = marketOrder.getTimeslot();
if (sortedBids.get(timeslot) == null) {
sortedBids.put(timeslot, new ArrayList());
}
sortedBids.get(timeslot).add(marketOrder);
}
// Collect min/max ask price ranges
private void collectAskRanges ()
{
// Prepare to collect minimum ask prices
Double[] minPriceArray = new Double[enabledTimeslots.size()];
Double[] maxPriceArray = new Double[enabledTimeslots.size()];
int timeslotIndex = 0;
for (Timeslot timeslot : enabledTimeslots) {
if (null == sortedAsks || null == sortedAsks.get(timeslot)) {
minPriceArray[timeslotIndex] = null;
maxPriceArray[timeslotIndex] = null;
}
else {
int lastIndex = sortedAsks.get(timeslot).size() - 1;
OrderWrapper minAsk = sortedAsks.get(timeslot).get(0);
OrderWrapper maxAsk = sortedAsks.get(timeslot).get(lastIndex);
if (null == minAsk || minAsk.isMarketOrder()) {
minPriceArray[timeslotIndex] = null;
}
else {
minPriceArray[timeslotIndex] = minAsk.getLimitPrice();
}
if (null == maxAsk || maxAsk.isMarketOrder()) {
maxPriceArray[timeslotIndex] = null;
}
else {
maxPriceArray[timeslotIndex] = maxAsk.getLimitPrice();
}
}
timeslotIndex++;
}
// store min ask prices in orderbookRepo
orderbookRepo.setMinAskPrices(minPriceArray);
orderbookRepo.setMaxAskPrices(maxPriceArray);
}
// test support -- get rid of saved timeslots
void clearEnabledTimeslots ()
{
enabledTimeslots = null;
}
class PendingTrade
{
Broker from;
Broker to;
double mWh;
PendingTrade (Broker from, Broker to, double mWh)
{
super();
this.from = from;
this.to = to;
this.mWh = mWh;
}
}
class OrderWrapper implements Comparable
{
Order order;
double executionMWh = 0.0;
double adjustedMWh = 0.0;
OrderWrapper(Order order)
{
super();
this.order = order;
this.adjustedMWh = order.getMWh();
}
// delegation API
Broker getBroker ()
{
return order.getBroker();
}
// valid if qty is non-zero
boolean isValid ()
{
return (adjustedMWh != 0.0);
}
boolean isMarketOrder ()
{
return (order.getLimitPrice() == null);
}
Double getLimitPrice ()
{
return order.getLimitPrice();
}
double getMWh ()
{
return adjustedMWh;
}
void setMWh (double newValue)
{
adjustedMWh = newValue;
}
Timeslot getTimeslot ()
{
return order.getTimeslot();
}
boolean isBuyOrder ()
{
return (order.getMWh() > 0.0);
}
@Override
public int compareTo(OrderWrapper o) {
OrderWrapper other = (OrderWrapper) o;
// negative qty is ask
double sign = Math.signum(this.getMWh());
Double thisQty = sign * this.getMWh();
Double otherQty = sign * other.getMWh();
if (this.isMarketOrder())
if (other.isMarketOrder())
return compareQty(thisQty, otherQty);
else
return -1;
else if (other.isMarketOrder())
return 1;
else
if (this.getLimitPrice().equals(other.getLimitPrice())) {
// qty is ascending for negative values, descending for positive values
return compareQty(thisQty, otherQty);
}
else {
// price is always ascending
return (this.getLimitPrice() > other.getLimitPrice() ? 1 : -1);
}
}
public boolean equals (OrderWrapper other) {
if (this.isMarketOrder())
if (other.isMarketOrder())
return (this.getMWh() == other.getMWh());
else
return false;
return (this.getLimitPrice().equals(other.getLimitPrice())
&& (this.getMWh() == other.getMWh()));
}
}
private int compareQty (Double thisQty, Double otherQty)
{
return -thisQty.compareTo(otherQty);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy