import clone from 'clone';
import { EventEmitter } from 'events';
import ProbJS from 'prob.js';
import {TradeTimingStrategy} from 'trade-timing-strategy';
let privateNextId = 1;
function nextId() { return privateNextId++; }
function sum(a) {
let i, l, total = 0;
for (i = 0, l = a.length;i<l;++i)
total += +a[i];
return total;
}
/* ignore this function in test coverage stats */
/* istanbul ignore next */
function dot(a, b) {
let i, l, total = 0;
if (a.length !== b.length)
throw new Error("market-agents: vector dimensions do not match in dot(a,b)");
for (i = 0, l = a.length;i < l;++i)
if (a[i] && b[i])
total += (a[i] * b[i]);
return total;
}
function poissonWake() {
if (this.rate<=0) return +Infinity; // if the rate is zero or negative the agent never acts
const delta = ProbJS.exponential(this.rate)();
// undefined is a valid this.wakeTime
const result = this.wakeTime + delta;
// block NaN and negative values
if (result > 0)
return result;
}
/**
* Agent with Poisson-distributed opportunities to act, with period managment, optional inventory, unit values and costs, and end-of-period production and consumption to satisfy trades
*
*/
export class Agent extends EventEmitter {
/**
* creates an Agent with clone of specified options and initializes with .init().
* Option properties are stored directly on the created agent's this.
*
* @param {Object} options Agent creation options
* @param {string} [options.description] text description of agent, optional
* @param {Object} [options.inventory={}] initial inventory, as object with good names as keys and levels as values
* @param {string} [options.money='money'] Good used as money by this agent
* @param {Object} [options.values={}] marginal value table of agent for goods that are redeemed at end-of-period, as object with goods as keys and numeric arrays as values
* @param {Object} [options.costs={}] marginal cost table of agent for goods that are produced at end-of-period, as object with goods as keys and numeric arrays as values
* @param {number} [options.wakeTime=0] initial wake-up time for agent, adjusted by this.init() to first poisson-based wake with .nextWake()
* @param {number} [options.rate=1] Poisson-arrival rate of agent wake events
* @param {function():number} [options.nextWake=poissonWake] calculates next Agent wake-up time
*
*/
constructor(options) {
super();
const defaults = {
id: nextId(),
description: 'blank agent',
inventory: {},
money: 'money',
values: {},
costs: {},
wakeTime: 0,
rate: 1,
period: {
number: 0,
duration: 1000,
equalDuration: true
},
nextWake: poissonWake
};
Object.assign(this, defaults, clone(options, false));
this.init();
}
/**
* initialize an agent to new settings
* @param {Object} [newSettings] see constructor
*
*/
init(newSettings) {
if (typeof(newSettings) === 'object') {
// work with a shallow copy of the newSettings so
// the code can delete the inventory setting without side effects
let mySettings = Object.assign({}, newSettings);
// copy new values to inventory. do not reset other inventory values
Object.assign(this.inventory, mySettings.inventory);
// reset non-inventory as specified, completely overwriting previous
// to execute this reset, first: delete the inventory settings, then apply the remainder
delete mySettings.inventory;
Object.assign(this, mySettings);
}
// if this.money is defined but is not in inventory, zero the inventory of this.money
if (this.money && !(this.inventory[this.money]))
this.inventory[this.money] = 0;
/**
* time, in JS ms since epoch, of agent wake
* @type {number} this.wakeTime
*/
this.wakeTime = this.nextWake();
}
/**
* re-initialize agent to the beginning of a new simulation period
*
* @param {number|Object} period A period initialization object, or a number indicating a new period using the previous period's initialization object
* @param {number} period.number A number, usually sequential, identifying the next period, e.g. 1,2,3,4,5,...
* @param {boolean} [period.equalDuration=false] with positive period.duration, autogenerates startTime and endTime as n or n+1 times period.duration
* @param {number} [period.duration] the length of the period, used with period.equalDuration
* @param {number} [period.startTime] period begins, manual setting for initial time value for agent wakeTime
* @param {number} [period.endTime] period ends, no agent wake events will be emitted for this period after this time
* @param {Object} [period.init] initializer for other agent properties, passed to .init()
* @emits {pre-period} when initialization to new period is complete
* @example
* myAgent.initPeriod({number:1, duration:1000, equalDuration: true});
* myAgent.initPeriod(2);
*/
initPeriod(period) {
// period might look like this
// period = {number:5, startTime:50000, init: {inventory:{X:0, Y:0}, values:{X:[300,200,100,0,0,0,0]}}}
// or period could be simply a number
if (typeof(period) === 'object')
this.period = clone(period, false);
else if (typeof(period) === 'number')
this.period.number = period;
if (this.period.equalDuration && this.period.duration) {
this.period.startTime = this.period.number * this.period.duration;
this.period.endTime = (1 + this.period.number) * this.period.duration;
}
if (typeof(this.period.startTime) === 'number')
this.wakeTime = this.period.startTime;
this.init(this.period.init);
this.emit('pre-period');
}
/**
* ends current period, causing agent to undertake end-of-period tasks such as production and redemption of units
*
* @emits {post-period} when period ends, always, but after first completing any production/redemption
*/
endPeriod() {
if (typeof(this.produce) === 'function') this.produce();
if (typeof(this.redeem) === 'function') this.redeem();
this.emit('post-period');
}
/**
* percent of period used
* @return {number} proportion of period time used as a number from 0.0, at beginning of period, to 1.0 at end of period.
*
*/
pctPeriod() {
if ((this.period.startTime !== undefined) && (this.period.endTime > 0) && (this.wakeTime !== undefined)) {
return (this.wakeTime - this.period.startTime) / (this.period.endTime - this.period.startTime);
}
}
/**
* decay an initial value into a final value over the time provided in the current period
* @param {number} initialValue initial value, greater than 0
* @param {number} finalValue final value, greater than 0
* @return {number} result current value
*/
decay(initialValue,finalValue){
const lambda = this.pctPeriod();
if ((lambda===undefined) || (initialValue<=0) || (finalValue<=0)) return undefined;
const myLog = (lambda*Math.log(finalValue))+((1-lambda)*Math.log(initialValue));
const result = Math.exp(myLog);
return (this.integer)? Math.round(result): result;
}
/**
* guess at number of random Poisson wakes remaining in period
*
* @return {number} "expected" number of remaining random Poisson wakes, calculated as (this.period.endTime-this.wakeTime)*rate
*
*/
poissonWakesRemainingInPeriod() {
if ((this.rate > 0) && (this.wakeTime !== undefined) && (this.period.endTime > 0)) {
return (this.period.endTime - this.wakeTime) * this.rate;
}
}
/**
* wakes agent so it can act, emitting wake, and sets next wakeTime from this.nextWake() unless period.endTime exceeded
*
* @param {Object} [info] optional info passed to this.emit('wake', info)
* @emits {wake(info)} immediately
*/
wake(info) {
this.emit('wake', info);
const nextTime = this.nextWake();
if (this.period.endTime) {
if (nextTime < this.period.endTime)
this.wakeTime = nextTime;
else
this.wakeTime = undefined;
} else {
this.wakeTime = nextTime;
}
}
/**
* increases or decreases agent's inventories of one or more goods and/or money
*
* @param {Object} myTransfers object with goods as keys and changes in inventory as number values
* @param {Object} [memo] optional memo passed to event listeners
* @emits {pre-transfer(myTransfers, memo)} before transfer takes place, modifications to myTransfers will change transfer
* @emits {post-transfer(myTransfers, memo)} after transfer takes place
*/
transfer(myTransfers, memo) {
if (myTransfers) {
this.emit('pre-transfer', myTransfers, memo);
const goods = Object.keys(myTransfers);
for (let i = 0, l = goods.length;i < l;++i) {
if (this.inventory[goods[i]])
this.inventory[goods[i]] += myTransfers[goods[i]];
else
this.inventory[goods[i]] = myTransfers[goods[i]];
}
this.emit('post-transfer', myTransfers, memo);
}
}
/**
* agent's marginal cost of producing next unit
*
* @param {String} good (e.g. "X", "Y")
* @param {Object} hypotheticalInventory object with goods as keys and values as numeric levels of inventory
* @return {number} marginal unit cost of next unit, at given (negative) hypothetical inventory, using agent's configured costs
*/
unitCostFunction(good, hypotheticalInventory) {
const costs = this.costs[good];
if ((Array.isArray(costs)) && (hypotheticalInventory[good] <= 0)) {
return costs[-hypotheticalInventory[good]];
}
}
/**
* agent's marginal value for redeeming next unit
*
* @param {String} good (e.g. "X", "Y")
* @param {Object} hypotheticalInventory object with goods as keys and values as numeric levels of inventory
* @return {number} marginal unit value of next unit, at given (positive) hypothetical inventory, using agent's configured values
*/
unitValueFunction(good, hypotheticalInventory) {
const vals = this.values[good];
if ((Array.isArray(vals)) && (hypotheticalInventory[good] >= 0)) {
return vals[hypotheticalInventory[good]];
}
}
/**
* redeems units in positive inventory with configured values, usually called automatically at end-of-period.
* transfer uses memo object {isRedeem:1}
*
* @emits {pre-redeem(transferAmounts)} before calling .transfer, can modify transferAmounts
* @emits {post-redeem(transferAmounts)} after calling .transfer
*/
redeem() {
if (this.values) {
const trans = {};
const goods = Object.keys(this.values);
trans[this.money] = 0;
for (let i = 0, l = goods.length;i < l;++i) {
let g = goods[i];
if (this.inventory[g] > 0) {
trans[g] = -this.inventory[g];
trans[this.money] += sum(this.values[g].slice(0, this.inventory[g]));
}
}
this.emit('pre-redeem', trans);
this.transfer(trans, { isRedeem: 1 });
this.emit('post-redeem', trans);
}
}
/**
* produces units in negative inventory with configured costs, usually called automatically at end-of-period.
* transfer uses memo object {isProduce:1}
*
* @emits {pre-redeem(transferAmounts)} before calling .transfer, can modify transferAmounts
* @emits {post-redeem(transferAmounts)} after calling .transfer
*/
produce() {
if (this.costs) {
const trans = {};
const goods = Object.keys(this.costs);
trans[this.money] = 0;
for (let i = 0, l = goods.length;i < l;++i) {
let g = goods[i];
if (this.inventory[g] < 0) {
trans[this.money] -= sum(this.costs[g].slice(0, -this.inventory[g]));
trans[g] = -this.inventory[g];
}
}
this.emit('pre-produce', trans);
this.transfer(trans, { isProduce: 1 });
this.emit('post-produce', trans);
}
}
}
/**
* agent that places trades in one or more markets based on marginal costs or values
*
* This is an abstract class, meant to be subclassed for particular strategies.
*
*/
export class Trader extends Agent {
/**
* @param {Object} [options] passed to Agent constructor(); Trader specific properties detailed below
* @param {Array<Object>} [options.markets=[]] list of market objects where this agent acts on wake
* @param {number} [options.minPrice=0] minimum price when submitting limit orders to buy
* @param {number} [options.maxPrice=1000] maximum price when submitting sell limit orders to sell
* @param {boolean} [options.ignoreBudgetConstraint=false] ignore budget constraint, substituting maxPrice for unit value when bidding, and minPrice for unit cost when selling
* @listens {wake} to trigger sendBidsAndAsks()
*
*/
constructor(options) {
const defaults = {
description: 'Trader',
markets: [],
minPrice: 0,
maxPrice: 1000
};
super(Object.assign({}, defaults, options));
this.on('wake', this.sendBidsAndAsks);
}
/** send a limit order to buy one unit to the indicated market at myPrice. Placeholder throws error. Must be overridden and implemented in other code.
* @abstract
* @param {Object} market
* @param {number} myPrice
* @throws {Error} when calling placeholder
*/
// eslint-disable-next-line no-unused-vars
bid(market, myPrice) {
throw new Error("called placeholder for abstract method .bid(market,myPrice) -- you must implement this method");
}
/**
* send a limit order to sell one unit to the indicated market at myPrice. Placeholder throws error. Must be overridden and implemented in other code.
* @abstract
* @param {Object} market
* @param {number} myPrice
* @throws {Error} when calling placeholder
*/
// eslint-disable-next-line no-unused-vars
ask(market, myPrice) {
throw new Error("called placeholder for abstract method .ask(market,myPrice) -- you must implement this method");
}
/**
* calculate price this agent is willing to pay. Placeholder throws error. Must be overridden and implemented in other code.
*
* @abstract
* @param {number} marginalValue The marginal value of redeeming the next unit.
* @param {Object} market For requesting current market conditions, previous trade price, etc.
* @return {number|undefined} agent's buy price or undefined if not willing to buy
* @throws {Error} when calling placeholder
*/
// eslint-disable-next-line no-unused-vars
bidPrice(marginalValue, market) {
throw new Error("called placeholder for abstract method .bidPrice(marginalValue, market) -- you must implement this method");
}
/**
* calculate price this agent is willing to accept. Placeholder throws error. Must be overridden and implemented in other code.
*
*
* @abstract
* @param {number} marginalCost The marginal cost of producing the next unit.
* @param {Object} market For requesting current market conditions, previous trade price, etc.
* @return {number|undefined} agent's sell price or undefined if not willing to sell
* @throws {Error} when calling placeholder
*/
// eslint-disable-next-line no-unused-vars
askPrice(marginalCost, market) {
throw new Error("called placeholder for abstract method .askPrice(marginalValue, market) -- you must implement this method");
}
/**
* For each market in agent's configured markets, calculates agent's price strategy for buy or sell prices and then sends limit orders for 1 unit at those prices.
* Normally you do not need to explicltly call this function: the wake listener set in the constructor of Trader and subclasses calls sendBidsAndAsks() automatcally on each wake event.
*
*
*/
sendBidsAndAsks() {
for (let i = 0, l = this.markets.length;i < l;++i) {
let market = this.markets[i];
let unitValue = this.unitValueFunction(market.goods, this.inventory);
if (unitValue > 0) {
if (this.ignoreBudgetConstraint)
unitValue = this.maxPrice;
let myPrice = this.bidPrice(unitValue, market); // calculate my buy price proposal
if (myPrice){
this.bid(market, myPrice); // send my price proposal
}
}
let unitCost = this.unitCostFunction(market.goods, this.inventory);
if (unitCost > 0) {
if (this.ignoreBudgetConstraint)
unitCost = this.minPrice;
let myPrice = this.askPrice(unitCost, market); // calculate my sell price proposal
if (myPrice){
this.ask(market, myPrice); // send my price proposal
}
}
}
}
uniformRandom(a,b){
if (typeof(a)!=='number') throw new TypeError('a '+a+' not a number');
if (typeof(b)!=='number') throw new TypeError('b '+b+' not a number');
if (a===b) return a;
if (a>b) throw new RangeError('a '+a+' should be less than b '+b);
const offset = (this.integer)? 1: 0;
const uRandom = ProbJS.uniform(a,b+offset);
let p;
if (this.integer) {
/* because Floor rounds down, 1 has been added to b to be in the range of possible prices */
/* guard against rare edge case with do/while */
do {
p = Math.floor(uRandom());
} while (p > b);
} else {
p = uRandom();
}
return p;
}
}
export class DoNothingAgent extends Trader {
/**
* creates do-nothing agent that never sends any bids or asks
* @param {Object} [options] passed to Trader and Agent constructors
*/
constructor(options) {
super(Object.assign({}, { description: 'DoNothing agent never bids or asks', color:"black" }, options));
}
bidPrice() {
return undefined;
}
askPrice() {
return undefined;
}
}
export class TruthfulAgent extends Trader {
/**
* creates "Truthful" robot agent that always sends bids at marginalValue or asks at marginalCost
*
* @param {Object} [options] passed to Trader and Agent constructors
*
*/
constructor(options) {
super(Object.assign({}, { description: 'Truthful Agent bids=value or asks=cost', color:"rosybrown" }, options));
}
bidPrice(marginalValue) {
if (typeof(marginalValue) !== 'number') return undefined;
return (this.integer) ? Math.floor(marginalValue) : marginalValue;
}
askPrice(marginalCost) {
if (typeof(marginalCost) !== 'number') return undefined;
return (this.integer) ? Math.ceil(marginalCost) : marginalCost;
}
}
export class DPPAgent extends Trader {
/**
* creates Decaying Potential Profit robot agent that prices items with a time-based exponentially decaying profit
*
* @param {Object} [options] passed to Trader and Agent constructors
*
*/
constructor(options){
super(Object.assign({}, { description: 'DPP Agent prices in a time-based decaying profit', color:"maroon" }, options));
}
bidPrice(marginalValue){
if (typeof(marginalValue)!=='number') return undefined;
if (marginalValue<this.minPrice) return undefined;
// set arbitrary lower price of 0.001 if minPrice otherwise too small
const iv = (this.minPrice<=0)? 0.001: this.minPrice;
const fv = marginalValue;
return this.decay(iv,fv);
}
askPrice(marginalCost){
if (typeof(marginalCost)!=='number') return undefined;
if (marginalCost>this.maxPrice) return undefined;
const iv = this.maxPrice;
const fv = (marginalCost<=0)? 0.001: marginalCost;
return this.decay(iv,fv);
}
}
export class HoarderAgent extends Trader {
/**
* creates "Hoarder" robot agent that always buys 1 unit at the current asking price.
* Hoarder agent never sells units, and disregards marginalValue, and so will sometimes overpay relative to value.
* Hoarder does not interact with an empty market.
*
*/
constructor(options) {
super(Object.assign({}, { description: 'Hoarder Agent always bids the current asking price and never asks', color:"hotpink" }, options));
}
bidPrice(marginalValue, market) {
const currentAskPrice = market.currentAskPrice();
if (currentAskPrice > 0)
return currentAskPrice; // Hoarder will send order to buy 1 unit at the current asking price
}
askPrice() {
return undefined; // Hoarder never sells
}
}
/**
* a reimplementation of Gode and Sunder's "Zero Intelligence" robots, as described in the economics research literature.
*
* see
*
* Gode, Dhananjay K., and S. Sunder. [1993]. ‘Allocative efficiency of markets with zero-intelligence traders: Market as a partial substitute for individual rationality.’ Journal of Political Economy, vol. 101, pp.119-137.
*
* Gode, Dhananjay K., and S. Sunder. [1993b]. ‘Lower bounds for efficiency of surplus extraction in double auctions.’ In Friedman, D. and J. Rust (eds). The Double Auction Market: Institutions, Theories, and Evidence, pp. 199-219.
*
* Gode, Dhananjay K., and S. Sunder. [1997a]. ‘What makes markets allocationally efficient?’ Quarterly Journal of Economics, vol. 112 (May), pp.603-630.
*
*/
export class ZIAgent extends Trader {
/**
* creates "Zero Intelligence" robot agent similar to those described in Gode and Sunder (1993)
*
* @param {Object} [options] passed to Trader and Agent constructors()
* @param {boolean} [options.integer] true instructs pricing routines to use positive integer prices, false allows positive real number prices
*/
constructor(options) {
super(Object.assign({}, { description: 'Gode and Sunder Style ZI Agent', color:"green" }, options));
}
/**
* calculate price this agent is willing to pay as a uniform random number ~ U[minPrice, marginalValue] inclusive.
* If this.integer is true, the returned price will be an integer.
*
*
* @param {number} marginalValue the marginal value of redeeming the next unit. sets the maximum price for random price generation
* @return {number|undefined} randomized buy price or undefined if marginalValue non-numeric or less than this.minPrice
*/
bidPrice(marginalValue) {
if (typeof(marginalValue) !== 'number') return undefined;
if (marginalValue < this.minPrice) return undefined;
return this.uniformRandom(this.minPrice, marginalValue);
}
/**
* calculate price this agent is willing to accept as a uniform random number ~ U[marginalCost, maxPrice] inclusive.
* If this.integer is true, the returned price will be an integer.
*
*
* @param {number} marginalCost the marginal coat of producing the next unit. sets the minimum price for random price generation
* @return {number|undefined} randomized sell price or undefined if marginalCost non-numeric or greater than this.maxPrice
*/
askPrice(marginalCost) {
if (typeof(marginalCost) !== 'number') return undefined;
if (marginalCost > this.maxPrice) return undefined;
return this.uniformRandom(marginalCost, this.maxPrice);
}
}
/**
* ZIJump agent: uses ZIAgent algorithm if there is no current Bid or Ask price. Afterward, randomizes over [currentBid,V] or [c,currentAsk].
*
*/
export class ZIJumpAgent extends ZIAgent {
/**
* creates "ZIJump" robot agent, a ZI that matches or improves on current bid or current ask
*
* @param {Object} [options] passed to ZIAgent, Trader, Agent constructors
*/
constructor(options){
const defaults = {
description: "ZIJump agent that bids/asks randomly to match or improve current Bid or Ask",
color: 'coffee' // color about halfway between green (ZI) and orangered (OneupmanshipAgent)
};
super(Object.assign({},defaults,options));
}
bidPrice(marginalValue, market){
if (typeof(marginalValue)!=='number') return undefined;
const currentBid = market.currentBidPrice();
if (currentBid===undefined) return super.bidPrice(marginalValue);
const lowerLimit = Math.max(currentBid, this.minPrice);
if (lowerLimit>marginalValue) return undefined;
return this.uniformRandom(lowerLimit, marginalValue);
}
askPrice(marginalCost, market){
if (typeof(marginalCost)!=='number') return undefined;
const currentAsk = market.currentAskPrice();
if (currentAsk===undefined) return super.askPrice(marginalCost);
const upperLimit = Math.min(currentAsk, this.maxPrice);
if (marginalCost>upperLimit) return undefined;
return this.uniformRandom(marginalCost, upperLimit);
}
}
/**
* ZISpread agent: equivalent to ZIAgent if there is no current Bid or Ask price.
* prices are distributed U[currentBid,currentAsk] if V>=currentAsk or C<=currentBid
* U[currentBid,V] if currentBid<=V<=currentAsk
* U[c,currentAsk] if currentBid<=c<=currenAsk
* undefined if V<currentBid
* undefined if c>currentAsk
*
*/
export class ZISpreadAgent extends Trader {
/**
* creates "ZISpread" robot agent
*
* @param {Object} [options] passed to Trader, Agent constructors
*/
constructor(options){
const defaults = {
description: "ZISpread agent that bids/asks randomly within the intersection of bid-ask spread and budget ",
color: 'chartreuse' // color about halfway between green (ZI) and goldenrod (MidpointAgent)
};
super(Object.assign({},defaults,options));
}
bidPrice(marginalValue, market){
if (typeof(marginalValue)!=='number') return undefined;
const currentBid = market.currentBidPrice();
const currentAsk = market.currentAskPrice();
let lower = this.minPrice;
let upper = marginalValue;
if (currentBid>lower) lower = currentBid;
if ((currentAsk>0) && (currentAsk<upper)) upper=currentAsk;
if (lower>upper) return undefined;
return this.uniformRandom(lower,upper);
}
askPrice(marginalCost, market){
if (typeof(marginalCost)!=='number') return undefined;
const currentBid = market.currentBidPrice();
const currentAsk = market.currentAskPrice();
let lower = marginalCost;
let upper = this.maxPrice;
if (currentBid>lower) lower = currentBid;
if ((currentAsk>0) && (currentAsk<upper)) upper=currentAsk;
if (lower>upper) return undefined;
return this.uniformRandom(lower,upper);
}
}
const um1p2 = ProbJS.uniform(-1, 2);
const um1p1 = ProbJS.uniform(-1, 1);
/**
* Unit agent: uses ZIAgent algorithm if there is no previous market price, afterward, bids/asks randomly within 1 price unit of previous price
*
* see also Brewer, Paul Chapter 4 in Handbook of Experimental Economics Results, Charles R. Plott and Vernon L. Smith, eds., Elsevier: 2008
*
* Chapter available on Google Books at https://books.google.com search for "Handbook of Experimental Economics Results" and go to pp. 31-45.
* or on Science Direct (paywall) at http://www.sciencedirect.com/science/article/pii/S1574072207000042
*
*
*
*/
export class UnitAgent extends ZIAgent {
/**
* creates "Unit" robot agent similar to those described in Brewer(2008)
*
* @param {Object} [options] passed to Trader and Agent constructors()
*/
constructor(options) {
const defaults = {
description: "Paul Brewer's UNIT agent that bids/asks within 1 price unit of previous price",
color: 'lime'
};
super(Object.assign({}, defaults, options));
}
/**
* calculates random change from previous transaction price
* @return {number} a uniform random number on [-1,1]; or, if this.integer is set, picked randomly from the set {-1,0,1}
*/
randomDelta() {
let delta;
if (this.integer) {
do {
delta = Math.floor(um1p2());
} while ((delta <= -2) || (delta >= 2.0));
} else {
do {
delta = um1p1();
} while ((delta < -1) || (delta > 1));
}
return delta;
}
/**
* Calculate price this agent is willing to pay.
* The returned price is within one price unit of the previous market trade price, or uses the ZIAgent random algorithm if there is no previous market trade price.
* Undefined (no bid) is returned if the propsed price would exceed the marginalValue parameter
* If this.integer is true, the returned price will be an integer.
*
*
* @param {number} marginalValue the marginal value of redeeming the next unit. sets the maximum price for allowable random price generation
* @param {Object} market The market for which a bid is being prepared. An object with lastTradePrice() method.
* @return {number|undefined} agent's buy price or undefined
*/
bidPrice(marginalValue, market) {
let p;
if (typeof(marginalValue) !== 'number') return undefined;
const previous = market.lastTradePrice();
if (previous)
p = previous + this.randomDelta();
else
p = super.bidPrice(marginalValue);
if ((p > marginalValue) || (p > this.maxPrice) || (p < this.minPrice)) return undefined;
return (p && this.integer) ? Math.floor(p) : p;
}
/**
* Calculate price this agent is willing to accept.
* The returned price is within one price unit of the previous market trade price, or uses the ZIAgent random algorithm if there is no previous market trade price.
* Undefined (no ask) is returned if the propsed price would be lower than the marginalCost parameter
* If this.integer is true, the returned price will be an integer.
*
*
* @param {number} marginalCost the marginal cost of producing the next unit. sets the minimum price for allowable random price generation
* @param {Object} market The market for which a bid is being prepared. An object with lastTradePrice() method.
* @return {number|undefined} agent's buy price or undefined
*/
askPrice(marginalCost, market) {
if (typeof(marginalCost) !== 'number') return undefined;
let p;
const previous = market.lastTradePrice();
if (previous)
p = previous + this.randomDelta();
else
p = super.askPrice(marginalCost);
if ((p < marginalCost) || (p > this.maxPrice) || (p < this.minPrice)) return undefined;
return (p && this.integer) ? Math.floor(p) : p;
}
}
export class TTAgent extends ZIAgent {
constructor(options){
const defaults = {
description: "Paul Brewer's TT agent that optimizes over a collated database of trades; degrades to ZI when no data/currentBid/currentAsk",
color: 'orange'
};
super(Object.assign({}, defaults, options));
const agent = this; // eslint-disable-line consistent-this
// allow tts creation to be overridden in options
// if it does not exist, create it and hook it up to necessary period and trade data
if (!agent.tts){
agent.tts = new TradeTimingStrategy();
agent.tts.connected = false;
agent.on('pre-period', function(){
agent.tts.newPeriod();
if (!agent.tts.connected){
agent.tts.connected = true;
agent.markets[0].on('trade', function(tradeSpec){
const { prices } = tradeSpec;
if (Array.isArray(prices))
prices.forEach((p)=>{agent.tts.newTrade(p);});
});
}
});
}
}
bidPrice(marginalValue,market){
if (typeof(marginalValue)!=='number') return undefined;
const currentBid = market.currentBidPrice();
const currentAsk = market.currentAskPrice();
const smooth = (currentBid || currentAsk)? 0.001 : 0;
const horizon = Math.round((1-this.pctPeriod())*(this.tts.tradeCollator.length-2));
const ttsBid = this.tts.suggestedBid(marginalValue,{currentBid,currentAsk,smooth,horizon});
if (ttsBid===undefined) return super.bidPrice(marginalValue); // revert to ZI if insufficient data
return (this.integer)? Math.floor(ttsBid): ttsBid;
}
askPrice(marginalCost, market) {
if (typeof(marginalCost) !== 'number') return undefined;
const currentBid = market.currentBidPrice();
const currentAsk = market.currentAskPrice();
const smooth = (currentBid || currentAsk)? 0.001 : 0;
const horizon = Math.round((1-this.pctPeriod())*(this.tts.tradeCollator.length-2));
const ttsAsk = this.tts.suggestedAsk(marginalCost,{currentBid,currentAsk,smooth,horizon});
if (ttsAsk===undefined) return super.askPrice(marginalCost); // revert to ZI if insufficient data
return (this.integer)? Math.ceil(ttsAsk): ttsAsk;
}
}
/**
* OneupmanshipAgent is a robotic version of that annoying market participant who starts at extremely high or low price, and always bid $1 more, or ask $1 less than any competition
*
*/
export class OneupmanshipAgent extends Trader {
/**
* create OneupmanshipAgent
* @param {Object} [options] Passed to Trader and Agent constructors
*
*/
constructor(options) {
const defaults = {
description: "Brewer's OneupmanshipAgent that increases the market bid or decreases the market ask by one price unit, if profitable to do so according to MV or MC",
color: 'orangered'
};
super(Object.assign({}, defaults, options));
}
/**
* Calculate price this agent is willing to pay.
* The returned price is either this.minPrice (no bidding), or market.currentBidPrice()+1, or undefined.
* Undefined (no bid) is returned if the propsed price would exceed the marginalValue parameter
* this.integer is ignored
*
*
* @param {number} marginalValue the marginal value of redeeming the next unit. sets the maximum price for allowable bidding
* @param {Object} market The market for which a bid is being prepared. An object with currentBidPrice() and currentAskPrice() methods.
* @return {number|undefined} agent's buy price or undefined
*/
bidPrice(marginalValue, market) {
if (typeof(marginalValue) !== 'number') return undefined;
const currentBid = market.currentBidPrice();
if (!currentBid)
return this.minPrice;
if (currentBid < (marginalValue - 1))
return currentBid + 1;
}
/**
* Calculate price this agent is willing to accept.
* The returned price is either this.maxPrice (no asks), or market.currentAskPrice()-1, or undefined.
* Undefined (no bid) is returned if the propsed price is less than the marginalCost parameter
* this.integer is ignored
*
*
* @param {number} marginalCost the marginal cost of producing the next unit. sets the minimum price for allowable bidding
* @param {Object} market The market for which a bid is being prepared. An object with currentBidPrice() and currentAskPrice() methods.
* @return {number|undefined} agent's buy price or undefined
*/
askPrice(marginalCost, market) {
if (typeof(marginalCost) !== 'number') return undefined;
const currentAsk = market.currentAskPrice();
if (!currentAsk)
return this.maxPrice;
if (currentAsk > (marginalCost + 1))
return currentAsk - 1;
}
}
/**
* MidpointAgent - An agent that bids/asks halfway between the current bid and current ask.
* When there is no current bid or current ask, the agent bids minPrice or asks maxPrice.
*
*/
export class MidpointAgent extends Trader {
constructor(options) {
const defaults = {
description: "Brewer's MidpointAgent bids/asks halfway between the bid and ask, if profitable to do according to MC or MV",
color: 'goldenrod'
};
super(Object.assign({}, defaults, options));
}
/**
* Calculate price this agent is willing to pay.
* The returned price is either the min price, the midpoint of the bid/ask, or undefined.
* Undefined (no bid) is returned if the propsed price would exceed the marginalValue parameter
* this.integer==true causes midpoint prices to be rounded up to the next integer before comparison with marginalValue
*
* @param {number} marginalValue the marginal value of redeeming the next unit. sets the maximum price for allowable bidding
* @param {Object} market The market for which a bid is being prepared. An object with currentBidPrice() and currentAskPrice() methods.
* @return {number|undefined} agent's buy price or undefined
*/
bidPrice(marginalValue, market) {
if (typeof(marginalValue) !== 'number') return undefined;
const currentBid = market.currentBidPrice();
if (!currentBid)
return (this.minPrice <= marginalValue) ? this.minPrice : undefined;
const currentAsk = market.currentAskPrice();
if (currentAsk) {
const midpoint = (currentBid + currentAsk) / 2;
const myBid = (this.integer) ? Math.ceil(midpoint) : midpoint;
if (myBid <= marginalValue)
return myBid;
}
}
/**
* Calculate price this agent is willing to accept.
* The returned price is either the max price, the midpoint of the bid/ask, or undefined.
* Undefined (no ask) is returned if the propsed price is less than the marginalCost parameter
* this.integer==true causes midpoint prices to be rounded up to the next integer before comparison with marginalValue
*
*
* @param {number} marginalCost the marginal cost of producing the next unit. sets the minimum price for allowable bidding
* @param {Object} market The market for which a bid is being prepared. An object with currentBidPrice() and currentAskPrice() methods.
* @return {number|undefined} agent's buy price or undefined
*/
askPrice(marginalCost, market) {
if (typeof(marginalCost) !== 'number') return undefined;
const currentAsk = market.currentAskPrice();
if (!currentAsk)
return (this.maxPrice >= marginalCost) ? this.maxPrice : undefined;
const currentBid = market.currentBidPrice();
if (currentBid) {
const midpoint = (currentBid + currentAsk) / 2;
const myAsk = (this.integer) ? Math.floor(midpoint) : midpoint;
if (myAsk >= marginalCost)
return myAsk;
}
}
}
export class Sniper extends Trader {
constructor(options) {
const defaults = {
buyOnCloseTime: 0,
sellOnCloseTime: 0
};
super(Object.assign({}, defaults, options));
}
buyNow() {
throw new Error("buyNow() remains abstract");
}
sellNow() {
throw new Error("sellNow() remains abstract");
}
bidPrice(marginalValue, market) {
if (typeof(marginalValue) !== 'number') return undefined;
const currentAsk = market.currentAskPrice();
if (currentAsk <= marginalValue) {
if (this.buyNow(marginalValue, market)) return currentAsk;
if ((this.buyOnCloseTime > 0) && (this.wakeTime >= this.buyOnCloseTime)) return currentAsk;
}
}
askPrice(marginalCost, market) {
if (typeof(marginalCost) !== 'number') return undefined;
const currentBid = market.currentBidPrice();
if (currentBid >= marginalCost) {
if (this.sellNow(marginalCost, market)) return currentBid;
if ((this.sellOnCloseTime > 0) && (this.wakeTime >= this.sellOnCloseTime)) return currentBid;
}
}
}
/**
* a reimplementation of a Kaplan Sniper Agent (JavaScript implementation by Paul Brewer)
*
* see e.g. "High Performance Bidding Agents for the Continuous Double Auction"
* Gerald Tesauro and Rajarshi Das, Institute for Advanced Commerce, IBM
*
* http://researcher.watson.ibm.com/researcher/files/us-kephart/dblauc.pdf
*
* for discussion of Kaplan's Sniper traders on pp. 4-5
*/
export class KaplanSniperAgent extends Sniper {
/**
* Create KaplanSniperAgent
*
* @param {Object} [options] options passed to Trader and Agent constructors
* @param {number} [options.desiredSpread=10] desiredSpread for sniping; agent will accept trade if ||market.currentAskPrice()-market.currentBidPrice()||<=desiredSpread
*/
constructor(options) {
const defaults = {
description: "Kaplan's snipers, trade on 'juicy' price, or low spread, or end of period",
desiredSpread: 10,
nearEndOfPeriod: 10,
color: 'navy'
};
super(Object.assign({}, defaults, options));
}
/**
* Calculates price this agent is willing to pay.
* The returned price always equals either undefined or the price of market.currentAsk(), triggering an immediate trade.
*
* The KaplanSniperAgent will buy, if market.currentAskPrice<=marginalValue, during one of three conditions:
* (A) market ask price is less than or equal to .getJuicyAskPrice(), which needs to be set at the simulation level to the previous period low trade price
* (B) when spread = (market ask price - market bid price) is less than or equal to agent's desiredSpread (default: 10)
* (C) when period is ending
*
*/
isLowSpread(market) {
const currentBid = market.currentBidPrice();
const currentAsk = market.currentAskPrice();
return ((currentAsk > 0) && (currentBid > 0) && ((currentAsk - currentBid) <= this.desiredSpread));
}
buyNow(marginalValue, market) {
const isJuicyPrice = (market.currentAskPrice() <= market.previousPeriod('lowPrice'));
if (isJuicyPrice) return true;
if (this.isLowSpread(market)) return true;
/* istanbul ignore else */
if (this.poissonWakesRemainingInPeriod() <= this.nearEndOfPeriod) return true;
}
sellNow(marginalCost, market) {
const isJuicyPrice = (market.currentBidPrice() >= market.previousPeriod('highPrice'));
if (isJuicyPrice) return true;
if (this.isLowSpread(market)) return true;
/* istanbul ignore else */
if (this.poissonWakesRemainingInPeriod() <= this.nearEndOfPeriod) return true;
}
}
export class MedianSniperAgent extends Sniper {
/**
* Create MedianSniperAgent
*
* @param {Object} [options] options passed to Trader and Agent constructors
*/
constructor(options) {
const defaults = {
description: "Median snipers, trade on price better than previous period median, or at end of period",
nearEndOfPeriod: 10,
color: 'aqua'
};
super(Object.assign({}, defaults, options));
}
buyNow(marginalValue, market) {
if (market.currentAskPrice() <= market.previousPeriod('medianPrice')) return true;
/* istanbul ignore else */
if (this.poissonWakesRemainingInPeriod() <= this.nearEndOfPeriod) return true;
}
sellNow(marginalCost, market) {
if (market.currentBidPrice() >= market.previousPeriod('medianPrice')) return true;
/* istanbul ignore else */
if (this.poissonWakesRemainingInPeriod() <= this.nearEndOfPeriod) return true;
}
}
export class AcceptSniperAgent extends Sniper {
/**
* Create AcceptSniperAgent from Sniper
* @param {Object} [options] to Trader and Agent constructors
*/
constructor(options){
const defaults = {
description: "AcceptSniperAgent, accepts any bid/ask from other side of market that meets no-loss constraint but does not make bids/asks",
color: 'purple'
};
super(Object.assign({},defaults,options));
}
buyNow(){ return true; }
sellNow(){ return true; }
}
export class RandomAcceptSniperAgent extends Sniper {
/**
* Create RandomAcceptSniperAgent from Sniper
* @param {Object} [options] to Trader and Agent constructors
*/
constructor(options){
const defaults = {
description: "RandomAcceptSniperAgent, at a probability between 0-1 (also determined randomly, once, at initialization) randomly accepts any bid/ask from other side of market that meets no-loss constraint. Does not make bids/asks",
color: 'darkred'
};
super(Object.assign({},defaults,options));
this.acceptRate = ProbJS.uniform(0.0,1.0);
}
accept(){
const r = ProbJS.uniform(0.0,1.0);
if (r<=this.acceptRate) return true;
return false;
}
buyNow(){
return this.accept();
}
sellNow(){
return this.accept();
}
}
export class FallingAskSniperAgent extends Sniper {
constructor(options){
const defaults = {
description: 'Sniper waits for Ask below previous trade price',
color: 'saddlebrown'
};
super(Object.assign({},defaults,options));
}
isFallingAsk(market){
const last = market.lastTradePrice();
const ask = market.currentAskPrice();
return ((last>0) && (ask>0) && (ask<last));
}
buyNow(marginalValue,market){
return this.isFallingAsk(market);
}
sellNow(marginalCost,market){
return this.isFallingAsk(market);
}
}
export class RisingBidSniperAgent extends Sniper {
constructor(options){
const defaults = {
description: 'Sniper waits for Bid above previous trade price',
color: 'darkslategrey'
};
super(Object.assign({},defaults,options));
}
isRisingBid(market){
const last = market.lastTradePrice();
const bid = market.currentBidPrice();
return ((last>0) && (bid>0) && (bid>last));
}
buyNow(marginalValue,market){
return this.isRisingBid(market);
}
sellNow(marginalCost,market){
return this.isRisingBid(market);
}
}
/**
* Pool for managing a collection of agents.
* Agents may belong to multiple pools.
*
*/
export class Pool {
constructor() {
this.agents = [];
this.agentsById = {};
}
/**
* Add an agent to the Pool
* @param {Object} agent to add to pool. Should be instanceof Agent, including subclasses.
*/
push(agent) {
if (!(agent instanceof Agent))
throw new Error("Pool.push(agent), agent is not an instance of Agent or descendents");
if (this.agentsById[agent.id]) {
throw new Error("Pool.push(agent), conflict: new agent has id of existing agent");
}
this.agents.push(agent);
this.agentsById[agent.id] = agent;
}
/**
* finds agent from Pool with lowest wakeTime
* @return {Object}
*/
next() {
if (this.nextCache!==undefined) return this.nextCache;
let tMin = 1e20,
i = 0,
l = this.agents.length,
A = this.agents,
t = 0,
result = 0,
endPeriod;
for (; i < l;i++) {
t = A[i].wakeTime;
endPeriod = A[i].period && A[i].period.endTime;
if (
(t > 0) &&
(t < tMin) &&
(
(endPeriod===undefined) ||
((endPeriod > 0) && (t < endPeriod))
)
) {
result = A[i];
tMin = t;
}
}
this.nextCache = result;
return result;
}
/**
* wakes agent in Pool with lowest wakeTime
*/
wake() {
const A = this.next();
/* istanbul ignore else */
if (A) {
A.wake();
// wipe nextCache
delete this.nextCache;
}
}
/**
* finds latest period.endTime of all agent in Pool
* @return {number} max of agents period.endTime
*/
endTime() {
let endTime = 0;
for (let i = 0, l = this.agents.length;i < l;++i) {
let a = this.agents[i];
if (a.period.endTime > endTime)
endTime = a.period.endTime;
}
if (endTime > 0) return endTime;
}
/**
* Repeatedly wake agents in Pool, until simulation time "untilTime" is reached. For a synchronous equivalent, see syncRun(untilTime, limitCalls)
*
* @param {number} untilTime Stop time for this run
* @param {number} batch Batch size of number of agents to wake up synchronously before surrendering to event loop
* @return {Promise<Object,Error>} returns promise resolving to pool, with caught errors passed to reject handler.
*/
runAsPromise(untilTime, batch) {
const pool = this; // eslint-disable-line consistent-this
return new Promise(function (resolve, reject) {
function loop() {
let nextAgent = 0;
/* can not test catch() block so drop from test coverage */
try {
pool.syncRun(untilTime, (batch || 1));
nextAgent = pool.next();
} // eslint-disable-line brace-style
/* c8 ignore start */
catch (e) { // eslint-disable-line brace-style
return reject(e);
}
/* c8 ignore stop */
return (nextAgent && (nextAgent.wakeTime < untilTime)) ? setImmediate(loop) : resolve(pool);
}
setImmediate(loop);
});
}
/**
* Repeatedly wake agents in Pool, until simulation time "untilTime" or "limitCalls" agent wake calls are reached.
* This method runs synchronously. It returns only when finished.
*
* @param {number} untilTime Stop time for this run
* @param {number} [limitCalls] Stop run once this number of agent wake up calls have been executed.
*
*/
syncRun(untilTime, limitCalls) {
let nextAgent = this.next();
let calls = 0;
while (nextAgent && (nextAgent.wakeTime < untilTime) && (!(calls >= limitCalls))) {
this.wake();
nextAgent = this.next();
calls++;
}
}
/**
* calls .initPeriod for all agents in the Pool
*
* @param {Object|number} param passed to each agent's .initPeriod()
*/
initPeriod(param) {
// clear the nextCache since the agent wakeTimes will be reset
delete this.nextCache;
// passing param to all the agents is safe because Agent.initPeriod does a deep clone
if (Array.isArray(param) && (param.length > 0)) {
for (let i = 0, l = this.agents.length;i < l;i++)
this.agents[i].initPeriod(param[i % (param.length)]);
} else {
for (let i = 0, l = this.agents.length;i < l;i++)
this.agents[i].initPeriod(param);
}
}
/**
* calls .endPeriod for all agents in the Pool
*/
endPeriod() {
for (let i = 0, l = this.agents.length;i < l;i++)
this.agents[i].endPeriod();
}
/**
* adjusts Pool agents inventories, via agent.transfer(), in response to one or more trades
* @param {Object} tradeSpec Object providing specifics of trades.
* @param {string} tradeSpec.bs 'b' for buy trade, 's' for sell trade. In a buy trade, buyQ, buyId are single element arrays. In a sell trade, sellQ, sellId are single element arrays,
* @param {string} tradeSpec.goods the name of the goods, as stored in agent inventory object
* @param {string} tradeSpec.money the name of money used for payment, as stored in agent inventory object
* @param {number[]} tradeSpec.prices the price of each trade
* @param {number[]} tradeSpec.buyId the agent id of a buyer in a trade
* @param {number[]} tradeSpec.buyQ the number bought by the corresponding agent in .buyId
* @param {number[]} tradeSpec.sellId the agent id of a seller in a trade
* @param {number[]} tradeSPec.sellQ the number bought by he corresponding agent in .sellId
* @throws {Error} when accounting identities do not balance or trade invalid
*/
trade(tradeSpec) {
let i, l, buyerTransfer, sellerTransfer;
if (typeof(tradeSpec) === 'undefined') return;
const { bs, goods, money, prices, buyQ, sellQ, buyId, sellId } = tradeSpec;
if (
(bs) &&
(goods) &&
(money) &&
(Array.isArray(prices)) &&
(Array.isArray(buyQ)) &&
(Array.isArray(sellQ)) &&
(Array.isArray(buyId)) &&
(Array.isArray(sellId))) {
if (bs === 'b') {
if (buyId.length !== 1)
throw new Error("Pool.trade expected tradeSpec.buyId.length===1, got:" + buyId.length);
if (buyQ[0] !== sum(sellQ))
throw new Error("Pool.trade invalid buy -- tradeSpec buyQ[0] != sum(sellQ)");
buyerTransfer = {};
buyerTransfer[goods] = buyQ[0];
buyerTransfer[money] = -dot(sellQ, prices);
this.agentsById[buyId[0]].transfer(buyerTransfer, { isTrade: 1, isBuy: 1 });
for (i = 0, l = prices.length;i < l;++i) {
sellerTransfer = {};
sellerTransfer[goods] = -sellQ[i];
sellerTransfer[money] = prices[i] * sellQ[i];
this.agentsById[sellId[i]].transfer(sellerTransfer, { isTrade: 1, isSellAccepted: 1 });
}
} else if (bs === 's') {
if (sellId.length !== 1)
throw new Error("Pool.trade expected tradeSpec.sellId.length===1. got:" + sellId.length);
if (sellQ[0] !== sum(buyQ))
throw new Error("Pool.trade invalid sell -- tradeSpec sellQ[0] != sum(buyQ)");
sellerTransfer = {};
sellerTransfer[goods] = -sellQ[0];
sellerTransfer[money] = dot(buyQ, prices);
this.agentsById[sellId[0]].transfer(sellerTransfer, { isTrade: 1, isSell: 1 });
for (i = 0, l = prices.length;i < l;++i) {
buyerTransfer = {};
buyerTransfer[goods] = buyQ[i];
buyerTransfer[money] = -prices[i] * buyQ[i];
this.agentsById[buyId[i]].transfer(buyerTransfer, { isTrade: 1, isBuyAccepted: 1 });
}
} else {
throw new Error("Pool.trade tradeSpec.bs must be b or s, got:"+bs);
}
} else {
throw new Error("Pool.trade tradeSpec object not in correct format");
}
}
/**
* distribute an aggregate setting of buyer Values or seller Costs to a pool of sellers, by giving each agent a successive value from the array without replacement
*
* @param {string} field "values" or "costs"
* @param {good} good name of good for agents inventories.
* @param {number[]} aggregateArray list of numeric values or costs reflecting the aggregate pool values or costs
* @throws {Error} when field is invalid or aggregateArray is wrong type
*/
distribute(field, good, aggregateArray) {
let i, l;
let myCopy;
if (Array.isArray(aggregateArray)) {
myCopy = aggregateArray.slice();
} else {
throw new Error("Error: Pool.prototype.distribute: expected aggregate to be Array, got: " + typeof(aggregateArray));
}
if ((field !== 'values') && (field !== 'costs'))
throw new Error("Pool.distribute(field,good,aggArray) field should be 'values' or 'costs', got:" + field);
for (i = 0, l = this.agents.length;i < l;++i) {
// the if statement probably would never be satisfied -- but better to fix missing field than throw an error
/* istanbul ignore next */
if (typeof(this.agents[i][field]) === 'undefined')
this.agents[i][field] = {};
this.agents[i][field][good] = [];
}
i = 0;
l = this.agents.length;
while (myCopy.length > 0) {
this.agents[i][field][good].push(myCopy.shift());
i = (i + 1) % l;
}
}
}