master 6ebc4155440b cached
76 files
1.2 MB
545.1k tokens
340 symbols
1 requests
Download .txt
Showing preview only (1,259K chars total). Download the full file or copy to clipboard to get everything.
Repository: davecliff/BristolStockExchange
Branch: master
Commit: 6ebc4155440b
Files: 76
Total size: 1.2 MB

Directory structure:
gitextract_ke5l3892/

├── .gitignore
├── .idea/
│   ├── .name
│   ├── BristolStockExchange.iml
│   ├── inspectionProfiles/
│   │   └── profiles_settings.xml
│   ├── misc.xml
│   ├── modules.xml
│   ├── vcs.xml
│   └── workspace.xml
├── BSE.py
├── BSE_VernonSmith1962_demo.ipynb
├── LICENSE.md
├── Offsets_BTC_USD/
│   └── empty_file
├── README.md
├── Trader_AA.py
├── ZhenZhang/
│   ├── README.md
│   └── source/
│       ├── BSE2.py
│       ├── BSE2_msg_classes.py
│       ├── BSE_trader_agents.py
│       ├── GDX.py
│       ├── IAA_MLOFI.py
│       ├── IAA_NEW.py
│       ├── IGDX_MLOFI.py
│       ├── IZIP_MLOFI.py
│       ├── Simple_MLOFI.py
│       ├── ZZISHV.py
│       └── dataAnalysis/
│           ├── box_analysis.py
│           ├── hypothesisTest.py
│           └── matplotlib4.py
├── clean.sh
├── snashall2019.py
└── wiki_images/
    └── TIFFs/
        ├── 2shockGVWY.tiff
        ├── 2shockSHVR.tiff
        ├── 2shockZIC.tiff
        ├── 2shockZIP.tiff
        ├── TwoShock4Up.tiff
        ├── fig4.1.tiff
        ├── fig4.2.tiff
        ├── fig4.3.tiff
        ├── fig4.4.tiff
        ├── fig4.5.tiff
        ├── fig4.6.tiff
        ├── fig4.7.tiff
        ├── fig5.1.tiff
        ├── fig5.2.tiff
        ├── gvwy_profit.tiff
        ├── gvwy_trans.tiff
        ├── shvr_profit.tiff
        ├── shvr_trans.tiff
        ├── sinetest.tiff
        ├── sinusoid_trans.tiff
        ├── snip4.1.tiff
        ├── snip4.2.tiff
        ├── snip5.1.tiff
        ├── snip5.2.tiff
        ├── snipBSEcode01.tiff
        ├── snip_ZIC.tiff
        ├── snip_bigtest.tiff
        ├── snip_gvwy.tiff
        ├── snip_lob01.tiff
        ├── snip_lob02.tiff
        ├── snip_lob03.tiff
        ├── snip_lob04.tiff
        ├── snip_shvr.tiff
        ├── snip_snpr.tiff
        ├── snip_zic_targetupdown.tiff
        ├── snip_zip_getorder.tiff
        ├── snip_zip_lobprocessor.tiff
        ├── snip_zip_profit_alter.tiff
        ├── snip_zip_willingtotrade.tiff
        ├── snip_zip_workbid.tiff
        ├── snpr_profit.tiff
        ├── snpr_trans.tiff
        ├── zic_profit.tiff
        ├── zic_trans.tiff
        ├── zip_profit.tiff
        └── zip_trans.tiff

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.DS_Store
.ipynb_checkpoints
__pycache__
*.csv


================================================
FILE: .idea/.name
================================================
BSE.py

================================================
FILE: .idea/BristolStockExchange.iml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
  <component name="NewModuleRootManager">
    <content url="file://$MODULE_DIR$">
      <excludeFolder url="file://$MODULE_DIR$/venv" />
    </content>
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />
  </component>
</module>

================================================
FILE: .idea/inspectionProfiles/profiles_settings.xml
================================================
<component name="InspectionProjectProfileManager">
  <settings>
    <option name="USE_PROJECT_PROFILE" value="false" />
    <version value="1.0" />
  </settings>
</component>

================================================
FILE: .idea/misc.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (BristolStockExchange)" project-jdk-type="Python SDK" />
</project>

================================================
FILE: .idea/modules.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectModuleManager">
    <modules>
      <module fileurl="file://$PROJECT_DIR$/.idea/BristolStockExchange.iml" filepath="$PROJECT_DIR$/.idea/BristolStockExchange.iml" />
    </modules>
  </component>
</project>

================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
  </component>
</project>

================================================
FILE: .idea/workspace.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="AutoImportSettings">
    <option name="autoReloadType" value="SELECTIVE" />
  </component>
  <component name="ChangeListManager">
    <list default="true" id="495f364a-cc51-4bd6-acc7-893e289ebda4" name="Changes" comment="" />
    <option name="SHOW_DIALOG" value="false" />
    <option name="HIGHLIGHT_CONFLICTS" value="true" />
    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
    <option name="LAST_RESOLUTION" value="IGNORE" />
  </component>
  <component name="Git.Settings">
    <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
  </component>
  <component name="MarkdownSettingsMigration">
    <option name="stateVersion" value="1" />
  </component>
  <component name="ProjectId" id="2GFqCJUSQoIJ8lkeULZPqC2YzBe" />
  <component name="ProjectLevelVcsManager" settingsEditedManually="true" />
  <component name="ProjectViewState">
    <option name="hideEmptyMiddlePackages" value="true" />
    <option name="showLibraryContents" value="true" />
  </component>
<<<<<<< HEAD
  <component name="PropertiesComponent">{
  &quot;keyToString&quot;: {
    &quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
    &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
    &quot;WebServerToolWindowFactoryState&quot;: &quot;false&quot;,
    &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
    &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
    &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
    &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
    &quot;settings.editor.selected.configurable&quot;: &quot;org.jetbrains.plugins.notebooks.jupyter.connections.configuration.JupyterServerConfigurable&quot;
=======

  <component name="PropertiesComponent"><![CDATA[{
  "keyToString": {
    "RunOnceActivity.OpenProjectViewOnStart": "true",
    "RunOnceActivity.ShowReadmeOnStart": "true",
    "WebServerToolWindowFactoryState": "false",
    "node.js.detected.package.eslint": "true",
    "node.js.detected.package.tslint": "true",
    "node.js.selected.package.eslint": "(autodetect)",
    "node.js.selected.package.tslint": "(autodetect)",
    "settings.editor.selected.configurable": "org.jetbrains.plugins.notebooks.jupyter.connections.configuration.JupyterServerConfigurable"
>>>>>>> fae9de5d8fd858b6ca07f57614c782d21246beaa
  }
}</component>
  <component name="RunManager">
    <configuration name="BSE" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
      <module name="BristolStockExchange" />
      <option name="INTERPRETER_OPTIONS" value="" />
      <option name="PARENT_ENVS" value="true" />
      <envs>
        <env name="PYTHONUNBUFFERED" value="1" />
      </envs>
      <option name="SDK_HOME" value="/usr/local/bin/python3.10" />
      <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
      <option name="IS_MODULE_SDK" value="false" />
      <option name="ADD_CONTENT_ROOTS" value="true" />
      <option name="ADD_SOURCE_ROOTS" value="true" />
      <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
      <option name="SCRIPT_NAME" value="$PROJECT_DIR$/BSE.py" />
      <option name="PARAMETERS" value="" />
      <option name="SHOW_COMMAND_LINE" value="false" />
      <option name="EMULATE_TERMINAL" value="false" />
      <option name="MODULE_MODE" value="false" />
      <option name="REDIRECT_INPUT" value="false" />
      <option name="INPUT_FILE" value="" />
      <method v="2" />
    </configuration>
  </component>
  <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
  <component name="TaskManager">
    <task active="true" id="Default" summary="Default task">
      <changelist id="495f364a-cc51-4bd6-acc7-893e289ebda4" name="Changes" comment="" />
      <created>1665994552377</created>
      <option name="number" value="Default" />
      <option name="presentableId" value="Default" />
      <updated>1665994552377</updated>
      <workItem from="1665994563147" duration="8237000" />
      <workItem from="1698491854751" duration="728000" />

    </task>
    <servers />
  </component>
  <component name="TypeScriptGeneratedFilesManager">
    <option name="version" value="3" />
  </component>
  <component name="Vcs.Log.Tabs.Properties">
    <option name="TAB_STATES">
      <map>
        <entry key="MAIN">
          <value>
            <State />
          </value>
        </entry>
      </map>
    </option>
  </component>
  <component name="com.intellij.coverage.CoverageDataManagerImpl">
    <SUITE FILE_PATH="coverage/BristolStockExchange$BSE.coverage" NAME="BSE Coverage Results" MODIFIED="1698488749188" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
  </component>
</project>

================================================
FILE: BSE.py
================================================
# -*- coding: utf-8 -*-
#
# BSE: The Bristol Stock Exchange
#
# Version 1.91: November 2024 fixed PT1 + PT2 parameter passing/unpacking
# Version 1.9: March 2024 added PT1+PT2, plus all the docstrings.
# Version 1.8; March 2023 added ZIPSH
# Version 1.7; September 2022 added PRDE
# Version 1.6; September 2021 added PRSH
# Version 1.5; 02 Jan 2021 -- was meant to be the final version before switch to BSE2.x, but that didn't happen :-)
# Version 1.4; 26 Oct 2020 -- change to Python 3.x
# Version 1.3; July 21st, 2018 (Python 2.x)
# Version 1.2; November 17th, 2012 (Python 2.x)
#
# Copyright (c) 2012-2024, Dave Cliff
#
#
# ------------------------
#
# MIT Open-Source License:
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
# associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or substantial
# portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# ------------------------
#
#
#
# BSE is a very simple simulation of automated execution traders
# operating on a very simple model of a limit order book (LOB) exchange's matching engine.
#
# major simplifications in this version:
#       (a) only one financial instrument being traded
#       (b) traders can only trade contracts of size 1
#       (c) each trader can have max of one order per single orderbook.
#       (d) traders can replace/overwrite earlier orders, and/or can cancel, with no fee/penalty imposed for doing so
#       (d) simply processes each order in sequence and republishes LOB to all traders
#           => no issues with exchange processing latency/delays or simultaneously issued orders.
#
# NB this code has been written to be readable/intelligible, not efficient!

import sys
import math
import random
import os
import time as chrono
import csv
from datetime import datetime

# a bunch of system constants (globals)
bse_sys_minprice = 1                    # minimum price in the system, in cents/pennies
bse_sys_maxprice = 500                  # maximum price in the system, in cents/pennies
# ticksize should be a param of an exchange (so different exchanges can have different ticksizes)
ticksize = 1  # minimum change in price, in cents/pennies


# an Order/quote has a trader id, a type (buy/sell) price, quantity, timestamp, and unique i.d.
class Order:
    """
    An Order: this is used both for client-orders from exogenous customers to the robot traders acting as sales traders,
    and for the trader-orders (aka quotes) sent by the robot traders to the BSE exchange.
    In both use-cases, an order has a trader-i.d., a type (buy/sell), price, quantity, timestamp, and unique quote-i.d.
    """

    def __init__(self, tid, otype, price, qty, time, qid):
        self.tid = tid  # trader i.d.
        self.otype = otype  # order type
        self.price = price  # price
        self.qty = qty  # quantity
        self.time = time  # timestamp
        self.qid = qid  # quote i.d. (unique to each quote)

    def __str__(self):
        return '[%s %s P=%03d Q=%s T=%5.2f QID:%d]' % \
               (self.tid, self.otype, self.price, self.qty, self.time, self.qid)


class OrderbookHalf:
    """
    OrderbookHalf is one side of the book: a list of bids or a list of asks, each sorted best-price-first,
    and with orders at the same price arranged by arrival time (oldest first) for time-priority processing.
    """

    def __init__(self, booktype, worstprice):
        """
        Create one side of the LOB
        :param booktype: specifies bid or ask side of the LOB.
        :param worstprice: the initial value of the worst price currently showing on the LOB.
        """
        # booktype: bids or asks?
        self.booktype = booktype
        # dictionary of orders received, indexed by Trader ID
        self.orders = {}
        # limit order book, dictionary indexed by price, with order info
        self.lob = {}
        # anonymized LOB, lists, with only price/qty info
        self.lob_anon = []
        # summary stats
        self.best_price = None
        self.best_tid = None
        self.worstprice = worstprice
        self.session_extreme = None    # most extreme price quoted in this session
        self.n_orders = 0  # how many orders?
        self.lob_depth = 0  # how many different prices on lob?

    def anonymize_lob(self):
        """
        anonymize a lob, strip out order details, format as a sorted list
        NB for asks, the sorting should be reversed
        :return: <nothing>
        """
        self.lob_anon = []
        for price in sorted(self.lob):
            qty = self.lob[price][0]
            self.lob_anon.append([price, qty])

    def build_lob(self):
        """
        Take a list of orders and build a limit-order-book (lob) from it
        NB the exchange needs to know arrival times and trader-id associated with each order
        also builds anonymized version (just price/quantity, sorted, as a list) for publishing to traders
        :return: lob as a dictionary (i.e., unsorted)
        """
        lob_verbose = False
        self.lob = {}
        for tid in self.orders:
            order = self.orders.get(tid)
            price = order.price
            if price in self.lob:
                # update existing entry
                qty = self.lob[price][0]
                orderlist = self.lob[price][1]
                orderlist.append([order.time, order.qty, order.tid, order.qid])
                self.lob[price] = [qty + order.qty, orderlist]
            else:
                # create a new dictionary entry
                self.lob[price] = [order.qty, [[order.time, order.qty, order.tid, order.qid]]]
        # create anonymized version
        self.anonymize_lob()
        # record best price and associated trader-id
        if len(self.lob) > 0:
            if self.booktype == 'Bid':
                self.best_price = self.lob_anon[-1][0]
            else:
                self.best_price = self.lob_anon[0][0]
            self.best_tid = self.lob[self.best_price][1][0][2]
        else:
            self.best_price = None
            self.best_tid = None

        if lob_verbose:
            print(self.lob)

    def book_add(self, order):
        """
        Add order to the dictionary holding the list of orders for one side of the LOB.
        Either overwrites old order from this trader
            or dynamically creates new entry in the dictionary
            so there is a max of one order per trader per list
        checks whether length or order list has changed, to distinguish addition/overwrite
        :param order: the order to be added to the book
        :return: character-string indicating whether order-book was added to or overwritten.
        """

        # if this is an ask, does the price set a new extreme-high record?
        if (self.booktype == 'Ask') and ((self.session_extreme is None) or (order.price > self.session_extreme)):
            self.session_extreme = int(order.price)

        # add the order to the book
        n_orders = self.n_orders
        self.orders[order.tid] = order
        self.n_orders = len(self.orders)
        self.build_lob()
        # print('book_add < %s %s' % (order, self.orders))
        if n_orders != self.n_orders:
            return 'Addition'
        else:
            return 'Overwrite'

    def book_del(self, order):
        """
        Delete order from the dictionary holding the orders for one half of the book.
        Assumes max of one order per trader per list.
        Checks that the Trader ID does actually exist in the dict before deletion.
        :param order: the order to be deleted.
        :return: <nothing>
        """
        if self.orders.get(order.tid) is not None:
            del (self.orders[order.tid])
            self.n_orders = len(self.orders)
            self.build_lob()
        # print('book_del %s', self.orders)

    def delete_best(self):
        """
        When the best bid/ask has been hit/lifted, delete it from the book.
        :return: TraderID of the deleted order is return-value, as counterparty to the trade.
        """

        best_price_orders = self.lob[self.best_price]
        best_price_qty = best_price_orders[0]
        best_price_counterparty = best_price_orders[1][0][2]
        if best_price_qty == 1:
            # here the order deletes the best price
            del (self.lob[self.best_price])
            del (self.orders[best_price_counterparty])
            self.n_orders = self.n_orders - 1
            if self.n_orders > 0:
                if self.booktype == 'Bid':
                    self.best_price = max(self.lob.keys())
                else:
                    self.best_price = min(self.lob.keys())
                self.lob_depth = len(self.lob.keys())
            else:
                self.best_price = self.worstprice
                self.lob_depth = 0
        else:
            # best_bid_qty>1 so the order decrements the quantity of the best bid
            # update the lob with the decremented order data
            self.lob[self.best_price] = [best_price_qty - 1, best_price_orders[1][1:]]

            # update the bid list: counterparty's bid has been deleted
            del (self.orders[best_price_counterparty])
            self.n_orders = self.n_orders - 1
        self.build_lob()
        return best_price_counterparty


class Orderbook(OrderbookHalf):
    """ Orderbook for a single tradeable asset: list of bids and list of asks """

    def __init__(self):
        """Construct a new orderbook"""

        self.bids = OrderbookHalf('Bid', bse_sys_minprice)
        self.asks = OrderbookHalf('Ask', bse_sys_maxprice)
        self.tape = []
        self.tape_length = 10000    # max events on in-memory tape (older events can be written to tape_dump file)
        self.quote_id = 0           # unique ID code for each quote accepted onto the book
        self.lob_string = ''        # character-string linearization of public lob items with nonzero quantities


class Exchange(Orderbook):
    """  Exchange's matching engine and limit order book"""

    def add_order(self, order, vrbs):
        """
        add an order to the exchange -- either match with a counterparty order on LOB, or add to LOB.
        :param order: the order to be added to the LOB
        :param vrbs: verbosity: if True, print a running commentary; if False, stay silent.
        :return: [order.qid, response] -- order.qid is the order's unique quote i.d., response is 'Overwrite'|'Addition'
        """
        # add a quote/order to the exchange and update all internal records; return unique i.d.
        order.qid = self.quote_id
        self.quote_id = order.qid + 1
        if vrbs:
            print('add_order QID=%d self.quote.id=%d' % (order.qid, self.quote_id))
        if order.otype == 'Bid':
            response = self.bids.book_add(order)
            best_price = self.bids.lob_anon[-1][0]
            self.bids.best_price = best_price
            self.bids.best_tid = self.bids.lob[best_price][1][0][2]
        else:
            response = self.asks.book_add(order)
            best_price = self.asks.lob_anon[0][0]
            self.asks.best_price = best_price
            self.asks.best_tid = self.asks.lob[best_price][1][0][2]
        return [order.qid, response]

    def del_order(self, time, order, tape_file, vrbs):
        """
        Delete an order from the exchange.
        :param time: the current time.
        :param order: the order to be deleted from the LOB.
        :param tape_file: if not None, write details of the cancellation to the tape file.
        :param vrbs: verbosity: if True, print a running commentary; if False, stay silent.
        :return: <nothing>
        """
        # delete a trader's quote/order from the exchange, update all internal records
        if vrbs:
            print('del_order QID=%d' % order.qid)
        if order.otype == 'Bid':
            self.bids.book_del(order)
            if self.bids.n_orders > 0:
                best_price = self.bids.lob_anon[-1][0]
                self.bids.best_price = best_price
                self.bids.best_tid = self.bids.lob[best_price][1][0][2]
            else:  # this side of book is empty
                self.bids.best_price = None
                self.bids.best_tid = None
            cancel_record = {'type': 'Cancel', 'time': time, 'order': order}
            if tape_file is not None:
                tape_file.write('CAN, %f, %d, Bid, %d\n' % (time, order.qid, order.price))
            self.tape.append(cancel_record)
            # right-truncate the tape so that it keeps only the most recent items
            self.tape = self.tape[-self.tape_length:]

        elif order.otype == 'Ask':
            self.asks.book_del(order)
            if self.asks.n_orders > 0:
                best_price = self.asks.lob_anon[0][0]
                self.asks.best_price = best_price
                self.asks.best_tid = self.asks.lob[best_price][1][0][2]
            else:  # this side of book is empty
                self.asks.best_price = None
                self.asks.best_tid = None
            
            cancel_record = {'type': 'Cancel', 'time': time, 'order': order}
            if tape_file is not None:
                tape_file.write('CAN, %f, %d, Ask, %d\n' % (time, order.qid, order.price))
            self.tape.append(cancel_record)
            # right-truncate the tape so that it keeps only the most recent items
            self.tape = self.tape[-self.tape_length:]
        else:
            # neither bid nor ask?
            sys.exit('bad order type in del_quote()')

    def process_order(self, time, order, tape_file, vrbs):
        """
        Process an order from a trader -- this is the BSE Matching Engine.
        :param time: the current time.
        :param order: the order to be processed.
        :param tape_file: if is not None then write details of transaction to tape_file
        :param vrbs: verbosity: if True, print a running commentary; if False, stay silent.
        :return: transaction_record if the order results in a transaction, otherwise None.
        """
        # receive an order and either add it to the relevant LOB (ie treat as limit order)
        # or if it crosses the best counterparty offer, execute it (treat as a market order)
        oprice = order.price
        counterparty = None
        price = None
        [qid, response] = self.add_order(order, vrbs)  # add it to the order lists -- overwriting any previous order
        order.qid = qid
        if vrbs:
            print('QUID: order.quid=%d' % order.qid)
            print('RESPONSE: %s' % response)
        best_ask = self.asks.best_price
        best_ask_tid = self.asks.best_tid
        best_bid = self.bids.best_price
        best_bid_tid = self.bids.best_tid
        if order.otype == 'Bid':
            if self.asks.n_orders > 0 and best_bid >= best_ask:
                # bid lifts the best ask
                if vrbs:
                    print("Bid $%s lifts best ask" % oprice)
                counterparty = best_ask_tid
                price = best_ask  # bid crossed ask, so use ask price
                if vrbs:
                    print('counterparty, price', counterparty, price)
                # delete the ask just crossed
                self.asks.delete_best()
                # delete the bid that was the latest order
                self.bids.delete_best()
        elif order.otype == 'Ask':
            if self.bids.n_orders > 0 and best_ask <= best_bid:
                # ask hits the best bid
                if vrbs:
                    print("Ask $%s hits best bid" % oprice)
                # remove the best bid
                counterparty = best_bid_tid
                price = best_bid  # ask crossed bid, so use bid price
                if vrbs:
                    print('counterparty, price', counterparty, price)
                # delete the bid just crossed, from the exchange's records
                self.bids.delete_best()
                # delete the ask that was the latest order, from the exchange's records
                self.asks.delete_best()
        else:
            # we should never get here
            sys.exit('process_order() given neither Bid nor Ask')
        # NB at this point we have deleted the order from the exchange's records
        # but the two traders concerned still have to be notified
        if vrbs:
            print('counterparty %s' % counterparty)
        if counterparty is not None:
            # process the trade
            if vrbs:
                print('>>>>>>>>>>>>>>>>>TRADE t=%010.3f $%d %s %s' % (time, price, counterparty, order.tid))
            transaction_record = {'type': 'Trade',
                                  'time': time,
                                  'price': price,
                                  'party1': counterparty,
                                  'party2': order.tid,
                                  'qty': order.qty
                                  }
            if tape_file is not None:
                tape_file.write('TRD, %f, %d\n' % (time, price))
            self.tape.append(transaction_record)
            # right-truncate the tape so that it keeps only the most recent items
            self.tape = self.tape[-self.tape_length:]

            return transaction_record
        else:
            return None

    def tape_dump(self, fname, fmode, tmode):
        """
        Currently tape_dump only writes a list of transactions (i.e., it ignores any cancellations)
        :param fname: filename to write to.
        :param fmode: file-open write/append mode.
        :param tmode: if set to 'wipe', wipes the tape clean after writing it to file.
        :return:
        """
        dumpfile = open(fname, fmode)
        dumpfile.write('Event Type, Time, Price\n')
        for tapeitem in self.tape:
            if tapeitem['type'] == 'Trade':
                dumpfile.write('Trd, %010.3f, %s\n' % (tapeitem['time'], tapeitem['price']))
        dumpfile.close()
        if tmode == 'wipe':
            self.tape = []

    def publish_lob(self, time, lob_file, vrbs):
        """
        Returns the public LOB data published by the exchange, 
        i.e. the version of the LOB that's accessible to the traders.
        :param time: the current time.
        :param lob_file: 
        :param vrbs: verbosity: if True, print a running commentary; if False, stay silent.
        :return: the public LOB data.
        """
        public_data = dict()
        public_data['time'] = time
        public_data['bids'] = {'best': self.bids.best_price,
                               'worst': self.bids.worstprice,
                               'n': self.bids.n_orders,
                               'lob': self.bids.lob_anon}
        public_data['asks'] = {'best': self.asks.best_price,
                               'worst': self.asks.worstprice,
                               'sess_hi': self.asks.session_extreme,
                               'n': self.asks.n_orders,
                               'lob': self.asks.lob_anon}
        public_data['QID'] = self.quote_id
        public_data['tape'] = self.tape

        if lob_file is not None:
            # build a linear character-string summary of only those prices on LOB with nonzero quantities
            lobstring = 'Bid:,'
            n_bids = len(self.bids.lob_anon)
            if n_bids > 0:
                lobstring += '%d,' % n_bids
                for lobitem in self.bids.lob_anon:
                    price_str = '%d,' % lobitem[0]
                    qty_str = '%d,' % lobitem[1]
                    lobstring = lobstring + price_str + qty_str
            else:
                lobstring += '0,'
            lobstring += 'Ask:,'
            n_asks = len(self.asks.lob_anon)
            if n_asks > 0:
                lobstring += '%d,' % n_asks
                for lobitem in self.asks.lob_anon:
                    price_str = '%d,' % lobitem[0]
                    qty_str = '%d,' % lobitem[1]
                    lobstring = lobstring + price_str + qty_str
            else:
                lobstring += '0,'
            # is this different to the last lob_string?
            if lobstring != self.lob_string:
                # write it
                lob_file.write('%.3f, %s\n' % (time, lobstring))
                # remember it
                self.lob_string = lobstring

        if vrbs:
            vstr = 'publish_lob: t=%f' % time
            vstr += ' BID_lob=%s' % public_data['bids']['lob']
            # vstr += 'best=%s; worst=%s; n=%s ' % (self.bids.best_price, self.bids.worstprice, self.bids.n_orders)
            vstr += ' ASK_lob=%s' % public_data['asks']['lob']
            # vstr += 'qid=%d' % self.quote_id
            print(vstr)

        return public_data


# #################--Traders below here--#############


# Trader superclass
# all Traders have a trader id, bank balance, blotter, and list of orders to execute
class Trader:
    """The parent class for all types of robot trader in BSE"""

    def __init__(self, ttype, tid, balance, params, time):
        """
        Initializes a generic trader with attributes common to all/most types of trader
        Some trader types (e.g. ZIP) then have additional specialised initialization steps
        :param ttype: the trader type
        :param tid: the trader I.D. (a non-negative integer)
        :param balance: how much money it has in the bank when it is created
        :param params: a set of parameter-values, for those trader-types that have parameters
        :param time: the time this trader was created
        """
        self.ttype = ttype          # what type / strategy this trader is
        self.tid = tid              # trader unique ID code
        self.balance = balance      # money in the bank
        self.params = params        # parameters/extras associated with this trader-type or individual trader.
        self.blotter = []           # record of trades executed
        self.blotter_length = 100   # maximum length of blotter
        self.orders = []            # customer orders currently being worked (fixed at len=1 in BSE1.x)
        self.n_quotes = 0           # number of quotes live on LOB
        self.birthtime = time       # used when calculating age of a trader/strategy
        self.profitpertime = 0      # profit per unit time
        self.profit_mintime = 60    # minimum duration in seconds for calculating profitpertime
        self.n_trades = 0           # how many trades has this trader done?
        self.lastquote = None       # record of what its last quote was

    def __str__(self):
        """ return a character-string that summarises a trader """
        return '[TID %s type %s balance %s blotter %s orders %s n_trades %s profitpertime %s]' \
               % (self.tid, self.ttype, self.balance, self.blotter, self.orders, self.n_trades, self.profitpertime)

    def add_order(self, order, vrbs):
        """
        What a trader calls when it receives a new customer order/assignment
        :param order: the customer order/assignment to be added
        :param vrbs: verbosity: if True, print a running commentary; if False, stay silent.
        :return response: string to indicate whether the trader needs to cancel its current order on the LOB
        """
        # in this version, trader has at most one order,
        # if allow more than one, this needs to be self.orders.append(order)
        if self.n_quotes > 0:
            # this trader has a live quote on the LOB, from a previous customer order
            # need response to signal cancellation/withdrawal of that quote
            response = 'LOB_Cancel'
        else:
            response = 'Proceed'
        self.orders = [order]
        if vrbs:
            print('add_order < response=%s' % response)
        return response

    def del_order(self, order):
        """What a trader calls when it wants to delete an existing customer order/assignment """
        if order is None:
            pass    # this line is purely to stop PyCharm from warning about order being an unused parameter
        # this is lazy: assumes each trader has only one customer order with quantity=1, so deleting sole order
        self.orders = []

    def profitpertime_update(self, time, birthtime, totalprofit):
        """
        Calculates the trader's profit per unit time, but only if it has been alive longer than profit_mintime
        This is to avoid situations where a trader is created and then immediately makes a profit and
        hence the profit per unit time is a sky-high value, because the time_alive divisor is close to zero.
        :param time: the current time.
        :param birthtime: the time when the trader was created.
        :param totalprofit: the trader's current total accumulated profit.
        :return: profit per second.
        """
        time_alive = (time - birthtime)
        if time_alive >= self.profit_mintime:
            profitpertime = totalprofit / time_alive
        else:
            # if it's not been alive long enough, divide it by mintime instead of actual time
            profitpertime = totalprofit / self.profit_mintime
        return profitpertime

    def bookkeep(self, time, trade, order, vrbs):
        """
        Update trader's individual records of transactions, profit/loss etc.
        :param trade: details of the transaction that took place.
        :param order: details of the customer order that led to the transaction.
        :param vrbs: verbosity: if True, print a running commentary; if False, stay silent.
        :param time: the current time.
        :return: <nothing>
        """
        outstr = ""
        for order in self.orders:
            outstr = outstr + str(order)

        self.blotter.append(trade)  # add trade record to trader's blotter
        self.blotter = self.blotter[-self.blotter_length:]  # right-truncate to keep to length

        # NB What follows is **LAZY** -- assumes all orders are quantity=1
        transactionprice = trade['price']
        if self.orders[0].otype == 'Bid':
            profit = self.orders[0].price - transactionprice
        else:
            profit = transactionprice - self.orders[0].price
        self.balance += profit
        self.n_trades += 1
        self.profitpertime = self.balance / (time - self.birthtime)

        if profit < 0:
            print(profit)
            print(trade)
            print(order)
            sys.exit('FAIL: negative profit')

        if vrbs:
            print('%s profit=%d balance=%d profit/time=%s' % (outstr, profit, self.balance, str(self.profitpertime)))
        self.del_order(order)  # delete the order

        # if the trader has multiple strategies (e.g. PRSH/PRDE/ZIPSH/ZIPDE) then there is more work to do...
        if hasattr(self, 'strats') and hasattr(self, 'active_strat'):
            if self.strats is not None:
                self.strats[self.active_strat]['profit'] += profit
                totalprofit = self.strats[self.active_strat]['profit']
                birthtime = self.strats[self.active_strat]['start_t']
                self.strats[self.active_strat]['pps'] = self.profitpertime_update(time, birthtime, totalprofit)

    def respond(self, time, lob, trade, vrbs):
        """
        Specify how a trader responds to events in the market.
        For Trader superclass, this is minimal action, but expect it to be overloaded by specific trading strategies.
        :param time:
        :param lob:
        :param trade:
        :param vrbs: verbosity: if True, print a running commentary; if False, stay silent.
        :return:
        """

        # any trader subclass with custom respond() must include this update of profitpertime
        self.profitpertime = self.profitpertime_update(time, self.birthtime, self.balance)
        return None


class TraderGiveaway(Trader):
    """
    Trader subclass Giveaway (GVWY): even dumber than a ZI-U: just give the deal away (but never make a loss)
    """

    def getorder(self, time, countdown, lob):
        """
        Create this trader's order to be sent to the exchange.
        :param time: the current time.
        :param countdown: how much time before market closes (not used by GVWY).
        :param lob: the current state of the LOB.
        :return: a new order from this trader.
        """
        # this test for negative countdown is purely to stop PyCharm warning about unused parameter value
        if countdown < 0:
            sys.exit('Negative countdown')

        if len(self.orders) < 1:
            order = None
        else:
            quoteprice = self.orders[0].price
            order = Order(self.tid,
                          self.orders[0].otype,
                          quoteprice,
                          self.orders[0].qty,
                          time, lob['QID'])
            self.lastquote = order
        return order


class TraderZIC(Trader):
    """
    Trader subclass ZI-C: after Gode & Sunder 1993
    """

    def getorder(self, time, countdown, lob):
        """
        Create this trader's order to be sent to the exchange.
        :param time: the current time.
        :param countdown: how much time before market closes (not used by ZIC).
        :param lob: the current state of the LOB.
        :return: a new order from this trader.
        """
        # this test for negative countdown is purely to stop PyCharm warning about unused parameter value
        if countdown < 0:
            sys.exit('Negative countdown')

        if len(self.orders) < 1:
            # no orders: return NULL
            order = None
        else:
            minprice = lob['bids']['worst']
            maxprice = lob['asks']['worst']
            qid = lob['QID']
            limit = self.orders[0].price
            otype = self.orders[0].otype
            if otype == 'Bid':
                quoteprice = random.randint(int(minprice), int(limit))
            else:
                quoteprice = random.randint(int(limit), int(maxprice))
                # NB should check it == 'Ask' and barf if not
            order = Order(self.tid, otype, quoteprice, self.orders[0].qty, time, qid)
            self.lastquote = order
        return order


class TraderShaver(Trader):
    """
    Trader subclass Shaver: shaves a penny off the best price;
    but if there is no best price, creates "stub quote" at system max/min
    """

    def getorder(self, time, countdown, lob):
        """
        Create this trader's order to be sent to the exchange.
        :param time: the current time.
        :param countdown: how much time before market close (not used by SHVR).
        :param lob: the current state of the LOB.
        :return: a new order from this trader.
        """
        # this test for negative countdown is purely to stop PyCharm warning about unused parameter value
        if countdown < 0:
            sys.exit('Negative countdown')

        if len(self.orders) < 1:
            order = None
        else:
            limitprice = self.orders[0].price
            otype = self.orders[0].otype
            if otype == 'Bid':
                if lob['bids']['n'] > 0:
                    quoteprice = lob['bids']['best'] + 1
                    if quoteprice > limitprice:
                        quoteprice = limitprice
                else:
                    quoteprice = lob['bids']['worst']
            else:
                if lob['asks']['n'] > 0:
                    quoteprice = lob['asks']['best'] - 1
                    if quoteprice < limitprice:
                        quoteprice = limitprice
                else:
                    quoteprice = lob['asks']['worst']
            order = Order(self.tid, otype, quoteprice, self.orders[0].qty, time, lob['QID'])
            self.lastquote = order
        return order


class TraderSniper(Trader):
    """
    Trader subclass Sniper (SNPR), inspired by Kaplan's Sniper, BSE version is based on Shaver,
    "lurks" until time remaining < threshold% of the trading session
    then gets increasing aggressive, increasing "shave thickness" as time runs out
    """

    def getorder(self, time, countdown, lob):
        """
        Create this trader's order to be sent to the exchange.
        :param time: the current time.
        :param countdown: how much time before market closes.
        :param lob: the current state of the LOB.
        :return: a new order from this trader.
        """
        lurk_threshold = 0.2
        shavegrowthrate = 3
        shave = int(1.0 / (0.01 + countdown / (shavegrowthrate * lurk_threshold)))
        if (len(self.orders) < 1) or (countdown > lurk_threshold):
            order = None
        else:
            limitprice = self.orders[0].price
            otype = self.orders[0].otype

            if otype == 'Bid':
                if lob['bids']['n'] > 0:
                    quoteprice = lob['bids']['best'] + shave
                    if quoteprice > limitprice:
                        quoteprice = limitprice
                else:
                    quoteprice = lob['bids']['worst']
            else:
                if lob['asks']['n'] > 0:
                    quoteprice = lob['asks']['best'] - shave
                    if quoteprice < limitprice:
                        quoteprice = limitprice
                else:
                    quoteprice = lob['asks']['worst']
            order = Order(self.tid, otype, quoteprice, self.orders[0].qty, time, lob['QID'])
            self.lastquote = order
        return order


class TraderPRZI(Trader):
    """
    Cliff's Parameterized-Response Zero-Intelligence (PRZI) trader -- pronounced "prezzie"
    but with added adaptive strategies, currently either...
       ++  a k-point Stochastic Hill-Climber (SHC) hence PRZI-SHC,
           PRZI-SHC pronounced "prezzy-shuck". Ticker symbol PRSH pronounced "purrsh";
    or
       ++ a simple differential evolution (DE) optimizer with pop_size=k, hence PRZE-DE or PRDE ('purdy")

    when optimizer == None then it implements plain-vanilla non-adaptive PRZI, with a fixed strategy-value.
    """

    @staticmethod
    def strat_csv_str(strat):
        """
        Return trader's strategy as a csv-format string
        (trivial in PRZI, but other traders with more complex strategies need this).
        :param strat: the strategy specification (for PRZI, a real number in [-1.0,+1.0]
        :return: the strategy as a scv-format string
        """
        csv_str = 's=,%+5.3f, ' % strat
        return csv_str

    def mutate_strat(self, s, mode):
        """
        How to mutate the PRZI strategy values when evolving / hill-climbing
        :param s: the strategy to be mutated
        :param mode:    'gauss'=> mutation is a draw from a zero-mean Gaussian;
                        'uniform_whole_range" => mutation is a draw from uniform distbn over [-1.0,+1.0].
                        'uniform_bounded_range" => mutation is a draw from a bounded unifrom distbn.
        :return: the mutated strategy value
        """
        s_min = self.strat_range_min
        s_max = self.strat_range_max
        if mode == 'gauss':
            sdev = 0.05
            newstrat = s
            while newstrat == s:
                newstrat = s + random.gauss(0.0, sdev)
                # truncate to keep within range
                newstrat = max(-1.0, min(1.0, newstrat))
        elif mode == 'uniform_whole_range':
            # draw uniformly from whole range
            newstrat = random.uniform(-1.0, +1.0)
        elif mode == 'uniform_bounded_range':
            # draw uniformly from bounded range
            newstrat = random.uniform(s_min, s_max)
        else:
            sys.exit('FAIL: bad mode in mutate_strat')
        return newstrat

    def strat_str(self):
        """
        Pretty-print a string summarising this trader's strategy/strategies
        :return: the string
        """
        string = '%s: %s active_strat=[%d]:\n' % (self.tid, self.ttype, self.active_strat)
        for s in range(0, self.k):
            strat = self.strats[s]
            stratstr = '[%d]: s=%+f, start=%f, $=%f, pps=%f\n' % \
                       (s, strat['stratval'], strat['start_t'], strat['profit'], strat['pps'])
            string = string + stratstr

        return string

    def __init__(self, ttype, tid, balance, params, time):
        """
        Construct a PRZI trader
        :param ttype: the ticker-symbol for the type of trader (its strategy)
        :param tid: the trader id
        :param balance: the trader's bank balance
        :param params: if params == "landscape-mapper" then it generates data for mapping the fitness landscape
        :param time: the current time.
        """

        vrbs = True

        Trader.__init__(self, ttype, tid, balance, params, time)

        # unpack the params
        # for all three of PRZI, PRSH, and PRDE params can include strat_min and strat_max
        # for PRSH and PRDE params should include values for optimizer and k
        # if no params specified then defaults to PRZI with strat values in [-1.0,+1.0]

        # default parameter values
        k = 1
        optimizer = None    # no optimizer => plain non-adaptive PRZI
        s_min = -1.0
        s_max = +1.0

        # did call provide different params?
        if type(params) is dict:
            if 'k' in params:
                k = params['k']
            if 'optimizer' in params:
                optimizer = params['optimizer']
            s_min = params['strat_min']
            s_max = params['strat_max']

        self.optmzr = optimizer     # this determines whether it's PRZI, PRSH, or PRDE
        self.k = k                  # number of sampling points (cf number of arms on a multi-armed-bandit, or pop-size)
        self.theta0 = 100           # threshold-function limit value
        self.m = 4                  # tangent-function multiplier
        self.strat_wait_time = 7200     # how many secs do we give any one strat before switching?
        self.strat_range_min = s_min    # lower-bound on randomly-assigned strategy-value
        self.strat_range_max = s_max    # upper-bound on randomly-assigned strategy-value
        self.active_strat = 0       # which of the k strategies are we currently playing? -- start with 0
        self.prev_qid = None        # previous order i.d.
        self.strat_eval_time = self.k * self.strat_wait_time   # time to cycle through evaluating all k strategies
        self.last_strat_change_time = time  # what time did we last change strategies?
        self.profit_epsilon = 0.0 * random.random()    # min profit-per-sec difference between strategies that counts
        self.strats = []            # strategies awaiting initialization
        self.pmax = None            # this trader's estimate of the maximum price the market will bear
        self.pmax_c_i = math.sqrt(random.randint(1, 10))  # multiplier coefficient when estimating p_max
        self.mapper_outfile = None
        # differential evolution parameters all in one dictionary
        self.diffevol = {'de_state': 'active_s0',          # initial state: strategy 0 is active (being evaluated)
                         's0_index': self.active_strat,    # s0 starts out as active strat
                         'snew_index': self.k,             # (k+1)th item of strategy list is DE's new strategy
                         'snew_stratval': None,            # assigned later
                         'F': 0.8                          # differential weight -- usually between 0 and 2
                         }

        start_t = time
        profit = 0.0
        profit_per_second = 0
        lut_bid = None
        lut_ask = None

        for s in range(self.k + 1):
            # initialise each of the strategies in sequence:
            # for PRZI: only one strategy is needed
            # for PRSH, one random initial strategy, then k-1 mutants of that initial strategy
            # for PRDE, use draws from uniform distbn over whole range and a (k+1)th strategy is needed to hold s_new
            strategy = None
            if s == 0:
                strategy = random.uniform(self.strat_range_min, self.strat_range_max)
            else:
                if self.optmzr == 'PRSH':
                    # simple stochastic hill climber: cluster other strats around strat_0
                    strategy = self.mutate_strat(self.strats[0]['stratval'], 'gauss')     # mutant of strats[0]
                elif self.optmzr == 'PRDE':
                    # differential evolution: seed initial strategies across whole space
                    strategy = self.mutate_strat(self.strats[0]['stratval'], 'uniform_bounded_range')
                else:
                    # plain PRZI -- do nothing
                    pass
            # add to the list of strategies
            if s == self.active_strat:
                active_flag = True
            else:
                active_flag = False
            self.strats.append({'stratval': strategy, 'start_t': start_t, 'active': active_flag,
                                'profit': profit, 'pps': profit_per_second, 'lut_bid': lut_bid, 'lut_ask': lut_ask})
            if self.optmzr is None:
                # PRZI -- so we stop after one iteration
                break
            elif self.optmzr == 'PRSH' and s == self.k - 1:
                # PRSH -- doesn't need the (k+1)th strategy
                break

        if self.params == 'landscape-mapper':
            # replace seed+mutants set of strats with regularly-spaced strategy values over the whole range
            self.strats = []
            strategy_delta = 0.01
            strategy = -1.0
            k = 0
            self.strats = []

            while strategy <= +1.0:
                self.strats.append({'stratval': strategy, 'start_t': start_t, 'active': False,
                                    'profit': profit, 'pps': profit_per_second, 'lut_bid': lut_bid, 'lut_ask': lut_ask})
                k += 1
                strategy += strategy_delta
            self.mapper_outfile = open('landscape_map.csv', 'w')
            self.k = k
            self.strat_eval_time = self.k * self.strat_wait_time

        if vrbs:
            print("%s\n" % self.strat_str())

    def getorder(self, time, countdown, lob):
        """
        Create this trader's order to be sent to the exchange.
        :param time: the current time.
        :param countdown: how much time before market close (not used by GVWY).
        :param lob: the current state of the LOB.
        :return: a new order from this trader.
        """
        # this test for negative countdown is purely to stop PyCharm warning about unused parameter value
        if countdown < 0:
            sys.exit('Negative countdown')

        def shvr_price(order_type, lim, pub_lob):
            """
            Return value is what price a SHVR would quote in these circumstances
            :param order_type: is the order bid or ask?
            :param lim: limit price on the order.
            :param pub_lob: the current state of the published LOB.
            :return: The price a SHVR would quote given this LOB and limit-price.
            """

            if order_type == 'Bid':
                if pub_lob['bids']['n'] > 0:
                    shvr_p = pub_lob['bids']['best'] + ticksize   # BSE ticksize is global var
                    if shvr_p > lim:
                        shvr_p = lim
                else:
                    shvr_p = pub_lob['bids']['worst']
            else:
                if pub_lob['asks']['n'] > 0:
                    shvr_p = pub_lob['asks']['best'] - ticksize   # BSE ticksize is global var
                    if shvr_p < lim:
                        shvr_p = lim
                else:
                    shvr_p = pub_lob['asks']['worst']

            # print('shvr_p=%f; ' % shvr_p)
            return shvr_p

        def calc_cdf_lut(strategy, t0, m, dirn, pmin, pmax):
            """
            calculate cumulative distribution function (CDF) look-up table (LUT)
            :param strategy: strategy-value in [-1,+1]
            :param t0: constant used in the threshold function
            :param m: constant used in the threshold function
            :param dirn: direction: 'buy' or 'sell'
            :param pmin: lower bound on discrete-valued price-range
            :param pmax: upper bound on discrete-valued price-range
            :return: {'strat': strategy, 'dirn': dirn, 'pmin': pmin, 'pmax': pmax, 'cdf_lut': cdf}
            """

            # the threshold function used to clip
            def threshold(theta0, x):
                t = max(-1*theta0, min(theta0, x))
                return t

            epsilon = 0.000001  # used to catch DIV0 errors
            lut_vrbs = False

            if (strategy > 1.0) or (strategy < -1.0):
                # out of range
                sys.exit('PRSH FAIL: strategy=%f out of range\n' % strategy)

            if (dirn != 'buy') and (dirn != 'sell'):
                # out of range
                sys.exit('PRSH FAIL: bad dirn=%s\n' % dirn)

            if pmax < pmin:
                # screwed
                sys.exit('PRSH FAIL: pmax %f < pmin %f \n' % (pmax, pmin))

            if lut_vrbs:
                print('PRSH calc_cdf_lut: strategy=%f dirn=%d pmin=%d pmax=%d\n' % (strategy, dirn, pmin, pmax))

            p_range = float(pmax - pmin)
            if p_range < 1:
                # special case: the SHVR-style strategy has shaved all the way to the limit price
                # the lower and upper bounds on the interval are adjacent prices;
                # so cdf is simply the limit-price with probability 1

                if dirn == 'buy':
                    cdf = [{'price': pmax, 'cum_prob': 1.0}]
                else:   # must be a sell
                    cdf = [{'price': pmin, 'cum_prob': 1.0}]

                if lut_vrbs:
                    print('\n\ncdf:', cdf)

                return {'strat': strategy, 'dirn': dirn, 'pmin': pmin, 'pmax': pmax, 'cdf_lut': cdf}

            c = threshold(t0, m * math.tan(math.pi * (strategy + 0.5)))

            # catch div0 errors here
            if abs(c) < epsilon:
                if c > 0:
                    c = epsilon
                else:
                    c = -epsilon

            e2cm1 = math.exp(c) - 1

            # calculate the discrete calligraphic-P function over interval [pmin, pmax]
            # (i.e., this is Equation 8 in the PRZI Technical Note)
            calp_interval = []
            calp_sum = 0
            for p in range(pmin, pmax + 1):
                # normalize the price to proportion of its range
                p_r = (p - pmin) / p_range  # p_r in [0.0, 1.0]
                if strategy == 0.0:
                    # special case: this is just ZIC
                    cal_p = 1 / (p_range + 1)
                elif strategy > 0:
                    if dirn == 'buy':
                        cal_p = (math.exp(c * p_r) - 1.0) / e2cm1
                    else:   # dirn == 'sell'
                        cal_p = (math.exp(c * (1 - p_r)) - 1.0) / e2cm1
                else:   # self.strat < 0
                    if dirn == 'buy':
                        cal_p = 1.0 - ((math.exp(c * p_r) - 1.0) / e2cm1)
                    else:   # dirn == 'sell'
                        cal_p = 1.0 - ((math.exp(c * (1 - p_r)) - 1.0) / e2cm1)

                if cal_p < 0:
                    cal_p = 0   # just in case

                calp_interval.append({'price': p, "cal_p": cal_p})
                calp_sum += cal_p

            if calp_sum <= 0:
                print('calp_interval:', calp_interval)
                print('pmin=%f, pmax=%f, calp_sum=%f' % (pmin, pmax, calp_sum))

            cdf = []
            cum_prob = 0
            # now go thru interval summing and normalizing to give the CDF
            for p in range(pmin, pmax + 1):
                cal_p = calp_interval[p-pmin]['cal_p']
                prob = cal_p / calp_sum
                cum_prob += prob
                cdf.append({'price': p, 'cum_prob': cum_prob})

            if lut_vrbs:
                print('\n\ncdf:', cdf)

            return {'strat': strategy, 'dirn': dirn, 'pmin': pmin, 'pmax': pmax, 'cdf_lut': cdf}

        vrbs = False

        if vrbs:
            print('t=%.1f PRSH getorder: %s, %s' % (time, self.tid, self.strat_str()))

        if len(self.orders) < 1:
            # no orders: return NULL
            order = None
        else:
            # unpack the assignment-order
            limit = self.orders[0].price
            otype = self.orders[0].otype
            qid = self.orders[0].qid

            if self.prev_qid is None:
                self.prev_qid = qid

            if qid != self.prev_qid:
                # customer-order i.d. has changed, so we're working a new customer-order now
                # this is the time to switch arms
                # print("New order! (how does it feel?)")
                pass

            # get extreme limits on price interval
            # lowest price the market will bear
            minprice = int(lob['bids']['worst'])  # default assumption: worst bid price possible as defined by exchange

            # trader's individual estimate highest price the market will bear
            maxprice = self.pmax    # default assumption
            if self.pmax is None:
                maxprice = int(limit * self.pmax_c_i + 0.5)     # in the absence of any other info, guess
                self.pmax = maxprice
            elif lob['asks']['sess_hi'] is not None:
                if self.pmax < lob['asks']['sess_hi']:        # some other trader has quoted higher than I expected
                    maxprice = lob['asks']['sess_hi']         # so use that as my new estimate of highest
                    self.pmax = maxprice

            # use the cdf look-up table
            # cdf_lut is a list of little dictionaries
            # each dictionary has form: {'cum_prob':nnn, 'price':nnn}
            # generate u=U(0,1) uniform disrtibution
            # starting with the lowest nonzero cdf value at cdf_lut[0],
            # walk up the lut (i.e., examine higher cumulative probabilities),
            # until we're in the range of u; then return the relevant price

            strat = self.strats[self.active_strat]['stratval']

            # what price would a SHVR quote?
            p_shvr = shvr_price(otype, limit, lob)

            if otype == 'Bid':

                p_max = int(limit)
                if strat > 0.0:
                    p_min = minprice
                else:
                    # shade the lower bound on the interval
                    # away from minprice and toward shvr_price
                    p_min = int(0.5 + (-strat * p_shvr) + ((1.0 + strat) * minprice))

                lut_bid = self.strats[self.active_strat]['lut_bid']

                if (lut_bid is None) or \
                        (lut_bid['strat'] != strat) or (lut_bid['pmin'] != p_min) or (lut_bid['pmax'] != p_max):
                    # need to compute a new LUT
                    if vrbs:
                        print('New bid LUT')
                    self.strats[self.active_strat]['lut_bid'] = \
                        calc_cdf_lut(strat, self.theta0, self.m, 'buy', p_min, p_max)

                lut = self.strats[self.active_strat]['lut_bid']

            else:   # otype == 'Ask'

                p_min = int(limit)
                if strat > 0.0:
                    p_max = maxprice
                else:
                    # shade the upper bound on the interval
                    # away from maxprice and toward shvr_price
                    p_max = int(0.5 + (-strat * p_shvr) + ((1.0 + strat) * maxprice))
                    if p_max < p_min:
                        # this should never happen, but just in case it does...
                        p_max = p_min

                lut_ask = self.strats[self.active_strat]['lut_ask']

                if (lut_ask is None) or \
                        (lut_ask['strat'] != strat) or \
                        (lut_ask['pmin'] != p_min) or \
                        (lut_ask['pmax'] != p_max):
                    # need to compute a new LUT
                    if vrbs:
                        print('New ask LUT')
                    self.strats[self.active_strat]['lut_ask'] = \
                        calc_cdf_lut(strat, self.theta0, self.m, 'sell', p_min, p_max)

                lut = self.strats[self.active_strat]['lut_ask']

            vrbs = False
            if vrbs:
                print('PRZI strat=%f LUT=%s \n \n' % (strat, lut))
                # for debugging: print a table of lut: price and cum_prob, with the discrete derivative (gives PMF).
                last_cprob = 0.0
                for lut_entry in lut['cdf_lut']:
                    cprob = lut_entry['cum_prob']
                    print('%d, %f, %f' % (lut_entry['price'], cprob - last_cprob, cprob))
                    last_cprob = cprob
                print('\n')
                
                # print ('[LUT print suppressed]')
            
            # do inverse lookup on the LUT to find the price
            quoteprice = None
            u = random.random()
            for entry in lut['cdf_lut']:
                if u < entry['cum_prob']:
                    quoteprice = entry['price']
                    break

            order = Order(self.tid, otype, quoteprice, self.orders[0].qty, time, lob['QID'])

            self.lastquote = order

        return order

    def bookkeep(self, time, trade, order, vrbs):
        """
        Update trader's individual records of transactions, profit/loss etc.
        :param trade: details of the transaction that took place
        :param order: details of the customer order that led to the transaction
        :param vrbs: if True then print a running commentary of what's going on
        :param time: the current time
        :return: (nothing)
        """

        outstr = ""
        for order in self.orders:
            outstr = outstr + str(order)

        self.blotter.append(trade)  # add trade record to trader's blotter
        self.blotter = self.blotter[-self.blotter_length:]      # right-truncate to keep to length

        # NB What follows is **LAZY** -- assumes all orders are quantity=1
        transactionprice = trade['price']
        if self.orders[0].otype == 'Bid':
            profit = self.orders[0].price - transactionprice
        else:
            profit = transactionprice - self.orders[0].price
        self.balance += profit
        self.n_trades += 1
        self.profitpertime = self.balance / (time - self.birthtime)

        if profit < 0:
            print(profit)
            print(trade)
            print(order)
            sys.exit('PRSH FAIL: negative profit')

        if vrbs:
            print('%s profit=%d balance=%d profit/time=%d' % (outstr, profit, self.balance, self.profitpertime))
        self.del_order(order)  # delete the order

        self.strats[self.active_strat]['profit'] += profit
        time_alive = time - self.strats[self.active_strat]['start_t']
        if time_alive > 0:
            profit_per_second = self.strats[self.active_strat]['profit'] / time_alive
            self.strats[self.active_strat]['pps'] = profit_per_second
        else:
            # if it trades at the instant it is born then it would have infinite profit-per-second, which is insane
            # to keep things sensible when time_alive == 0 we say the profit per second is whatever the actual profit is
            self.strats[self.active_strat]['pps'] = profit

    def respond(self, time, lob, trade, vrbs):
        """
        Respond to the current state of the LOB.
        For strategy-optimizers PRSH and PRDE, this can involve switching stratregy, and/or generating new strategies.
        :param time: the current time.
        :param lob: the current state of the LOB.
        :param trade: details of most recent trade, if any.
        :param vrbs: if True then print messages explaining what is going on.
        :return:
        """
        # "PRSH" is a very basic form of stochastic hill-climber (SHC) that's v easy to understand and to code
        # it cycles through the k different strats until each has been operated for at least eval_time seconds
        # but a strat that does nothing will get swapped out if it's been running for no_deal_time without a deal
        # then the strats with the higher total accumulated profit is retained,
        # and mutated versions of it are copied into the other k-1 strats
        # then all counters are reset, and this is repeated indefinitely
        #
        # "PRDE" uses a basic form of Differential Evolution. This maintains a population of at least four strats
        # iterates indefinitely on:
        #       shuffle the set of strats;
        #       name the first four strats s0 to s3;
        #       create new_strat=s1+f*(s2-s3);
        #       evaluate fitness of s0 and new_strat;
        #       if (new_strat fitter than s0) then new_strat replaces s0.
        #
        # todo: add in other optimizer algorithms that are cleverer than these
        #  e.g. inspired by multi-arm-bandit algos like like epsilon-greedy, softmax, or upper confidence bound (UCB)

        def strat_activate(t, s_index):
            """
            Activate a specified strategy
            :param t: the current time
            :param s_index: the index of the strategy in the list of strategies
            :return: <nothing>
            """
            # print('t=%f Strat_activate, index=%d, active=%s' % (t, s_index, self.strats[s_index]['active'] ))
            self.strats[s_index]['start_t'] = t
            self.strats[s_index]['active'] = True
            self.strats[s_index]['profit'] = 0.0
            self.strats[s_index]['pps'] = 0.0

        vrbs = False

        # first update each active strategy's profit-per-second (pps) value -- this is the "fitness" of each strategy
        for s in self.strats:
            # debugging check: make profit be directly proportional to strategy, no noise
            # s['profit'] = 100 * abs(s['stratval'])
            # update pps
            active_flag = s['active']
            if active_flag:
                s['pps'] = self.profitpertime_update(time, s['start_t'], s['profit'])

        if self.optmzr == 'PRSH':

            if vrbs:
                # print('t=%f %s PRSH respond: shc_algo=%s eval_t=%f max_wait_t=%f' %
                #     (time, self.tid, shc_algo, self.strat_eval_time, self.strat_wait_time))
                pass

            # do we need to swap strategies?
            # this is based on time elapsed since last reset -- waiting for the current strategy to get a deal
            # -- otherwise a hopeless strategy can just sit there for ages doing nothing,
            # which would disadvantage the *other* strategies because they would never get a chance to score any profit.

            # NB this *cycles* through the available strats in sequence

            s = self.active_strat
            time_elapsed = time - self.last_strat_change_time
            if time_elapsed > self.strat_wait_time:
                # we have waited long enough: swap to another strategy
                self.strats[s]['active'] = False

                new_strat = s + 1
                if new_strat > self.k - 1:
                    new_strat = 0

                self.active_strat = new_strat
                self.strats[new_strat]['active'] = True
                self.last_strat_change_time = time

                if vrbs:
                    swt = self.strat_wait_time
                    print('t=%.3f (%.2fdays), %s PRSHrespond: strat[%d] elpsd=%.3f; wait_t=%.3f, pps=%f, new strat=%d' %
                          (time, time/86400, self.tid, s, time_elapsed, swt, self.strats[s]['pps'], new_strat))

            # code below here deals with creating a new set of k-1 mutants from the best of the k strats

            # assume that all strats have had long enough, and search for evidence to the contrary
            all_old_enough = True
            for s in self.strats:
                lifetime = time - s['start_t']
                if lifetime < self.strat_eval_time:
                    all_old_enough = False
                    break

            if all_old_enough:
                # all strategies have had long enough: which has made most profit?

                # sort them by profit
                strats_sorted = sorted(self.strats, key=lambda k: k['pps'], reverse=True)
                # strats_sorted = self.strats     # use this as a control: unsorts the strats, gives pure random walk.

                if vrbs:
                    print('PRSH %s: strat_eval_time=%f, all_old_enough=True' % (self.tid, self.strat_eval_time))
                    for s in strats_sorted:
                        print('s=%f, start_t=%f, lifetime=%f, $=%f, pps=%f' %
                              (s['stratval'], s['start_t'], time-s['start_t'], s['profit'], s['pps']))

                if self.params == 'landscape-mapper':
                    for s in self.strats:
                        self.mapper_outfile.write('time, %f, strat, %f, pps, %f\n' %
                                                  (time, s['stratval'], s['pps']))
                    self.mapper_outfile.flush()
                    sys.exit()

                else:
                    # if the difference between the top two strats is too close to call then flip a coin
                    # this is to prevent the same good strat being held constant simply by chance cos it is at index [0]
                    best_strat = 0
                    prof_diff = strats_sorted[0]['pps'] - strats_sorted[1]['pps']
                    if abs(prof_diff) < self.profit_epsilon:
                        # they're too close to call, so just flip a coin
                        best_strat = random.randint(0, 1)

                    if best_strat == 1:
                        # need to swap strats[0] and strats[1]
                        tmp_strat = strats_sorted[0]
                        strats_sorted[0] = strats_sorted[1]
                        strats_sorted[1] = tmp_strat

                    # the sorted list of strats replaces the existing list
                    self.strats = strats_sorted

                    # at this stage, strats_sorted[0] is our newly-chosen elite-strat, about to replicate

                    # now replicate and mutate the elite into all the other strats
                    for s in range(1, self.k):    # note range index starts at one not zero (elite is at [0])
                        self.strats[s]['stratval'] = self.mutate_strat(self.strats[0]['stratval'], 'gauss')
                        self.strats[s]['start_t'] = time
                        self.strats[s]['profit'] = 0.0
                        self.strats[s]['pps'] = 0.0
                    # and then update (wipe) records for the elite
                    self.strats[0]['start_t'] = time
                    self.strats[0]['profit'] = 0.0
                    self.strats[0]['pps'] = 0.0
                    self.active_strat = 0

                if vrbs:
                    print('%s: strat_eval_time=%f, MUTATED:' % (self.tid, self.strat_eval_time))
                    for s in self.strats:
                        print('s=%f start_t=%f, lifetime=%f, $=%f, pps=%f' %
                              (s['stratval'], s['start_t'], time-s['start_t'], s['profit'], s['pps']))

        elif self.optmzr == 'PRDE':
            # simple differential evolution

            # only initiate diff-evol once the active strat has been evaluated for long enough
            actv_lifetime = time - self.strats[self.active_strat]['start_t']
            if actv_lifetime >= self.strat_wait_time:

                if self.k < 4:
                    sys.exit('FAIL: k too small for diffevol')

                if self.diffevol['de_state'] == 'active_s0':
                    self.strats[self.active_strat]['active'] = False
                    # we've evaluated s0, so now we need to evaluate s_new
                    self.active_strat = self.diffevol['snew_index']
                    strat_activate(time, self.active_strat)

                    self.diffevol['de_state'] = 'active_snew'

                elif self.diffevol['de_state'] == 'active_snew':
                    # now we've evaluated s_0 and s_new, so we can do DE adaptive step
                    if vrbs:
                        print('PRDE trader %s' % self.tid)
                    i_0 = self.diffevol['s0_index']
                    i_new = self.diffevol['snew_index']
                    fit_0 = self.strats[i_0]['pps']
                    fit_new = self.strats[i_new]['pps']

                    if verbose:
                        print('DiffEvol: t=%.1f, i_0=%d, i0fit=%f, i_new=%d, i_new_fit=%f' %
                              (time, i_0, fit_0, i_new, fit_new))

                    if fit_new >= fit_0:
                        # new strat did better than old strat0, so overwrite new into strat0
                        self.strats[i_0]['stratval'] = self.strats[i_new]['stratval']

                    # do differential evolution

                    # pick four individual strategies at random, but they must be distinct
                    stratlist = list(range(0, self.k))    # create sequential list of strategy-numbers
                    random.shuffle(stratlist)             # shuffle the list

                    # s0 is next iteration's candidate for possible replacement
                    self.diffevol['s0_index'] = stratlist[0]

                    # s1, s2, s3 used in DE to create new strategy, potential replacement for s0
                    s1_index = stratlist[1]
                    s2_index = stratlist[2]
                    s3_index = stratlist[3]

                    # unpack the actual strategy values
                    s1_stratval = self.strats[s1_index]['stratval']
                    s2_stratval = self.strats[s2_index]['stratval']
                    s3_stratval = self.strats[s3_index]['stratval']

                    # this is the differential evolution "adaptive step": create a new individual
                    new_stratval = s1_stratval + self.diffevol['F'] * (s2_stratval - s3_stratval)

                    # clip to bounds
                    new_stratval = max(-1, min(+1, new_stratval))

                    # record it for future use (s0 will be evaluated first, then s_new)
                    self.strats[self.diffevol['snew_index']]['stratval'] = new_stratval

                    if verbose:
                        print('DiffEvol: t=%.1f, s0=%d, s1=%d, (s=%+f), s2=%d, (s=%+f), s3=%d, (s=%+f), sNew=%+f' %
                              (time, self.diffevol['s0_index'],
                               s1_index, s1_stratval, s2_index, s2_stratval, s3_index, s3_stratval, new_stratval))

                    # DC's intervention for fully converged populations
                    # is the stddev of the strategies in the population equal/close to zero?
                    strat_sum = 0.0
                    for s in range(self.k):
                        strat_sum += self.strats[s]['stratval']
                    strat_mean = strat_sum / self.k
                    sumsq = 0.0
                    for s in range(self.k):
                        diff = self.strats[s]['stratval'] - strat_mean
                        sumsq += (diff * diff)
                    strat_stdev = math.sqrt(sumsq / self.k)
                    if vrbs:
                        print('t=,%.1f, MeanStrat=, %+f, stdev=,%f' % (time, strat_mean, strat_stdev))
                    if strat_stdev < 0.0001:
                        # this population has converged
                        # mutate one strategy at random
                        randindex = random.randint(0, self.k - 1)
                        self.strats[randindex]['stratval'] = random.uniform(-1.0, +1.0)
                        if verbose:
                            print('Converged pop: set strategy %d to %+f' %
                                  (randindex, self.strats[randindex]['stratval']))

                    # set up next iteration: first evaluate s0
                    self.active_strat = self.diffevol['s0_index']
                    strat_activate(time, self.active_strat)

                    self.diffevol['de_state'] = 'active_s0'

                else:
                    sys.exit('FAIL: self.diffevol[\'de_state\'] not recognized')

        elif self.optmzr is None:
            # this is PRZI -- nonadaptive, no optimizer, nothing to change here.
            pass

        else:
            sys.exit('FAIL: bad value for self.optmzr')


class TraderZIP(Trader):
    """
    The Zero-Intelligence-Plus (ZIP) adaptive trading strategy of Cliff (1997).
    The code here implements the original ZIP, and also the strategy-optimizing variuants ZIPSH and ZIPDE.
    """

    # ZIP init key param-values are those used in Cliff's 1997 original HP Labs tech report
    # NB this implementation keeps separate margin values for buying & selling,
    #    so a single trader can both buy AND sell
    #    -- in the original, traders were either buyers OR sellers

    @staticmethod
    def strat_csv_str(strat):
        """
        Take a ZIP strategy vector and return it as a csv-format string.
        :param strat: the vector of values for the ZIP trader's strategy
        :return: the csv-format string.
        """
        if strat is None:
            csv_str = 'None, '
        else:
            csv_str = 'mBuy=,%+5.3f, mSel=,%+5.3f, b=,%5.3f, m=,%5.3f, ca=,%6.4f, cr=,%6.4f, ' % \
                      (strat['m_buy'], strat['m_sell'], strat['beta'], strat['momntm'], strat['ca'], strat['cr'])
        return csv_str

    @staticmethod
    def mutate_strat(s, mode):
        """
        How to mutate the strategy values when evolving / hill-climbing
        :param s: the strategy to be mutated.
        :param mode: specify Gaussian or some other form of distribution for the mutation delta (currently only Gauss).
        :return: the mutated strategy.
        """

        def gauss_mutate_clip(value, sdev, range_min, range_max):
            """
            Mutation of strategy-value by injection of zero-mean Gaussian noise, followed by clipping to keep in range.
            :param value: the value to be mutated.
            :param sdev: the standard deviation on the Gaussian noise.
            :param range_min: lower bound on the range.
            :param range_max: upper bound opn the range.
            :return: the mutated value.
            """
            mut_val = value
            while mut_val == value:
                mut_val = value + random.gauss(0.0, sdev)
                if mut_val > range_max:
                    mut_val = range_max
                elif mut_val < range_min:
                    mut_val = range_min
            return mut_val

        # mutate each element of a ZIP strategy independently
        # and clip each to remain within bounds
        if mode == 'gauss':
            big_sdev = 0.025
            small_sdev = 0.0025
            margin_buy = gauss_mutate_clip(s['m_buy'], big_sdev, -1.0, 0)
            margin_sell = gauss_mutate_clip(s['m_sell'], big_sdev, 0.0, 1.0)
            beta = gauss_mutate_clip(s['beta'], big_sdev, 0.0, 1.0)
            momntm = gauss_mutate_clip(s['momntm'], big_sdev, 0.0, 1.0)
            ca = gauss_mutate_clip(s['ca'], small_sdev, 0.0, 1.0)
            cr = gauss_mutate_clip(s['cr'], small_sdev, 0.0, 1.0)
            new_strat = {'m_buy': margin_buy, 'm_sell': margin_sell, 'beta': beta, 'momntm': momntm, 'ca': ca, 'cr': cr}
        else:
            sys.exit('FAIL: bad mode in mutate_strat')
        return new_strat

    def __init__(self, ttype, tid, balance, params, time):
        """
        Create a ZIP/ZIPSH/ZIPDE trader.
        :param ttype: the string identifying the trader-type (what strategy is this).
        :param tid: the trader i.d. string.
        :param balance: the starting bank balance for this trader.
        :param params: any additional parameters.
        :param time: the current time.
        """

        Trader.__init__(self, ttype, tid, balance, params, time)

        # this set of one-liner functions named init_*() are just to make the init params obvious for ease of editing
        # for ZIP, a strategy is specified as a 6-tuple: (margin_buy, margin_sell, beta, momntm, ca, cr)
        # the 'default' values mentioned in comments below come from Cliff 1997 -- good ranges for most situations

        def init_beta():
            """in Cliff 1997 the initial beta values are U(0.1, 0.5)"""
            return random.uniform(0.1, 0.5)

        def init_momntm():
            """in Cliff 1997 the initial momentum values are U(0.0, 0.1)"""
            return random.uniform(0.0, 0.1)

        def init_ca():
            # in Cliff 1997 c_a was a system constant, the same for all traders, set to 0.05
            # here we take the liberty of introducing some variation
            return random.uniform(0.01, 0.05)

        def init_cr():
            # in Cliff 1997 c_r was a system constant, the same for all traders, set to 0.05
            # here we take the liberty of introducing some variation
            return random.uniform(0.01, 0.05)

        def init_margin():
            # in Cliff 1997 the initial margin values are U(0.05, 0.35)
            return random.uniform(0.05, 0.35)

        def init_stratwaittime():
            # not in Cliff 1997: use whatever limits you think best.
            return 7200 + random.randint(0, 3600)

        # unpack the params
        # for ZIPSH and ZIPDE params should include values for optimizer and k
        # if no params specified then defaults to ZIP with strat values as in Cliff1997

        # default parameter values
        k = 1
        optimizer = None    # no optimizer => plain non-optimizing ZIP
        logging = False

        # did call provide different params?
        if type(params) is dict:
            if 'k' in params:
                k = params['k']
            if 'optimizer' in params:
                optimizer = params['optimizer']
            self.logfile = None
            if 'logfile' in params:
                logging = True
                logfilename = params['logfile'] + '_' + tid + '_log.csv'
                self.logfile = open(logfilename, 'w')

        # the following set of variables are needed for original ZIP *and* for its optimizing extensions e.g. ZIPSH
        self.logging = logging
        self.willing = 1
        self.able = 1
        self.job = None             # this gets switched to 'Bid' or 'Ask' depending on order-type
        self.active = False         # gets switched to True while actively working an order
        self.prev_change = 0        # this was called last_d in Cliff'97
        self.beta = init_beta()
        self.momntm = init_momntm()
        self.ca = init_ca()         # self.ca & self.cr were hard-coded in '97 but parameterised later
        self.cr = init_cr()
        self.margin = None          # this was called profit in Cliff'97
        self.margin_buy = -1.0 * init_margin()
        self.margin_sell = init_margin()
        self.price = None
        self.limit = None
        self.prev_best_bid_p = None     # best bid price on LOB on previous update
        self.prev_best_bid_q = None     # best bid quantity on LOB on previous update
        self.prev_best_ask_p = None     # best ask price on LOB on previous update
        self.prev_best_ask_q = None     # best ask quantity on LOB on previous update

        # the following set of variables are needed only by ZIP with added hyperparameter optimization (e.g. ZIPSH)
        self.k = k                  # how many strategies evaluated at any one time?
        self.optmzr = optimizer     # what form of strategy-optimizer we're using
        self.strats = None          # the list of strategies, each of which is a dictionary
        self.strat_wait_time = init_stratwaittime()     # how many secs do we give any one strat before switching?
        self.strat_eval_time = self.k * self.strat_wait_time  # time to cycle through evaluating all k strategies
        self.last_strat_change_time = time  # what time did we last change strategies?
        self.active_strat = 0       # which of the k strategies are we currently playing? -- start with 0
        self.profit_epsilon = 0.0 * random.random()     # min profit-per-sec difference between strategies that counts

        if self.optmzr is not None and k > 1:
            # we're doing some form of k-armed strategy-optimization with multiple strategies
            self.strats = []
            # strats[0] is whatever we've just assigned, and is the active strategy
            strategy = {'m_buy': self.margin_buy, 'm_sell': self.margin_sell, 'beta': self.beta,
                        'momntm': self.momntm, 'ca': self.ca, 'cr': self.cr}
            self.strats.append({'stratvec': strategy, 'start_t': time, 'active': True,
                                'profit': 0, 'pps': 0, 'evaluated': False})

            # rest of *initial* strategy set is generated from same distributions, but these are all inactive
            for s in range(1, k):
                strategy = {'m_buy': -1.0 * init_margin(), 'm_sell': init_margin(), 'beta': init_beta(),
                            'momntm': init_momntm(), 'ca': init_ca(), 'cr': init_cr()}
                self.strats.append({'stratvec': strategy, 'start_t': time, 'active': False,
                                    'profit': 0, 'pps': 0, 'evaluated': False})

        if self.logging:
            self.logfile.write('ZIP, Tid, %s, ttype, %s, optmzr, %s, strat_wait_time, %f, n_strats=%d:\n' %
                               (self.tid, self.ttype, self.optmzr, self.strat_wait_time, self.k))
            for s in self.strats:
                self.logfile.write(str(s)+'\n')

    def getorder(self, time, countdown, lob):
        """
        Create the next order for this trader
        :param time: the current time
        :param countdown: time remaining until market closes (not used in ZIP)
        :param lob: the current state of the LOB
        :return: this trader's next order.
        """
        # this test for negative countdown is purely to stop PyCharm warning about unused parameter value
        if countdown < 0:
            sys.exit('Negative countdown')

        if len(self.orders) < 1:
            self.active = False
            order = None
        else:
            self.active = True
            self.limit = self.orders[0].price
            self.job = self.orders[0].otype
            if self.job == 'Bid':
                # currently a buyer (working a bid order)
                self.margin = self.margin_buy
            else:
                # currently a seller (working a sell order)
                self.margin = self.margin_sell
            quoteprice = int(self.limit * (1 + self.margin))

            lastprice = -1  # dummy value for if there is no lastprice
            if self.lastquote is not None:
                lastprice = self.lastquote.price

            self.price = quoteprice
            order = Order(self.tid, self.job, quoteprice, self.orders[0].qty, time, lob['QID'])
            self.lastquote = order

            if self.logging and order.price != lastprice:
                self.logfile.write('%f, Order:, %s\n' % (time, str(order)))
        return order

    def respond(self, time, lob, trade, vrbs):
        """
        Update ZIP profit margin on basis of what happened in market.
        For ZIPSH and ZIPDE, also maybe switch strategy and/or generate new strategies to evaluate.
        :param time: the current time.
        :param lob: the current state of the LOB.
        :param trade: details of most recent trade, if any.
        :param vrbs: if True then print a running commentary of what is going on.
        :return: snapshot: if Ture, then the caller of respond() should print the next frame of system snapshot data.
        """
        # ZIP trader responds to market events, altering its margin
        # does this whether it currently has an order to work or not

        def target_up(price):
            """ Generate a higher target price by randomly perturbing given price"""
            ptrb_abs = self.ca * random.random()  # absolute shift
            ptrb_rel = price * (1.0 + (self.cr * random.random()))  # relative shift
            target = int(round(ptrb_rel + ptrb_abs, 0))
            # #                        print('TargetUp: %d %d\n' % (price,target))
            return target

        def target_down(price):
            """ Generate a lower target price by randomly perturbing given price"""
            ptrb_abs = self.ca * random.random()  # absolute shift
            ptrb_rel = price * (1.0 - (self.cr * random.random()))  # relative shift
            target = int(round(ptrb_rel - ptrb_abs, 0))
            # #                        print('TargetDn: %d %d\n' % (price,target))
            return target

        def willing_to_trade(price):
            """ Am I willing to trade at this price?"""
            willing = False
            if self.job == 'Bid' and self.active and self.price >= price:
                willing = True
            if self.job == 'Ask' and self.active and self.price <= price:
                willing = True
            return willing

        def profit_alter(price):
            """
            ZIP profit-margin update on basis of target price -- updates self.margin.
            :param price: the target price.
            :return: <nothing>
            """
            oldprice = self.price
            diff = price - oldprice
            change = ((1.0 - self.momntm) * (self.beta * diff)) + (self.momntm * self.prev_change)
            self.prev_change = change
            newmargin = ((self.price + change) / self.limit) - 1.0

            if self.job == 'Bid':
                if newmargin < 0.0:
                    self.margin_buy = newmargin
                    self.margin = newmargin
            else:
                if newmargin > 0.0:
                    self.margin_sell = newmargin
                    self.margin = newmargin

            # set the price from limit and profit-margin
            self.price = int(round(self.limit * (1.0 + self.margin), 0))

        def load_strat(stratvec, birthtime):
            """
            Copy the strategy vector into the ZIP trader's params and timestamp it.
            :param stratvec: the strategy vector.
            :param birthtime: the timestamp.
            :return: <nothing>
            """
            self.margin_buy = stratvec['m_buy']
            self.margin_sell = stratvec['m_sell']
            self.beta = stratvec['beta']
            self.momntm = stratvec['momntm']
            self.ca = stratvec['ca']
            self.cr = stratvec['cr']
            # bookkeeping
            self.n_trades = 0
            self.birthtime = birthtime
            self.balance = 0
            self.profitpertime = 0

        def strat_activate(t, s_index):
            """
            Activate a specified strategy-vector.
            :param t: the current time.
            :param s_index: the index of the strategy to be activated.
            :return: <nothing>
            """
            # print('t=%f Strat_activate, index=%d, active=%s' % (t, s_index, self.strats[s_index]['active'] ))
            self.strats[s_index]['start_t'] = t
            self.strats[s_index]['active'] = True
            self.strats[s_index]['profit'] = 0.0
            self.strats[s_index]['pps'] = 0.0
            self.strats[s_index]['evaluated'] = False

        # snapshot says whether the caller of respond() should print next frame of system snapshot data
        snapshot = False

        if self.optmzr == 'ZIPSH':

            # ZIP with simple-stochastic-hillclimber optimization of strategy (hyperparameter values)

            # NB this *cycles* through the available strats in sequence (i.e., it doesn't shuffle them)

            # first update the pps for each active strategy
            for s in self.strats:
                # update pps
                active_flag = s['active']
                if active_flag:
                    s['pps'] = self.profitpertime_update(time, s['start_t'], s['profit'])

            # have we evaluated all the strategies?
            # (could instead just compare active_strat to k, but checking them all in sequence is arguably clearer)
            # assume that all strats have been evaluated, and search for evidence to the contrary
            all_evaluated = True
            for s in self.strats:
                if s['evaluated'] is False:
                    all_evaluated = False
                    break

            if all_evaluated:
                # time to generate a new set/population of k candidate strategies
                # NB when the final strategy in the trader's set/popln is evaluated, the set is then sorted into
                # descending order of profitability, so when we get to here we know that strats[0] is elite

                if vrbs and self.tid == 'S00':
                    print('t=%.3f, ZIPSH %s: strat_eval_time=%.3f,' % (time, self.tid, self.strat_eval_time))
                    for s in self.strats:
                        print('%s, start_t=%f, $=%f, pps=%f' %
                              (self.strat_csv_str(s['stratvec']), s['start_t'], s['profit'], s['pps']))

                # if the difference between the top two strats is too close to call then flip a coin
                # this is to prevent the same good strat being held constant simply by chance cos it is at index [0]
                best_strat = 0
                prof_diff = self.strats[0]['pps'] - self.strats[1]['pps']
                if abs(prof_diff) < self.profit_epsilon:
                    # they're too close to call, so just flip a coin
                    best_strat = random.randint(0, 1)

                    if best_strat == 1:
                        # need to swap strats[0] and strats[1]
                        tmp_strat = self.strats[0]
                        self.strats[0] = self.strats[1]
                        self.strats[1] = tmp_strat

                # at this stage, strats[0] is our newly-chosen elite-strat, about to replicate & mutate

                # now replicate and mutate the elite into all the other strats
                for s in range(1, self.k):  # note range index starts at one not zero (elite is at [0])
                    self.strats[s]['stratvec'] = self.mutate_strat(self.strats[0]['stratvec'], 'gauss')
                    strat_activate(time, s)

                # and then update (wipe) records for the elite
                strat_activate(time, 0)

                # load the elite into the ZIP trader params
                load_strat(self.strats[0]['stratvec'], time)

                self.active_strat = 0

                if vrbs and self.tid == 'S00':
                    print('%s: strat_eval_time=%f, best_strat=%d, MUTATED:' %
                          (self.tid, self.strat_eval_time, best_strat))
                    for s in self.strats:
                        print('%s start_t=%.3f, lifetime=%.3f, $=%.3f, pps=%f' %
                              (self.strat_csv_str(s['stratvec']), s['start_t'], time - s['start_t'], s['profit'],
                               s['pps']))

            else:
                # we're still evaluating

                s = self.active_strat
                time_elapsed = time - self.strats[s]['start_t']
                if time_elapsed >= self.strat_wait_time:
                    # this strategy has had long enough: update records for this strategy, then swap to another strategy
                    self.strats[s]['active'] = False
                    self.strats[s]['profit'] = self.balance
                    self.strats[s]['pps'] = self.profitpertime
                    self.strats[s]['evaluated'] = True

                    new_strat = s + 1
                    if new_strat > self.k - 1:
                        # we've just evaluated the last of this trader's set of strategies
                        # sort the strategies into order of descending profitability
                        strats_sorted = sorted(self.strats, key=lambda k: k['pps'], reverse=True)

                        # use this as a control: unsorts the strats, gives pure random walk.
                        # strats_sorted = self.strats

                        # the sorted list of strats replaces the existing list
                        self.strats = strats_sorted

                        # signal that we want to record a system snapshot because this trader's eval loop finished
                        snapshot = True

                        # NB not updating self.active_strat here because next call to respond() generates new popln

                    else:
                        # copy the new strategy vector into the trader's params
                        load_strat(self.strats[new_strat]['stratvec'], time)
                        self.strats[new_strat]['start_t'] = time
                        self.active_strat = new_strat
                        self.strats[new_strat]['active'] = True
                        self.last_strat_change_time = time

                    if vrbs and self.tid == 'S00':
                        vstr = 't=%.3f (%.2fdays) %s ZIPSH respond:' % (time, time/86400, self.tid)
                        vstr += ' strat[%d] elapsed=%.3f; wait_t=%.3f, pps=%f' % \
                                (s, time_elapsed, self.strat_wait_time, self.strats[s]['pps'])
                        if new_strat > self.k - 1:
                            print(vstr)
                        else:
                            vstr += ' switching to strat[%d]: %s' %\
                                    (new_strat, self.strat_csv_str(self.strats[new_strat]['stratvec']))

        elif self.optmzr is None:
            # this is vanilla ZIP -- nonadaptive, no optimizer, nothing to change here.
            pass

        # what, if anything, has happened on the bid LOB?
        bid_improved = False
        bid_hit = False
        lob_best_bid_p = lob['bids']['best']
        lob_best_bid_q = None
        if lob_best_bid_p is not None:
            # non-empty bid LOB
            lob_best_bid_q = lob['bids']['lob'][-1][1]
            if (self.prev_best_bid_p is not None) and (self.prev_best_bid_p < lob_best_bid_p):
                # best bid has improved
                # NB doesn't check if the improvement was by self
                bid_improved = True
            elif trade is not None and ((self.prev_best_bid_p > lob_best_bid_p) or (
                    (self.prev_best_bid_p == lob_best_bid_p) and (self.prev_best_bid_q > lob_best_bid_q))):
                # previous best bid was hit
                bid_hit = True
        elif self.prev_best_bid_p is not None:
            # the bid LOB has been emptied: was it cancelled or hit?
            last_tape_item = lob['tape'][-1]
            if last_tape_item['type'] == 'Cancel':
                bid_hit = False
            else:
                bid_hit = True

        # what, if anything, has happened on the ask LOB?
        ask_improved = False
        ask_lifted = False
        lob_best_ask_p = lob['asks']['best']
        lob_best_ask_q = None
        if lob_best_ask_p is not None:
            # non-empty ask LOB
            lob_best_ask_q = lob['asks']['lob'][0][1]
            if (self.prev_best_ask_p is not None) and (self.prev_best_ask_p > lob_best_ask_p):
                # best ask has improved -- NB doesn't check if the improvement was by self
                ask_improved = True
            elif trade is not None and ((self.prev_best_ask_p < lob_best_ask_p) or (
                    (self.prev_best_ask_p == lob_best_ask_p) and (self.prev_best_ask_q > lob_best_ask_q))):
                # trade happened and best ask price has got worse, or stayed same but quantity reduced
                # -- assume previous best ask was lifted
                ask_lifted = True
        elif self.prev_best_ask_p is not None:
            # the ask LOB is empty now but was not previously: canceled or lifted?
            last_tape_item = lob['tape'][-1]
            if last_tape_item['type'] == 'Cancel':
                ask_lifted = False
            else:
                ask_lifted = True

        if vrbs and (bid_improved or bid_hit or ask_improved or ask_lifted):
            print('ZIP respond: B_improved', bid_improved, 'B_hit', bid_hit,
                  'A_improved', ask_improved, 'A_lifted', ask_lifted)

        deal = bid_hit or ask_lifted

        if self.job == 'Ask':
            # seller
            if deal:
                tradeprice = trade['price']
                if self.price <= tradeprice:
                    # could sell for more? raise margin
                    target_price = target_up(tradeprice)
                    profit_alter(target_price)
                elif ask_lifted and self.active and not willing_to_trade(tradeprice):
                    # wouldn't have got this deal, still working order, so reduce margin
                    target_price = target_down(tradeprice)
                    profit_alter(target_price)
            else:
                # no deal: aim for a target price higher than best bid
                if ask_improved and self.price > lob_best_ask_p:
                    if lob_best_bid_p is not None:
                        target_price = target_up(lob_best_bid_p)
                    else:
                        target_price = lob['asks']['worst']  # stub quote
                    profit_alter(target_price)

        if self.job == 'Bid':
            # buyer
            if deal:
                tradeprice = trade['price']
                if self.price >= tradeprice:
                    # could buy for less? raise margin (i.e. cut the price)
                    target_price = target_down(tradeprice)
                    profit_alter(target_price)
                elif bid_hit and self.active and not willing_to_trade(tradeprice):
                    # wouldn't have got this deal, still working order, so reduce margin
                    target_price = target_up(tradeprice)
                    profit_alter(target_price)
            else:
                # no deal: aim for target price lower than best ask
                if bid_improved and self.price < lob_best_bid_p:
                    if lob_best_ask_p is not None:
                        target_price = target_down(lob_best_ask_p)
                    else:
                        target_price = lob['bids']['worst']  # stub quote
                    profit_alter(target_price)

        # remember the best LOB data ready for next response
        self.prev_best_bid_p = lob_best_bid_p
        self.prev_best_bid_q = lob_best_bid_q
        self.prev_best_ask_p = lob_best_ask_p
        self.prev_best_ask_q = lob_best_ask_q

        # return value of respond() tells caller whether to print a new frame of system-snapshot data
        return snapshot


class TraderPT1(Trader):
    """
    A minimally simple propreitary trader that buys & sells to make profit

    PT1 long-only buy-and-hold strategy in pseudocode:

    1 wait until the market has been open for 5 minutes (to give prices a chance to settle)
    2 then repeat forever:
    2.1 if (I am not holding a unit)
    2.1.1  and (best ask price is "cheap" -- i.e., less than average of recent transaction prices)
    2.1.2  and (I have enough money in my bank to pay the asking price)
    2.2 then
    2.2.1   (buy the unit -- lift the ask)
    2.2.2   (remember the purchase-price I paid for it)
    2.3 else if (I am holding a unit)
    2.4 then
    2.4.1   (my asking-price is that unit’s purchase-price plus my profit margin)
    2.4.1   if (best bid price is more than my asking price)
    2.4.1   then
    2.4.1.1    (sell my unit -- hit the bid)
    2.4.1.2    (put the money in my bank)
    """

    def __init__(self, ttype, tid, balance, params, time):
        """
        Construct a PT1 trader
        :param ttype: the ticker-symbol for the type of trader (its strategy)
        :param tid: the trader id
        :param balance: the trader's bank balance
        :param params: a dictionary of optional parameter-values to override the defaults
        :param time: the current time.
        """
        
        init_verbose = True
        
        Trader.__init__(self, ttype, tid, balance, params, time)
        self.job = 'Buy'  # flag switches between 'Buy' & 'Sell'; shows what PT1 is currently trying to do
        self.last_purchase_price = None

        # Default parameter-values
        self.n_past_trades = 5      # how many recent trades used to compute average price (avg_p)?
        self.bid_percent = 0.9999   # what percentage of avg_p should best_ask be for this trader to bid
        self.ask_delta = 5          # how much (absolute value) to improve on purchase price

        # Did the caller provide different params?
        if type(params) is dict:
            if 'bid_percent' in params:
                self.bid_percent = params['bid_percent']
                if self.bid_percent > 1.0 or self.bid_percent < 0.01:
                    sys.exit('FAIL: self.bid_percent=%f not in range [0.01,1.0])' % self.bid_percent)
            if 'ask_delta' in params:
                self.ask_delta = params['ask_delta']
                if self.ask_delta < 0:
                    sys.exit('Fail: PT1 ask_delta can\'t be negative (it\'s an absolute value)')
            if 'n_past_trades' in params:
                self.n_past_trades = int(round(params['n_past_trades']))
                if self.n_past_trades < 1:
                    sys.exit('Fail: PT1 n_past trades must be 1 or more')
                    
        if init_verbose:
            print('PT1 init: n_past_trades=%d, bid_percent=%6.5f, ask_delta=%d\n'
                  % (self.n_past_trades, self.bid_percent, self.ask_delta))
            
    def getorder(self, time, countdown, lob):
        """
        return this trader's order when it is polled in the main market_session loop.
        :param time: the current time.
        :param countdown: the time remaining until market closes (not currently used).
        :param lob: the public lob.
        :return: trader's new order, or None.
        """
        # this test for negative countdown is purely to stop PyCharm warning about unused parameter value
        if countdown < 0:
            sys.exit('Negative countdown')

        if len(self.orders) < 1 or time < 5 * 60:
            order = None
        else:
            quoteprice = self.orders[0].price
            order = Order(self.tid,
                          self.orders[0].otype,
                          quoteprice,
                          self.orders[0].qty,
                          time, lob['QID'])
            self.lastquote = order
        return order

    def respond(self, time, lob, trade, vrbs):
        """
        Respond to the current state of the public lob.
        Buys if best bid is less than simple moving average of recent transcaction prices.
        Sells as soon as it can make an acceptable profit.
        :param time: the current time
        :param lob: the current public lob
        :param trade:
        :param vrbs: verbosity -- if True then print running commentary, else stay silent
        :return: <nothing>
        """

        vstr = 't=%f PT1 respond: ' % time

        # what is average price of most recent n trades?
        # work backwards from end of tape (most recent trade)
        tape_position = -1
        n_prices = 0
        sum_prices = 0
        avg_price_ok = False
        avg_price = -1
        while n_prices < self.n_past_trades and abs(tape_position) < len(lob['tape']):
            if lob['tape'][tape_position]['type'] == 'Trade':
                price = lob['tape'][tape_position]['price']
                n_prices += 1
                sum_prices += price
            tape_position -= 1
        if n_prices == self.n_past_trades:
            # there's been enough trades to form an acceptable average
            avg_price = int(round(sum_prices / n_prices))
            avg_price_ok = True
        vstr += "avg_price_ok=%s, avg_price=%d " % (avg_price_ok, avg_price)

        # buying?
        if self.job == 'Buy' and avg_price_ok:
            vstr += 'Buying - '
            # see what's on the LOB
            if lob['asks']['n'] > 0:
                # there is at least one ask on the LOB
                best_ask = lob['asks']['best']
                if best_ask / avg_price < self.bid_percent:
                    # bestask is good value: send a spread-crossing bid to lift the ask
                    bidprice = best_ask + 1
                    if bidprice < self.balance:
                        # can afford to buy
                        # create the bid by issuing order to self, which will be processed in getorder()
                        order = Order(self.tid, 'Bid', bidprice, 1, time, lob['QID'])
                        self.orders = [order]
                        vstr += 'Best ask=%d, bidprice=%d, order=%s ' % (best_ask, bidprice, order)
                else:
                    vstr += 'bestask=%d >= avg_price=%d' % (best_ask, avg_price)
            else:
                vstr += 'No asks on LOB'
        # selling?
        elif self.job == 'Sell':
            vstr += 'Selling - '
            # see what's on the LOB
            if lob['bids']['n'] > 0:
                # there is at least one bid on the LOB
                best_bid = lob['bids']['best']
                # sell single unit at price of purchaseprice+askdelta
                askprice = self.last_purchase_price + self.ask_delta
                if askprice < best_bid:
                    # seems we have a buyer
                    # lift the ask by issuing order to self, which will processed in getorder()
                    order = Order(self.tid, 'Ask', askprice, 1, time, lob['QID'])
                    self.orders = [order]
                    vstr += 'Best bid=%d greater than askprice=%d order=%s ' % (best_bid, askprice, order)
                else:
                    vstr += 'Best bid=%d too low for askprice=%d ' % (best_bid, askprice)
            else:
                vstr += 'No bids on LOB'

        self.profitpertime = self.profitpertime_update(time, self.birthtime, self.balance)

        if vrbs:
            print(vstr)

    def bookkeep(self, time, trade, order, vrbs):
        """
        Update trader's records of its bank balance, current orders, and current job
        :param trade: the current time
        :param order: this trader's successful order
        :param vrbs: verbosity -- if True then print running commentary, else stay silent.
        :param time: the current time.
        :return: <nothing>
        """

        # output string outstr is printed if vrbs==True
        mins = int(time//60)
        secs = time - 60 * mins
        hrs = int(mins//60)
        mins = mins - 60 * hrs
        outstr = 't=%f (%dh%02dm%02ds) %s (%s) bookkeep: orders=' % (time, hrs, mins, secs, self.tid, self.ttype)
        for order in self.orders:
            outstr = outstr + str(order)

        self.blotter.append(trade)  # add trade record to trader's blotter

        # NB What follows is **LAZY** -- assumes all orders are quantity=1
        transactionprice = trade['price']
        if self.orders[0].otype == 'Bid':
            # Bid order succeeded, remember the price and adjust the balance
            self.balance -= transactionprice
            self.last_purchase_price = transactionprice
            self.job = 'Sell'  # now try to sell it for a profit
        elif self.orders[0].otype == 'Ask':
            # Sold! put the money in the bank
            self.balance += transactionprice
            self.last_purchase_price = 0
            self.job = 'Buy'  # now go back and buy another one
        else:
            sys.exit('FATAL: PT1 doesn\'t know .otype %s\n' % self.orders[0].otype)

        if vrbs:
            net_worth = self.balance + self.last_purchase_price
            print('%s Balance=%d NetWorth=%d' % (outstr, self.balance, net_worth))

        self.del_order(order)  # delete the order

    # end of PT1 definition


class TraderPT2(Trader):
    """
    A A minimally simple propreitary trader that buys & sells to make profit

    PT2 long-only buy-and-hold strategy in pseudocode:

    1 wait until the market has been open for 5 minutes (to give prices a chance to settle)
    2 then repeat forever:
    2.1 if (I am not holding a unit)
    2.1.1  and (best ask price is "cheap" -- i.e., less than average of recent transaction prices)
    2.1.2  and (I have enough money in my bank to pay the asking price)
    2.2 then
    2.2.1   (buy the unit -- lift the ask)
    2.2.2   (remember the purchase-price I paid for it)
    2.3 else if (I am holding a unit)
    2.4 then
    2.4.1   (my asking-price is that unit’s purchase-price plus my profit margin)
    2.4.1   if (best bid price is more than my asking price)
    2.4.1   then
    2.4.1.1    (sell my unit -- hit the bid)
    2.4.1.2    (put the money in my bank)
    """

    def __init__(self, ttype, tid, balance, params, time):
        """
        Construct a PT2 trader
        :param ttype: the ticker-symbol for the type of trader (its strategy)
        :param tid: the trader id
        :param balance: the trader's bank balance
        :param params: a dictionary of optional parameter-values to override the defaults
        :param time: the current time.
        """

        Trader.__init__(self, ttype, tid, balance, params, time)
        self.job = 'Buy'  # flag switches between 'Buy' & 'Sell'; shows what PT2 is currently trying to do
        self.last_purchase_price = None
        
        init_verbose = True

        # Default parameter-values
        self.n_past_trades = 5      # how many recent trades used to compute average price (avg_p)?
        self.bid_percent = 0.9999   # what percentage of avg_p should best_ask be for this trader to bid
        self.ask_delta = 5          # how much (absolute value) to improve on purchase price

        # Did the caller provide different params?
        if type(params) is dict:
            if 'bid_percent' in params:
                self.bid_percent = params['bid_percent']
                if self.bid_percent > 1.0 or self.bid_percent < 0.01:
                    sys.exit('FAIL: PT2 self.bid_percent=%f not in range [0.01,1.0])' % self.bid_percent)
            if 'ask_delta' in params:
                self.ask_delta = params['ask_delta']
                if self.ask_delta < 0:
                    sys.exit('Fail: PT2 ask_delta can\'t be negative (it\'s an absolute value)')
            if 'n_past_trades' in params:
                self.n_past_trades = int(round(params['n_past_trades']))
                if self.n_past_trades < 1:
                    sys.exit('Fail: PT2 n_past trades must be 1 or more')
                    
        if init_verbose:
            print('PT2 init: n_past_trades=%d, bid_percent=%6.5f, ask_delta=%d\n'
                  % (self.n_past_trades, self.bid_percent, self.ask_delta))

    def getorder(self, time, countdown, lob):
        """
        return this trader's order when it is polled in the main market_session loop.
        :param time: the current time.
        :param countdown: the time remaining until market closes (not currently used).
        :param lob: the public lob.
        :return: trader's new order, or None.
        """
        # this test for negative countdown is purely to stop PyCharm warning about unused parameter value
        if countdown < 0:
            sys.exit('Negative countdown')

        if len(self.orders) < 1 or time < 5 * 60:
            order = None
        else:
            quoteprice = self.orders[0].price
            order = Order(self.tid,
                          self.orders[0].otype,
                          quoteprice,
                          self.orders[0].qty,
                          time, lob['QID'])
            self.lastquote = order
        return order

    def respond(self, time, lob, trade, vrbs):
        """
        Respond to the current state of the public lob.
        Buys if best bid is less than simple moving average of recent transcaction prices.
        Sells as soon as it can make an acceptable profit.
        :param time: the current time
        :param lob: the current public lob
        :param trade:
        :param vrbs: if True then print running commentary, else stay silent
        :return: <nothing>
        """

        vstr = 't=%f PT2 respond: ' % time

        # what is average price of most recent n trades?
        # work backwards from end of tape (most recent trade)
        tape_position = -1
        n_prices = 0
        sum_prices = 0
        avg_price_ok = False
        avg_price = -1
        while n_prices < self.n_past_trades and abs(tape_position) < len(lob['tape']):
            if lob['tape'][tape_position]['type'] == 'Trade':
                price = lob['tape'][tape_position]['price']
                n_prices += 1
                sum_prices += price
            tape_position -= 1
        if n_prices == self.n_past_trades:
            # there's been enough trades to form an acceptable average
            avg_price = int(round(sum_prices / n_prices))
            avg_price_ok = True
        vstr += "avg_price_ok=%s, avg_price=%d " % (avg_price_ok, avg_price)

        # buying?
        if self.job == 'Buy' and avg_price_ok:
            vstr += 'Buying - '
            # see what's on the LOB
            if lob['asks']['n'] > 0:
                # there is at least one ask on the LOB
                best_ask = lob['asks']['best']
                if best_ask / avg_price < self.bid_percent:
                    # bestask is good value: send a spread-crossing bid to lift the ask
                    bidprice = best_ask + 1
                    if bidprice < self.balance:
                        # can afford to buy
                        # create the bid by issuing order to self, which will be processed in getorder()
                        order = Order(self.tid, 'Bid', bidprice, 1, time, lob['QID'])
                        self.orders = [order]
                        vstr += 'Best ask=%d, bidprice=%d, order=%s ' % (best_ask, bidprice, order)
                else:
                    vstr += 'bestask=%d >= avg_price=%d' % (best_ask, avg_price)
            else:
                vstr += 'No asks on LOB'
        # selling?
        elif self.job == 'Sell':
            vstr += 'Selling - '
            # see what's on the LOB
            if lob['bids']['n'] > 0:
                # there is at least one bid on the LOB
                best_bid = lob['bids']['best']
                # sell single unit at price of purchaseprice+askdelta
                askprice = self.last_purchase_price + self.ask_delta
                if askprice < best_bid:
                    # seems we have a buyer
                    # lift the ask by issuing order to self, which will processed in getorder()
                    order = Order(self.tid, 'Ask', askprice, 1, time, lob['QID'])
                    self.orders = [order]
                    vstr += 'Best bid=%d greater than askprice=%d order=%s ' % (best_bid, askprice, order)
                else:
                    vstr += 'Best bid=%d too low for askprice=%d ' % (best_bid, askprice)
            else:
                vstr += 'No bids on LOB'

        self.profitpertime = self.profitpertime_update(time, self.birthtime, self.balance)

        if vrbs:
            print(vstr)

    def bookkeep(self, time, trade, order, vrbs):
        """
        Update trader's records of its bank balance, current orders, and current job
        :param trade: the current time
        :param order: this trader's successful order
        :param vrbs: if True then print a running commentary, otherwise stay silent.
        :param time: the current time.
        :return: <nothing>
        """

        # output string outstr is printed if vrbs==True
        mins = int(time//60)
        secs = time - 60 * mins
        hrs = int(mins//60)
        mins = mins - 60 * hrs
        outstr = 't=%f (%dh%02dm%02ds) %s (%s) bookkeep: orders=' % (time, hrs, mins, secs, self.tid, self.ttype)
        for order in self.orders:
            outstr = outstr + str(order)

        self.blotter.append(trade)  # add trade record to trader's blotter

        # NB What follows is **LAZY** -- assumes all orders are quantity=1
        transactionprice = trade['price']
        if self.orders[0].otype == 'Bid':
            # Bid order succeeded, remember the price and adjust the balance
            self.balance -= transactionprice
            self.last_purchase_price = transactionprice
            self.job = 'Sell'  # now try to sell it for a profit
        elif self.orders[0].otype == 'Ask':
            # Sold! put the money in the bank
            self.balance += transactionprice
            self.last_purchase_price = 0
            self.job = 'Buy'  # now go back and buy another one
        else:
            sys.exit('FATAL: PT2 doesn\'t know .otype %s\n' % self.orders[0].otype)

        if vrbs:
            net_worth = self.balance + self.last_purchase_price
            print('%s Balance=%d NetWorth=%d' % (outstr, self.balance, net_worth))

        self.del_order(order)  # delete the order

    # end of PT2 definition

# ########################---trader-types have all been defined now--################


# #########################---Below lies the experiment/test-rig---##################


def trade_stats(expid, traders, dumpfile, time, lob):
    """
    Dump CSV statistics on exchange data and trader population to file for later analysis.
    This makes no assumptions about the number of types of traders, or the number of traders of any one type
    -- allows either/both to change between successive calls, but that does make it inefficient as it has to
    re-analyse the entire set of traders on each call.
    :param expid: the experiment-I.D. character-string.
    :param traders: the list of traders in the market.
    :param dumpfile: the file that will be written to.
    :param time: the current time.
    :param lob: the current state of the LOB.
    :return: <nothing>
    """

    # Analyse the set of traders, to see what types we have
    trader_types = {}
    for t in traders:
        ttype = traders[t].ttype
        if ttype in trader_types.keys():
            t_balance = trader_types[ttype]['balance_sum'] + traders[t].balance
            n = trader_types[ttype]['n'] + 1
        else:
            t_balance = traders[t].balance
            n = 1
        trader_types[ttype] = {'n': n, 'balance_sum': t_balance}

    # first two columns of output are the session_id and the time
    dumpfile.write('%s, %06d, ' % (expid, time))

    # second two columns of output are the LOB best bid and best offer (or 'None' if they're undefined)
    if lob['bids']['best'] is not None:
        dumpfile.write('%d, ' % (lob['bids']['best']))
    else:
        dumpfile.write('None, ')
    if lob['asks']['best'] is not None:
        dumpfile.write('%d, ' % (lob['asks']['best']))
    else:
        dumpfile.write('None, ')

    # total remaining number of columns printed depends on number of different trader-types at this timestep
    # for each trader type we print FOUR columns...
    # TraderTypeCode, TotalProfitForThisTraderType, NumberOfTradersOfThisType, AverageProfitPerTraderOfThisType
    for ttype in sorted(list(trader_types.keys())):
        n = trader_types[ttype]['n']
        s = trader_types[ttype]['balance_sum']
        dumpfile.write('%s, %d, %d, %f, ' % (ttype, s, n, s / float(n)))

    dumpfile.write('\n')


def populate_market(trdrs_spec, traders, shuffle, vrbs):
    """
    Create a bunch of traders from traders-specification.
    Optionally shuffles the pack of buyers and the pack of sellers.
    :param trdrs_spec: the specification of the population of traders.
    :param traders: the list into which the newly-created traders traders will be written, as a return parameter
    :param shuffle: whether to shuffle the ordering of buyers/sellers within the respective list.
    :param vrbs: verbosity Boolean: if True, print a running commentary; if False, stay silent.
    :return: tuple (n_buyers, n_sellers)
    """
    # trdrs_spec is a list of buyer-specs and a list of seller-specs
    # each spec is (<trader type>, <number of this type of trader>, optionally: <params for this type of trader>)

    def trader_type(robottype, name, parameters):
        """
        Create a newly instantiated trader of the designated type.
        :param robottype: the 'ticker-symbol' abbreviation indicating what type of trader to create.
        :param name: this trader's trader-I.D. character string.
        :param parameters: a list of parameter values for this trader-type.
        :return: a newly created trader of the designated type.
        """
        balance = 0.00
        proptrader_balance = 500  # marketmakers start with zero inventory and a balance of $500
        time0 = 0
        if robottype == 'GVWY':
            return TraderGiveaway('GVWY', name, balance, parameters, time0)
        elif robottype == 'ZIC':
            return TraderZIC('ZIC', name, balance, parameters, time0)
        elif robottype == 'SHVR':
            return TraderShaver('SHVR', name, balance, parameters, time0)
        elif robottype == 'SNPR':
            return TraderSniper('SNPR', name, balance, parameters, time0)
        elif robottype == 'ZIP':
            return TraderZIP('ZIP', name, balance, parameters, time0)
        elif robottype == 'ZIPSH':
            return TraderZIP('ZIPSH', name, balance, parameters, time0)
        elif robottype == 'PRZI':
            return TraderPRZI('PRZI', name, balance, parameters, time0)
        elif robottype == 'PRSH':
            return TraderPRZI('PRSH', name, balance, parameters, time0)
        elif robottype == 'PRDE':
            return TraderPRZI('PRDE', name, balance, parameters, time0)
        elif robottype == 'PT1':
            return TraderPT1('PT1', name, proptrader_balance, parameters, time0)
        elif robottype == 'PT2':
            return TraderPT2('PT2', name, proptrader_balance, parameters, time0)
        else:
            sys.exit('FATAL: don\'t know trader type %s\n' % robottype)

    def shuffle_traders(ttype_char, n, trader_list):
        """
        Shuffles the trader-I.D. character strings of the traders in trader_list
        :param ttype_char: the lead character on the trader-I.D. strings (B for buyer, S for seller, etc)
        :param n: how many traders of this type
        :param trader_list: the list of traders in which the shuffling happens
        :return: <nothing>
        """
        for swap in range(n):
            t1 = (n - 1) - swap
            t2 = random.randint(0, t1)
            t1name = '%c%02d' % (ttype_char, t1)
            t2name = '%c%02d' % (ttype_char, t2)
            trader_list[t1name].tid = t2name
            trader_list[t2name].tid = t1name
            temp = traders[t1name]
            trader_list[t1name] = trader_list[t2name]
            trader_list[t2name] = temp

    def unpack_params(trader_params, mapping):
        """
        Unpack the parameters for those trader-types that have them
        :param trader_params: the paramaters being passed to this trader.
        :param mapping: Boolean flag: if True, enable fitness-landscape-mapping; otherwise do nothing for mapping.
        :return: the dictionary of parameters for this trader.
        """

        parameters = None

        if ttype == 'ZIPSH' or ttype == 'ZIP':
            # parameters matter...
            if mapping:
                parameters = 'landscape-mapper'
            elif trader_params is not None:
                parameters = trader_params.copy()
                # trader-type determines type of optimizer used
                if ttype == 'ZIPSH':
                    parameters['optimizer'] = 'ZIPSH'
                else:   # ttype=ZIP
                    parameters['optimizer'] = None
        if ttype == 'PRSH' or ttype == 'PRDE' or ttype == 'PRZI':
            # parameters matter...
            if mapping:
                parameters = 'landscape-mapper'
            elif trader_params is not None:
                # params determines type of optimizer used
                if ttype == 'PRSH':
                    parameters = {'optimizer': 'PRSH', 'k': trader_params['k'],
                                  'strat_min': trader_params['s_min'], 'strat_max': trader_params['s_max']}
                elif ttype == 'PRDE':
                    parameters = {'optimizer': 'PRDE', 'k': trader_params['k'],
                                  'strat_min': trader_params['s_min'], 'strat_max': trader_params['s_max']}
                else:   # ttype=PRZI
                    parameters = {'optimizer': None, 'k': 1,
                                  'strat_min': trader_params['s_min'], 'strat_max': trader_params['s_max']}
            else:
                sys.exit('FAIL: PRZI/PRSH/PRDE trader needs one or more parameters to be specified')
                
        # for PT1/PT2 the parameters are optional...
        # ...and are unpacked in __init__, so here they're just passed straight on through
        if ttype == 'PT1':
            parameters = trader_params
        if ttype == 'PT2':
            parameters = trader_params

        return parameters

    landscape_mapping = False   # set to true when mapping fitness landscape (for PRSH etc).

    # the code that follows is a bit of a kludge, needs tidying up.
    n_buyers = 0
    for bs in trdrs_spec['buyers']:
        ttype = bs[0]
        for b in range(bs[1]):
            tname = 'B%02d' % n_buyers  # buyer i.d. string
            if len(bs) > 2:
                # third part of the buyer-spec is params for this trader-type
                params = unpack_params(bs[2], landscape_mapping)
            else:
                params = unpack_params(None, landscape_mapping)
            traders[tname] = trader_type(ttype, tname, params)
            n_buyers = n_buyers + 1

    if n_buyers < 1:
        sys.exit('FATAL: no buyers specified\n')

    if shuffle:
        shuffle_traders('B', n_buyers, traders)

    n_sellers = 0
    for ss in trdrs_spec['sellers']:
        ttype = ss[0]
        for s in range(ss[1]):
            tname = 'S%02d' % n_sellers  # buyer i.d. string
            if len(ss) > 2:
                # third part of the buyer-spec is params for this trader-type
                params = unpack_params(ss[2], landscape_mapping)
            else:
                params = unpack_params(None, landscape_mapping)
            traders[tname] = trader_type(ttype, tname, params)
            n_sellers = n_sellers + 1

    if n_sellers < 1:
        sys.exit('FATAL: no sellers specified\n')

    if shuffle:
        shuffle_traders('S', n_sellers, traders)

    n_proptraders = 0
    if 'proptraders' in trdrs_spec and len(trdrs_spec['proptraders']) > 0:
        for pts in trdrs_spec['proptraders']:
            ttype = pts[0]
            for pt in range(pts[1]):
                tname = 'P%02d' % n_proptraders  # proptrader i.d. string
                if len(pts) > 2:
                    # third part of the buyer-spec is params for this trader-type
                    params = unpack_params(pts[2], landscape_mapping)
                else:
                    params = unpack_params(None, landscape_mapping)
                traders[tname] = trader_type(ttype, tname, params)
                n_proptraders = n_proptraders + 1

    # NB markets with zero proptraders don't cause a fatal error

    if n_proptraders > 0 and shuffle:
        shuffle_traders('P', n_proptraders, traders)

    if vrbs:
        for t in range(n_buyers):
            tname = 'B%02d' % t
            print(traders[tname])
        for t in range(n_sellers):
            tname = 'S%02d' % t
            print(traders[tname])
        for t in range(n_proptraders):
            tname = 'P%02d' % t
            print(traders[tname])

    return {'n_buyers': n_buyers, 'n_sellers': n_sellers, 'n_proptraders': n_proptraders}


def customer_orders(time, traders, trader_stats, orders_sched, pending, vrbs):
    """
    Generate a list of new customer-orders to be issued to the traders in the immediate/near future,
    and a list of any existing customer-orders that need to be cancelled because they are overridden by new ones.
    :param time: the current time.
    :param traders: the population of traders.
    :param trader_stats: summary statistics about the population of traders.
    :param orders_sched: the supply/demand schedule from which the orders will be generated...
            os['timemode'] is either 'periodic', 'drip-fixed', 'drip-jitter', or 'drip-poisson';
            os['interval'] is number of seconds for a full cycle of replenishment;
            drip-poisson sequences will be normalised to ensure time of last replenishment <= interval.
            If a supply or demand schedule mode is "random" and more than one range is supplied in ranges[],
            then each time a price is generated one of the ranges is chosen equiprobably and the price is
            then generated uniform-randomly from that range.
            if len(range)==2, interpreted as min and max values on the schedule, specifying linear supply/demand curve.
            if len(range)==3, first two vals are min & max for linear sup/dem curves, and third value should be a
            callable function that generates a dynamic price offset; he offset value applies equally to the min & max,
            so gradient of linear sup/dem curves doesn't vary, but equilibrium price does.
            if len(range)==4, the third value is function that gives dynamic offset for schedule min, and 4th is a
            function giving dynamic offset for schedule max, so gradient of sup/dem linear curve can vary dynamically
            along with the varying equilibrium price.
    :param pending: the list of currently pending future orders if this is empty, generates a new one).
    :param vrbs: verbosity Boolean: if True, print a running commentary; if False, stay silent.
    :return: [new_pending, cancellations]:
            new_pending is list of new orders to be issued;
            cancellations is list of previously-issued orders now cancelled.
    """

    def sysmin_check(price):
        """ if price is less than system minimum price, issue a warning and clip the price to the minimum"""
        if price < bse_sys_minprice:
            print('WARNING: price < bse_sys_min -- clipped')
            price = bse_sys_minprice
        return price

    def sysmax_check(price):
        """ if price is greater than system maximum price, issue a warning and clip the price to the maximum"""
        if price > bse_sys_maxprice:
            print('WARNING: price > bse_sys_max -- clipped')
            price = bse_sys_maxprice
        return price

    def getorderprice(i, schedules, n, stepmode, orderissuetime):
        """
        Generate a price for an order, using the given supply/demand schedule, and specified step-mode.
        :param i: index of trader (position in list of traders).
        :param schedules: the supply/demand schedules.
        :param n: the number of traders that this schedule sup/dem is being applied to.
        :param stepmode: what type of steps to have between successive prices on the sup/dem schedule.
                stepmode=='fixed' => all steps are equal at one fixed size -- a "uniform-step" (see "jittered", below);
                stepmode=='jittered' => all steps are random, constrained to be within 2 uniform-steps of each other;
                stepmode=='random' => all steps are generated from a uniform distribution.
        :param orderissuetime: the time that this order will be issued at.
        :return: the price.
        """

        # does the first schedule range include optional dynamic offset function(s)?
        if len(schedules[0]) > 2:
            offsetfn = schedules[0][2]
            if callable(offsetfn[0]):
                # same offset for min and max
                offset_min = offsetfn[0](orderissuetime, *offsetfn[1])
                offset_max = offset_min
            else:
                sys.exit('FAIL: 3rd argument of sched in getorderprice() not callable')
            if len(schedules[0]) > 3:
                # if second offset function is specified, that applies only to the max value
                offsetfn = schedules[0][3]
                if callable(offsetfn):
                    # this function applies to max
                    offset_max = offsetfn(orderissuetime)
                else:
                    sys.exit('FAIL: 4th argument of sched in getorderprice() not callable')
        else:
            offset_min = 0.0
            offset_max = 0.0

        pmin = sysmin_check(offset_min + min(schedules[0][0], schedules[0][1]))
        pmax = sysmax_check(offset_max + max(schedules[0][0], schedules[0][1]))
        prange = pmax - pmin
        stepsize = prange / (n - 1)
        halfstep = round(stepsize / 2.0)

        if stepmode == 'fixed':
            order_price = pmin + int(i * stepsize)
        elif stepmode == 'jittered':
            order_price = pmin + int(i * stepsize) + random.randint(-halfstep, halfstep)
        elif stepmode == 'random':
            if len(schedules) > 1:
                # more than one schedule: choose one equiprobably
                s = random.randint(0, len(schedules) - 1)
                pmin = sysmin_check(min(schedules[s][0], schedules[s][1]))
                pmax = sysmax_check(max(schedules[s][0], schedules[s][1]))
            order_price = random.randint(int(pmin), int(pmax))
        else:
            sys.exit('FAIL: Unknown mode in schedule')
        order_price = sysmin_check(sysmax_check(order_price))
        return order_price

    def getissuetimes(n_traders, timemode, interval, shuffle, fittointerval):
        """
        Generate a list of issue/arrival times for a set of future customer-orders, over a specified time-interval.
        :param n_traders: how many traders need issue times (i.e., the number of customer orders to be generated)
        :param timemode: character-string specifying the temporal spacing of orders:
                timemode=='periodic'=> orders issued to all traders at the same instant in time, every time-interval;
                timemode=='drip-fixed'=> order interarrival time is exactly one timestep, for all orders;
                timemode=='drip-jitter'=> order interarrival time is (1+r)*timestep, r=U[0,timestep], for all orders;
                timemode=='drip-poisson'=> order interarrival time is a Poisson random process, for all orders.
        :param interval: the time-interval between successive order issuals/arrivals.
        :param shuffle: if True then shuffle the arrival times, randomising the sequence in which traders get orders.
        :param fittointerval: if True then final order arrives at exactly t+interval; else may be slightly later.
        :return: the list of issue times.
        """
        interval = float(interval)
        if n_traders < 1:
            sys.exit('FAIL: n_traders < 1 in getissuetime()')
        elif n_traders == 1:
            tstep = interval
        else:
            tstep = interval / (n_traders - 1)
        arrtime = 0
        issue_times = []
        for trdr in range(n_traders):
            if timemode == 'periodic':
                arrtime = interval
            elif timemode == 'drip-fixed':
                arrtime = trdr * tstep
            elif timemode == 'drip-jitter':
                arrtime = trdr * tstep + tstep * random.random()
            elif timemode == 'drip-poisson':
                # poisson requires a bit of extra work
                interarrivaltime = random.expovariate(n_traders / interval)
                arrtime += interarrivaltime
            else:
                sys.exit('FAIL: unknown time-mode in getissuetimes()')
            issue_times.append(arrtime)
            # at this point, arrtime is the last arrival time

        if fittointerval and ((arrtime > interval) or (arrtime < interval)):
            # generated sum of interarrival times longer than the interval
            # squish them back so that last arrival falls at t=interval
            for trdr in range(n_traders):
                issue_times[trdr] = interval * (issue_times[trdr] / arrtime)
        # optionally randomly shuffle the times
        if shuffle:
            for trdr in range(n_traders):
                i = (n_traders - 1) - trdr
                j = random.randint(0, i)
                tmp = issue_times[i]
                issue_times[i] = issue_times[j]
                issue_times[j] = tmp
        return issue_times

    def getschedmode(t_now, order_schedules):
        """
        return the step-mode for supply/demand schedule at the current time
        :param t_now: the current time
        :param order_schedules: dictionary/list of order schedules
        :return: schedrange = the price range for this schedule; mode= the stepmode for this schedule
        """
        got_one = False
        schedrange = None
        stepmode = None
        for schedule in order_schedules:
            if (schedule['from'] <= t_now) and (t_now < schedule['to']):
                # within the timezone for this schedule
                schedrange = schedule['ranges']
                stepmode = schedule['stepmode']
                got_one = True
                break  # jump out the loop -- so the first matching timezone has priority over any others
        if not got_one:
            sys.exit('Fail: time=%5.2f not within any timezone in order_schedules=%s' % (t_now, order_schedules))
        return schedrange, stepmode

    n_buyers = trader_stats['n_buyers']
    n_sellers = trader_stats['n_sellers']

    shuffle_times = True

    cancellations = []

    if len(pending) < 1:
        # list of pending (to-be-issued) customer orders is empty, so generate a new one
        new_pending = []

        # demand side (buyers)
        issuetimes = getissuetimes(n_buyers, orders_sched['timemode'], orders_sched['interval'], shuffle_times, True)

        ordertype = 'Bid'
        (sched, mode) = getschedmode(time, orders_sched['dem'])
        for t in range(n_buyers):
            issuetime = time + issuetimes[t]
            tname = 'B%02d' % t
            orderprice = getorderprice(t, sched, n_buyers, mode, issuetime)
            order = Order(tname, ordertype, orderprice, 1, issuetime, chrono.time())
            new_pending.append(order)

        # supply side (sellers)
        issuetimes = getissuetimes(n_sellers, orders_sched['timemode'], orders_sched['interval'], shuffle_times, True)
        ordertype = 'Ask'
        (sched, mode) = getschedmode(time, orders_sched['sup'])
        for t in range(n_sellers):
            issuetime = time + issuetimes[t]
            tname = 'S%02d' % t
            orderprice = getorderprice(t, sched, n_sellers, mode, issuetime)
            # print('time %d sellerprice %d' % (time,orderprice))
            order = Order(tname, ordertype, orderprice, 1, issuetime, chrono.time())
            new_pending.append(order)
    else:
        # there are pending future orders: issue any whose timestamp is in the past
        new_pending = []
        for order in pending:
            if order.time < time:
                # this order should have been issued by now
                # issue it to the trader
                tname = order.tid
                response = traders[tname].add_order(order, vrbs)
                if vrbs:
                    print('Customer order: %s %s' % (response, order))
                if response == 'LOB_Cancel':
                    cancellations.append(tname)
                    if vrbs:
                        print('Cancellations: %s' % cancellations)
                # and then don't add it to new_pending (i.e., delete it)
            else:
                # this order stays on the pending list
                new_pending.append(order)
    return [new_pending, cancellations]


def market_session(sess_id, starttime, endtime, trader_spec, order_schedule, dumpfile_flags, sess_vrbs):
    """
    One session in the market.
    :param sess_id: the character-string ID for this session, used in naming output files.
    :param starttime: the time the session starts.
    :param endtime: the time the sessiom ends.
    :param trader_spec: specification of the traders populating the market for this session.
    :param order_schedule: specification of the "customer orders" assigned to traders, i.e. the supply/demand schedule.
    :param dumpfile_flags: a dictionary of Boolean flags specifying which output files to be written for this session.
    :param sess_vrbs: verbosity: if True, output a running commentary on what is going on; if False, stay silent.
    :return: <nothing>.
    """

    def dump_strats_frame(frametime, stratfile, trdrs):
        """
        Write one frame of strategy snapshot
        :param frametime: the time that the frame snapshot is printed.
        :param stratfile:  the file to write to.
        :param trdrs: the population of traders.
        :return: <nothing>
        """

        line_str = 't=,%.0f, ' % frametime

        best_buyer_id = None
        best_buyer_prof = 0
        best_buyer_strat = None
        best_seller_id = None
        best_seller_prof = 0
        best_seller_strat = None

        # loop through traders to find the best
        for trdr in traders:
            trader = trdrs[trdr]

            # print('PRSH/PRDE/ZIPSH strategy recording, t=%s' % trader)
            if trader.ttype == 'PRSH' or trader.ttype == 'PRDE' or trader.ttype == 'ZIPSH':
                line_str += 'id=,%s, %s,' % (trader.tid, trader.ttype)

                if trader.ttype == 'ZIPSH':
                    # we know that ZIPSH sorts the set of strats into best-first
                    act_strat = trader.strats[0]['stratvec']
                    act_prof = trader.strats[0]['pps']
                else:
                    act_strat = trader.strats[trader.active_strat]['stratval']
                    act_prof = trader.strats[trader.active_strat]['pps']

                line_str += 'actvstrat=,%s ' % trader.strat_csv_str(act_strat)
                line_str += 'actvprof=,%f, ' % act_prof

                if trader.tid[:1] == 'B':
                    # this trader is a buyer
                    if best_buyer_id is None or act_prof > best_buyer_prof:
                        best_buyer_id = trader.tid
                        best_buyer_strat = act_strat
                        best_buyer_prof = act_prof
                elif trader.tid[:1] == 'S':
                    # this trader is a seller
                    if best_seller_id is None or act_prof > best_seller_prof:
                        best_seller_id = trader.tid
                        best_seller_strat = act_strat
                        best_seller_prof = act_prof
                else:
                    # wtf?
                    sys.exit('unknown trader id type in market_session')

        if best_buyer_id is not None:
            line_str += 'best_B_id=,%s, best_B_prof=,%f, best_B_strat=, ' % (best_buyer_id, best_buyer_prof)
            line_str += traders[best_buyer_id].strat_csv_str(best_buyer_strat)

        if best_seller_id is not None:
            line_str += 'best_S_id=,%s, best_S_prof=,%f, best_S_strat=, ' % (best_seller_id, best_seller_prof)
            line_str += traders[best_seller_id].strat_csv_str(best_seller_strat)

        line_str += '\n'

        if verbose:
            print('line_str: %s' % line_str)
        stratfile.write(line_str)
        stratfile.flush()
        os.fsync(stratfile)

    def blotter_dump(session_id, trdrs):
        """
        Write the blotter for each trader.
        :param session_id: this market session's ID string (used for the filename).
        :param trdrs: the population of traders.
        :return: <nothing>
        """
        bdump = open(session_id+'_blotters.csv', 'w')
        for trdr in trdrs:
            bdump.write('%s, %d\n' % (trdrs[trdr].tid, len(trdrs[trdr].blotter)))
            for b in trdrs[trdr].blotter:
                bdump.write('%s, %s, %.3f, %d, %s, %s, %d\n'
                            % (traders[trdr].tid, b['type'], b['time'], b['price'], b['party1'], b['party2'], b['qty']))
        bdump.close()

    orders_verbose = False
    lob_verbose = False
    process_verbose = False
    respond_verbose = False
    bookkeep_verbose = False
    populate_verbose = False

    if dumpfile_flags['dump_strats']:
        strat_dump = open(sess_id + '_strats.csv', 'w')
    else:
        strat_dump = None

    if dumpfile_flags['dump_lobs']:
        lobframes = open(sess_id + '_LOB_frames.csv', 'w')
    else:
        lobframes = None

    if dumpfile_flags['dump_avgbals']:
        avg_bals = open(sess_id + '_avg_balance.csv', 'w')
    else:
        avg_bals = None
        
    if dumpfile_flags['dump_tape']:
        # NB writing transactions only -- not writing cancellations
        tape_dump = open(sess_id + '_tape.csv', 'w')
    else:
        tape_dump = None
        
    # initialise the exchange
    exchange = Exchange()

    # create a bunch of traders
    traders = {}
    trader_stats = populate_market(trader_spec, traders, True, populate_verbose)

    # timestep set so that can process all traders in one second
    # NB minimum interarrival time of customer orders may be much less than this!!
    timestep = 1.0 / float(trader_stats['n_buyers'] + trader_stats['n_sellers'] + trader_stats['n_proptraders'])

    session_duration = float(endtime - starttime)

    time = starttime

    pending_cust_orders = []

    if sess_vrbs:
        print('\n%s;  ' % sess_id)

    # frames_done is record of what frames we have printed data for thus far
    frames_done = set()

    while time < endtime:

        # how much time left, as a percentage?
        time_left = (endtime - time) / session_duration

        if sess_vrbs:
            print('\n\n%s; t=%08.2f (%4.1f/100) ' % (sess_id, time, time_left*100))

        [pending_cust_orders, kills] = customer_orders(time, traders, trader_stats,
                                                       order_schedule, pending_cust_orders, orders_verbose)

        # if any newly-issued customer orders mean quotes on the LOB need to be cancelled, kill them
        if len(kills) > 0:
            # if verbose : print('Kills: %s' % (kills))
            for kill in kills:
                # if verbose : print('lastquote=%s' % traders[kill].lastquote)
                if traders[kill].lastquote is not None:
                    # if verbose : print('Killing order %s' % (str(traders[kill].lastquote)))
                    # NB if exchange.del_order() third argument = None then cancellations not written to tape file.
                    # exchange.del_order(time, traders[kill].lastquote, tape_dump, sess_vrbs)
                    exchange.del_order(time, traders[kill].lastquote, None, sess_vrbs)

        # get a limit-order quote (or None) from a randomly chosen trader
        tid = list(traders.keys())[random.randint(0, len(traders) - 1)]

        order = traders[tid].getorder(time, time_left, exchange.publish_lob(time, lobframes, lob_verbose))
        if sess_vrbs:
            print('trader=%s order=%s' % (tid, order))

        if order is not None:
            if order.otype == 'Ask' and order.price < traders[tid].orders[0].price:
                sys.exit('Bad ask')
            if order.otype == 'Bid' and order.price > traders[tid].orders[0].price:
                sys.exit('Bad bid')
            # send order to exchange
            traders[tid].n_quotes = 1
            trade = exchange.process_order(time, order, tape_dump, process_verbose)
            if trade is not None:
                # trade occurred,
                # so the counterparties update order lists and blotters
                traders[trade['party1']].bookkeep(time, trade, order, bookkeep_verbose)
                traders[trade['party2']].bookkeep(time, trade, order, bookkeep_verbose)
                if dumpfile_flags['dump_avgbals']:
                    trade_stats(sess_id, traders, avg_bals, time, exchange.publish_lob(time, lobframes, lob_verbose))

            # traders respond to whatever happened
            lob = exchange.publish_lob(time, lobframes, lob_verbose)
            any_record_frame = False
            for t in traders:
                # NB respond just updates trader's internal variables
                # doesn't alter the LOB, so processing each trader in
                # sequence (rather than random/shuffle) isn't a problem
                record_frame = traders[t].respond(time, lob, trade, respond_verbose)
                if record_frame:
                    any_record_frame = True

            # log all the PRSH/PRDE/ZIPSH strategy info for this timestep?
            if any_record_frame and dumpfile_flags['dump_strats']:
                # print one more frame to strategy dumpfile
                dump_strats_frame(time, strat_dump, traders)
                # record that we've written this frame
                frames_done.add(int(time))

        time = time + timestep

    # session has ended

    # write trade_stats for this session (NB could use this to write end-of-session summary only)
    if dumpfile_flags['dump_avgbals']:
        trade_stats(sess_id, traders, avg_bals, time, exchange.publish_lob(time, lobframes, lob_verbose))
        avg_bals.close()

    if dumpfile_flags['dump_blotters']:
        # record the blotter for each trader
        blotter_dump(sess_id, traders)

    if dumpfile_flags['dump_strats']:
        strat_dump.close()

    if dumpfile_flags['dump_lobs']:
        lobframes.close()


#############################
# # Below here is where we set up and run a whole series of experiments

if __name__ == "__main__":

    price_offset_filename = 'offset_BTC_USD_20250211.csv'

    # if called from the command line with one argument, the first argument is the price offset filename
    if len(sys.argv) > 1:
        price_offset_filename = sys.argv[1]

    # set up common parameters for all market sessions
    # 1000 days is often good, but 3*365=1095, so may as well go for three years.
    n_days = 1
    hours_in_a_day = 24     # how many hours the exchange operates for in a working day (e.g. NYSE = 7.5)
    start_time = 0.0
    end_time = 60.0 * 60.0 * hours_in_a_day * n_days
    duration = end_time - start_time


    def schedule_offsetfn_read_file(filename, col_t, col_p, scale_factor=75):
        """
        Read in a CSV data-file for the supply/demand schedule time-varying price-offset value
        :param filename: the CSV file to read
        :param col_t: column in the CSV that has the time data
        :param col_p: column in the CSV that has the price data
        :param scale_factor: multiplier on prices
        :return: on offset value event-list: one item for each change in offset value
                -- each item is percentage time elapsed, followed by the new offset value at that time
        """
        
        vrbs = True
        
        # does two passes through the file
        # assumes data file is all for one date, sorted in time order, in correct format, etc. etc.
        rwd_csv = csv.reader(open(filename, 'r'))
        
        # first pass: get time & price events, find out how long session is, get min & max price
        minprice = None
        maxprice = None
        firsttimeobj = None
        timesincestart = 0
        priceevents = []
        
        first_row_is_header = True
        this_is_first_row = True
        this_is_first_data_row = True
        first_date = None
        
        for line in rwd_csv:
            
            if vrbs:
                print(line)
            
            if this_is_first_row and first_row_is_header:
                this_is_first_row = False
                this_is_first_data_row = True
                continue
                
            row_date = line[col_t][:10]
            
            if this_is_first_data_row:
                first_date = row_date
                this_is_first_data_row = False
                
            if row_date != first_date:
                continue
                
            time = line[col_t][11:19]
            if firsttimeobj is None:
                firsttimeobj = datetime.strptime(time, '%H:%M:%S')
                
            timeobj = datetime.strptime(time, '%H:%M:%S')
            
            price_str = line[col_p]
            # delete any commas so 1,000,000 becomes 1000000
            price_str_no_commas = price_str.replace(',', '')
            price = float(price_str_no_commas)
            
            if minprice is None or price < minprice:
                minprice = price
            if maxprice is None or price > maxprice:
                maxprice = price
            timesincestart = (timeobj - firsttimeobj).total_seconds()
            priceevents.append([timesincestart, price])
            
            if vrbs:
                print(row_date, time, timesincestart, price)
            
        # second pass: normalise times to fractions of entire time-series duration
        #              & normalise price range
        pricerange = maxprice - minprice
        endtime = float(timesincestart)
        offsetfn_eventlist = []
        for event in priceevents:
            # normalise price
            normld_price = (event[1] - minprice) / pricerange
            # clip
            normld_price = min(normld_price, 1.0)
            normld_price = max(0.0, normld_price)
            # scale & convert to integer cents
            price = int(round(normld_price * scale_factor))
            normld_event = [event[0] / endtime, price]
            if vrbs:
                print(normld_event)
            offsetfn_eventlist.append(normld_event)
        
        return offsetfn_eventlist


    def schedule_offsetfn_from_eventlist(time, params):
        """
        Returns a price offset-value for the current time, by reading from an offset event-list.
        :param time: the current time
        :param params: a list of parameter values...
            params[1] is the final time (the end-time) of the current session.
            params[2] is the offset event-list: one item for each change in offset value
                        -- each item is percentage time elapsed, followed by the new offset value at that time
        :return: integer price offset value
        """

        final_time = float(params[0])
        offset_events = params[1]
        # this is quite inefficient: on every call it walks the event-list
        percent_elapsed = time/final_time
        offset = None
        for event in offset_events:
            offset = event[1]
            if percent_elapsed < event[0]:
                break
        return offset


    def schedule_offsetfn_increasing_sinusoid(t, params):
        """
        Returns sinusoidal time-dependent price-offset, steadily increasing in frequency & amplitude
        :param t: time
        :param params: set of parameters for the offsetfn: this is empty-set for this offsetfn but nonempty in others
        :return: the time-dependent price offset at time t
        """
        if params is None:  # this test of params is here only to prevent PyCharm from warning about unused parameters
            pass
        scale = -7500
        multiplier = 7500000    # determines rate of increase of frequency and amplitude
        offset = ((scale * t) / multiplier) * (1 + math.sin((t*t)/(multiplier * math.pi)))
        return int(round(offset, 0))

    # Here is an example of how to use the offset function
    #
    # range1 = (10, 190, (schedule_offsetfn, args)) # args is the list of arguments to the function
    # range2 = (200, 300, (schedule_offsetfn, args))

    # Here is an example of how to switch from range1 to range2 and then back to range1,
    # introducing two "market shocks"
    # -- here the timings of the shocks are at 1/3 and 2/3 into the duration of the session.
    #
    # supply_schedule = [ {'from':start_time, 'to':duration/3, 'ranges':[range1], 'stepmode':'fixed'},
    #                     {'from':duration/3, 'to':2*duration/3, 'ranges':[range2], 'stepmode':'fixed'},
    #                     {'from':2*duration/3, 'to':end_time, 'ranges':[range1], 'stepmode':'fixed'}
    #                   ]

    offsetfn_events = None
    if price_offset_filename is not None:
        offsetfn_events = schedule_offsetfn_read_file(price_offset_filename, 0, 1)

    # supply schedule (defines the supply curve)
    range1 = (75, 110, (schedule_offsetfn_from_eventlist, [[end_time, offsetfn_events]]))
    supply_schedule = [{'from': start_time, 'to': end_time, 'ranges': [range1], 'stepmode': 'random'}]

    # demand schedule (defines the demand curve)
    range2 = (125, 90, (schedule_offsetfn_from_eventlist, [[end_time, offsetfn_events]]))
    demand_schedule = [{'from': start_time, 'to': end_time, 'ranges': [range2], 'stepmode': 'random'}]

    # new customer orders arrive at each trader approx once every order_interval seconds
    order_interval = 10

    # order schedule wraps up the supply/demand schedules and details of how customer orders/assignments are issued
    order_sched = {'sup': supply_schedule, 'dem': demand_schedule,
                   'interval': order_interval, 'timemode': 'drip-poisson'}

    # now run a sequence of trials, one session per trial

    # if verbose = True, print a running commentary describing what's going on.
    verbose = False

    # n_trials is how many trials (i.e. market sessions) to run in total
    n_trials = 1

    # n_recorded is how many trials (i.e. market sessions) to write full data-files for
    n_trials_recorded = 5

    trial = 1

    while trial < (n_trials+1):

        # create unique i.d. string for this trial
        trial_id = 'bse_d%03d_i%02d_%04d' % (n_days, order_interval, trial)

        # buyer_spec specifies the strategies played by buyers, and for each strategy how many such buyers to create
        buyers_spec = [('SHVR', 5), ('GVWY', 5), ('ZIC', 2), ('ZIP', 13)]
        #     ('PRZI', 5, {'s_min': -1.0, 's_max': +1.0})]

        # seller_spec specifies the strategies played by sellers, and for each strategy how many such sellers to create
        sellers_spec = buyers_spec

        # proptraders_spec specifies strategies played by proprietary-traders, and how many of each
        proptraders_spec = [('PT1', 1, {'bid_percent': 0.95, 'ask_delta': 7}), ('PT2', 1, {'n_past_trades': 25})]

        # trader_spec wraps up the specifications for the buyers, sellers, and proptraders
        traders_spec = {'sellers': sellers_spec, 'buyers': buyers_spec, 'proptraders': proptraders_spec}

        if trial > n_trials_recorded:
            # switch off recording of detailed data-files
            dump_flags = {'dump_blotters': False, 'dump_lobs': False, 'dump_strats': False,
                          'dump_avgbals': False, 'dump_tape': False}
        else:
            # we're still recording all the required data-files
            dump_flags = {'dump_blotters': True, 'dump_lobs': False, 'dump_strats': True,
                          'dump_avgbals': True, 'dump_tape': True}

        # simulate the market session
        market_session(trial_id, start_time, end_time, traders_spec, order_sched, dump_flags, verbose)

        trial = trial + 1

    # The code in comments below here is for illustration, in case you want to do an exhaustive sweep of all possible
    # combinations of some set of trading strategies: if its of no interest, it can be deleted.
    #
    # run a sequence of trials that exhaustively varies the ratio of four trader types
    # NB this has weakness of symmetric proportions on buyers/sellers -- combinatorics of varying that are quite nasty
    #
    # n_trader_types = 4
    # equal_ratio_n = 4
    # n_trials_per_ratio = 50
    #
    # n_traders = n_trader_types * equal_ratio_n
    #
    # fname = 'balances_%03d.csv' % equal_ratio_n
    #
    # tdump = open(fname, 'w')
    #
    # min_n = 1
    #
    # trialnumber = 1
    # trdr_1_n = min_n
    # while trdr_1_n <= n_traders:
    #     trdr_2_n = min_n
    #     while trdr_2_n <= n_traders - trdr_1_n:
    #         trdr_3_n = min_n
    #         while trdr_3_n <= n_traders - (trdr_1_n + trdr_2_n):
    #             trdr_4_n = n_traders - (trdr_1_n + trdr_2_n + trdr_3_n)
    #             if trdr_4_n >= min_n:
    #                 buyers_spec = [('GVWY', trdr_1_n), ('SHVR', trdr_2_n),
    #                                ('ZIC', trdr_3_n), ('ZIP', trdr_4_n)]
    #                 sellers_spec = buyers_spec
    #                 traders_spec = {'sellers': sellers_spec, 'buyers': buyers_spec}
    #                 # print buyers_spec
    #                 trial = 1
    #                 while trial <= n_trials_per_ratio:
    #                     trial_id = 'trial%07d' % trialnumber
    #                     market_session(trial_id, start_time, end_time, traders_spec,
    #                                    order_sched, tdump, False, True)
    #                     tdump.flush()
    #                     trial = trial + 1
    #                     trialnumber = trialnumber + 1
    #             trdr_3_n += 1
    #         trdr_2_n += 1
    #     trdr_1_n += 1
    # tdump.close()
    #
    # print(trialnumber)


================================================
FILE: BSE_VernonSmith1962_demo.ipynb
================================================
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    },
    "tags": []
   },
   "source": [
    "# Simple BSE demo/walkthrough\n",
    "Dave Cliff, University of Bristol, October 2022\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## BSE System Architecture\n",
    "\n",
    "The figure below shows a schematic illustration of the overall architecture of the *Bristol Stock Exchange* (BSE), a simulation of a contemporary fully electronic financial exchange with automated traders."
   ]
  },
  {
   "attachments": {
    "8626ec3c-5c9c-4ae1-821a-31ee98123508.png": {
     "image/png": "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\nHm+Ton0A7OioGFP98ETvPm5Y5Cum2FxTA73+RrMwD/zDJvv67/b9zxkoVP82/wtGFg4dGvBaHgAA\nAFZlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA5KGAAcAAAASAAAARKACAAQAAAABAAAC\nnKADAAQAAAABAAACgAAAAABBU0NJSQAAAFNjcmVlbnNob3RF2l2VAAAB1mlUWHRYTUw6Y29tLmFk\nb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0i\nWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3Jn\nLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjph\nYm91dD0iIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYv\nMS4wLyI+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj42NDA8L2V4aWY6UGl4ZWxZRGlt\nZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NjY4PC9leGlmOlBpeGVsWERp\nbWVuc2lvbj4KICAgICAgICAgPGV4aWY6VXNlckNvbW1lbnQ+U2NyZWVuc2hvdDwvZXhpZjpVc2Vy\nQ29tbWVudD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1l\ndGE+CsCaQrwAAEAASURBVHgB7L0HYF7VfTb+vEOvtiwP2fK2sQ02YDYESCgQQiAkbSgZbVZJ06Rp\nku6m/bf9Jx3p/NKvX/q1SdOZNINmkCZpRgkQEkLY4IABgwfe25Zkbend3/P8zr3SKyFbNshGkn9H\neu8994zfOee555773N9ZiTIN3DgCjoAj4Ag4Ao6AI+AIOAInCYHkSZLrYh0BR8ARcAQcAUfAEXAE\nHAFDwAmnVwRHwBFwBBwBR8ARcAQcgZOKgBPOkwqvC3cEHAFHwBFwBBwBR8ARcMLpdcARcAQcAUfA\nEXAEHAFH4KQi4ITzpMLrwh0BR8ARcAQcAUfAEXAEnHB6HXAEHAFHwBFwBBwBR8AROKkIOOE8qfC6\n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
Download .txt
gitextract_ke5l3892/

├── .gitignore
├── .idea/
│   ├── .name
│   ├── BristolStockExchange.iml
│   ├── inspectionProfiles/
│   │   └── profiles_settings.xml
│   ├── misc.xml
│   ├── modules.xml
│   ├── vcs.xml
│   └── workspace.xml
├── BSE.py
├── BSE_VernonSmith1962_demo.ipynb
├── LICENSE.md
├── Offsets_BTC_USD/
│   └── empty_file
├── README.md
├── Trader_AA.py
├── ZhenZhang/
│   ├── README.md
│   └── source/
│       ├── BSE2.py
│       ├── BSE2_msg_classes.py
│       ├── BSE_trader_agents.py
│       ├── GDX.py
│       ├── IAA_MLOFI.py
│       ├── IAA_NEW.py
│       ├── IGDX_MLOFI.py
│       ├── IZIP_MLOFI.py
│       ├── Simple_MLOFI.py
│       ├── ZZISHV.py
│       └── dataAnalysis/
│           ├── box_analysis.py
│           ├── hypothesisTest.py
│           └── matplotlib4.py
├── clean.sh
├── snashall2019.py
└── wiki_images/
    └── TIFFs/
        ├── 2shockGVWY.tiff
        ├── 2shockSHVR.tiff
        ├── 2shockZIC.tiff
        ├── 2shockZIP.tiff
        ├── TwoShock4Up.tiff
        ├── fig4.1.tiff
        ├── fig4.2.tiff
        ├── fig4.3.tiff
        ├── fig4.4.tiff
        ├── fig4.5.tiff
        ├── fig4.6.tiff
        ├── fig4.7.tiff
        ├── fig5.1.tiff
        ├── fig5.2.tiff
        ├── gvwy_profit.tiff
        ├── gvwy_trans.tiff
        ├── shvr_profit.tiff
        ├── shvr_trans.tiff
        ├── sinetest.tiff
        ├── sinusoid_trans.tiff
        ├── snip4.1.tiff
        ├── snip4.2.tiff
        ├── snip5.1.tiff
        ├── snip5.2.tiff
        ├── snipBSEcode01.tiff
        ├── snip_ZIC.tiff
        ├── snip_bigtest.tiff
        ├── snip_gvwy.tiff
        ├── snip_lob01.tiff
        ├── snip_lob02.tiff
        ├── snip_lob03.tiff
        ├── snip_lob04.tiff
        ├── snip_shvr.tiff
        ├── snip_snpr.tiff
        ├── snip_zic_targetupdown.tiff
        ├── snip_zip_getorder.tiff
        ├── snip_zip_lobprocessor.tiff
        ├── snip_zip_profit_alter.tiff
        ├── snip_zip_willingtotrade.tiff
        ├── snip_zip_workbid.tiff
        ├── snpr_profit.tiff
        ├── snpr_trans.tiff
        ├── zic_profit.tiff
        ├── zic_trans.tiff
        ├── zip_profit.tiff
        └── zip_trans.tiff
Download .txt
SYMBOL INDEX (340 symbols across 13 files)

FILE: BSE.py
  class Order (line 69) | class Order:
    method __init__ (line 76) | def __init__(self, tid, otype, price, qty, time, qid):
    method __str__ (line 84) | def __str__(self):
  class OrderbookHalf (line 89) | class OrderbookHalf:
    method __init__ (line 95) | def __init__(self, booktype, worstprice):
    method anonymize_lob (line 117) | def anonymize_lob(self):
    method build_lob (line 128) | def build_lob(self):
    method book_add (line 165) | def book_add(self, order):
    method book_del (line 191) | def book_del(self, order):
    method delete_best (line 205) | def delete_best(self):
  class Orderbook (line 240) | class Orderbook(OrderbookHalf):
    method __init__ (line 243) | def __init__(self):
  class Exchange (line 254) | class Exchange(Orderbook):
    method add_order (line 257) | def add_order(self, order, vrbs):
    method del_order (line 281) | def del_order(self, time, order, tape_file, vrbs):
    method process_order (line 329) | def process_order(self, time, order, tape_file, vrbs):
    method tape_dump (line 407) | def tape_dump(self, fname, fmode, tmode):
    method publish_lob (line 424) | def publish_lob(self, time, lob_file, vrbs):
  class Trader (line 492) | class Trader:
    method __init__ (line 495) | def __init__(self, ttype, tid, balance, params, time):
    method __str__ (line 519) | def __str__(self):
    method add_order (line 524) | def add_order(self, order, vrbs):
    method del_order (line 544) | def del_order(self, order):
    method profitpertime_update (line 551) | def profitpertime_update(self, time, birthtime, totalprofit):
    method bookkeep (line 569) | def bookkeep(self, time, trade, order, vrbs):
    method respond (line 613) | def respond(self, time, lob, trade, vrbs):
  class TraderGiveaway (line 629) | class TraderGiveaway(Trader):
    method getorder (line 634) | def getorder(self, time, countdown, lob):
  class TraderZIC (line 659) | class TraderZIC(Trader):
    method getorder (line 664) | def getorder(self, time, countdown, lob):
  class TraderShaver (line 695) | class TraderShaver(Trader):
    method getorder (line 701) | def getorder(self, time, countdown, lob):
  class TraderSniper (line 737) | class TraderSniper(Trader):
    method getorder (line 744) | def getorder(self, time, countdown, lob):
  class TraderPRZI (line 780) | class TraderPRZI(Trader):
    method strat_csv_str (line 793) | def strat_csv_str(strat):
    method mutate_strat (line 803) | def mutate_strat(self, s, mode):
    method strat_str (line 831) | def strat_str(self):
    method __init__ (line 845) | def __init__(self, ttype, tid, balance, params, time):
    method getorder (line 961) | def getorder(self, time, countdown, lob):
    method bookkeep (line 1233) | def bookkeep(self, time, trade, order, vrbs):
    method respond (line 1280) | def respond(self, time, lob, trade, vrbs):
  class TraderZIP (line 1540) | class TraderZIP(Trader):
    method strat_csv_str (line 1552) | def strat_csv_str(strat):
    method mutate_strat (line 1566) | def mutate_strat(s, mode):
    method __init__ (line 1608) | def __init__(self, ttype, tid, balance, params, time):
    method getorder (line 1724) | def getorder(self, time, countdown, lob):
    method respond (line 1763) | def respond(self, time, lob, trade, vrbs):
  class TraderPT1 (line 2088) | class TraderPT1(Trader):
    method __init__ (line 2111) | def __init__(self, ttype, tid, balance, params, time):
    method getorder (line 2151) | def getorder(self, time, countdown, lob):
    method respond (line 2175) | def respond(self, time, lob, trade, vrbs):
    method bookkeep (line 2253) | def bookkeep(self, time, trade, order, vrbs):
  class TraderPT2 (line 2298) | class TraderPT2(Trader):
    method __init__ (line 2321) | def __init__(self, ttype, tid, balance, params, time):
    method getorder (line 2361) | def getorder(self, time, countdown, lob):
    method respond (line 2385) | def respond(self, time, lob, trade, vrbs):
    method bookkeep (line 2463) | def bookkeep(self, time, trade, order, vrbs):
  function trade_stats (line 2513) | def trade_stats(expid, traders, dumpfile, time, lob):
  function populate_market (line 2563) | def populate_market(trdrs_spec, traders, shuffle, vrbs):
  function customer_orders (line 2753) | def customer_orders(time, traders, trader_stats, orders_sched, pending, ...
  function market_session (line 2983) | def market_session(sess_id, starttime, endtime, trader_spec, order_sched...
  function schedule_offsetfn_read_file (line 3234) | def schedule_offsetfn_read_file(filename, col_t, col_p, scale_factor=75):
  function schedule_offsetfn_from_eventlist (line 3324) | def schedule_offsetfn_from_eventlist(time, params):
  function schedule_offsetfn_increasing_sinusoid (line 3347) | def schedule_offsetfn_increasing_sinusoid(t, params):

FILE: Trader_AA.py
  class Trader_AA (line 23) | class Trader_AA(object):
    method __init__ (line 25) | def __init__(self):
    method updateEq (line 68) | def updateEq(self, price):
    method newton4Buying (line 73) | def newton4Buying(self):
    method newton4Selling (line 91) | def newton4Selling(self):
    method updateTarget (line 109) | def updateTarget(self):
    method calcRshout (line 138) | def calcRshout(self, target, buying):
    method updateAgg (line 161) | def updateAgg(self, up, buying, target):
    method updateSmithsAlpha (line 175) | def updateSmithsAlpha(self, price):
    method updateTheta (line 186) | def updateTheta(self):
    method getorder (line 193) | def getorder(self, time, countdown, lob):
    method respond (line 222) | def respond(self, time, lob, trade, verbose):

FILE: ZhenZhang/source/BSE2.py
  class Orderbook_half (line 78) | class Orderbook_half:
    method __init__ (line 80) | def __init__(self, booktype, worstprice):
    method __str__ (line 121) | def __str__(self):
    method anonymize_lob (line 141) | def anonymize_lob(self, verbose):
    method build_lob (line 158) | def build_lob(self, verbose):
    method book_add (line 217) | def book_add(self, order, verbose):
    method book_CAN (line 227) | def book_CAN(self, time, order, pool_id, verbose):
    method book_take (line 263) | def book_take(self, time, order, pool_id, verbose):
  class Orderbook (line 509) | class Orderbook(Orderbook_half):
    method __init__ (line 512) | def __init__(self, id_string):
    method __str__ (line 522) | def __str__(self):
    method midprice (line 531) | def midprice(self, bid_p, bid_q, ask_p, ask_q):
    method microprice (line 545) | def microprice(self, bid_p, bid_q, ask_p, ask_q):
    method add_lim_order (line 553) | def add_lim_order(self, order, verbose):
    method process_order_CAN (line 567) | def process_order_CAN(self, time, order, verbose):
    method process_order_XXX (line 589) | def process_order_XXX(self, time, order, verbose):
    method process_order_take (line 614) | def process_order_take(self, time, order, verbose):
    method process_order_LIM (line 632) | def process_order_LIM(self, time, order, verbose):
    method process_order_pending (line 673) | def process_order_pending(self, time, order, verbose):
  class Exchange (line 704) | class Exchange(Orderbook):
    method __init__ (line 707) | def __init__(self, eid):
    method __str__ (line 717) | def __str__(self):
    class trader_record (line 730) | class trader_record:
      method __init__ (line 733) | def __init__(self, time, tid):
      method __str__ (line 742) | def __str__(self):
    method consolidate_responses (line 747) | def consolidate_responses(self, responses):
    method mkt_open (line 763) | def mkt_open(self, time, verbose):
    method mkt_close (line 792) | def mkt_close(self):
    method tape_update (line 824) | def tape_update(self, tr, verbose):
    method dump_tape (line 841) | def dump_tape(self, session_id, dumpfile, tmode,traders):
    method process_order (line 879) | def process_order(self, time, order, verbose):
    method publish_lob (line 1076) | def publish_lob(self, time, tape_depth, verbose):
  function trade_stats (line 1153) | def trade_stats(expid, traders, dumpfile, time, lob):
  function populate_market (line 1188) | def populate_market(traders_spec, traders, shuffle, verbose):
  function customer_orders (line 1449) | def customer_orders(time, last_update, traders, trader_stats, os, pendin...
  function market_session (line 1649) | def market_session(sess_id, starttime, endtime, trader_spec, order_sched...

FILE: ZhenZhang/source/BSE2_msg_classes.py
  class Assignment (line 3) | class Assignment:
    method __init__ (line 5) | def __init__(self, customer_id, trader_id, otype, ostyle, price, qty, ...
    method __str__ (line 17) | def __str__(self):
  class Order (line 26) | class Order:
    method __init__ (line 28) | def __init__(self, trader_id, otype, ostyle, price, qty, time, endtime...
    method __str__ (line 40) | def __str__(self):
  class Exch_msg (line 46) | class Exch_msg:
    method __init__ (line 48) | def __init__(self, trader_id, order_id, eventtype, transactions, revis...
    method __str__ (line 57) | def __str__(self):

FILE: ZhenZhang/source/BSE_trader_agents.py
  class Trader (line 15) | class Trader:
    method __init__ (line 17) | def __init__(self, ttype, tid, balance, time):
    method __str__ (line 34) | def __str__(self):
    method add_cust_order (line 42) | def add_cust_order(self, order, verbose):
    method del_cust_order (line 59) | def del_cust_order(self, cust_order_id, verbose):
    method revise_cust_order (line 72) | def revise_cust_order(self, cust_order_id, revised_order, verbose):
    method del_exch_order (line 93) | def del_exch_order(self, oid, verbose):
    method bookkeep (line 105) | def bookkeep(self, msg, time, verbose):
    method respond (line 219) | def respond(self, time, lob, trade, verbose):
    method mutate (line 225) | def mutate(self, time, lob, trade, verbose):
  class Trader_Giveaway (line 232) | class Trader_Giveaway(Trader):
    method getorder (line 234) | def getorder(self, time, countdown, lob, verbose):
  class Trader_ZIC (line 255) | class Trader_ZIC(Trader):
    method getorder (line 257) | def getorder(self, time, countdown, lob, verbose):
  class Trader_Shaver (line 284) | class Trader_Shaver(Trader):
    method getorder (line 286) | def getorder(self, time, countdown, lob, verbose):
  class Trader_ISHV (line 319) | class Trader_ISHV(Trader):
    method getorder (line 322) | def getorder(self, time, countdown, lob, verbose):
  class Trader_Sniper (line 383) | class Trader_Sniper(Trader):
    method getorder (line 385) | def getorder(self, time, countdown, lob, verbose):
  class Trader_ZIP (line 420) | class Trader_ZIP(Trader):
    method __init__ (line 427) | def __init__(self, ttype, tid, balance, time):
    method __str__ (line 453) | def __str__(self):
    method getorder (line 464) | def getorder(self, time, countdown, lob, verbose):
    method respond (line 514) | def respond(self, time, lob, trade, verbose):
  class Trader_AA (line 808) | class Trader_AA(Trader):
    method __init__ (line 810) | def __init__(self, ttype, tid, balance, time):
    method calcEq (line 861) | def calcEq(self): ##clear and correct
    method calcAlpha (line 875) | def calcAlpha(self): ##correct. but calcAlpha in snashall's version is...
    method calcTheta (line 882) | def calcTheta(self): ## clear and correct
    method calcRshout (line 897) | def calcRshout(self): ## unclear in Vytelingum's paper
    method calcAgg (line 930) | def calcAgg(self):
    method calcTarget (line 951) | def calcTarget(self):
    method getorder (line 1009) | def getorder(self, time, countdown, lob,verbose):
    method respond (line 1097) | def respond(self, time, lob, trade, verbose):
  class Trader_OAA (line 1182) | class Trader_OAA(Trader):
    method __init__ (line 1184) | def __init__(self, ttype, tid, balance, time):
    method calcEq (line 1235) | def calcEq(self):
    method calcAlpha (line 1249) | def calcAlpha(self):
    method calcTheta (line 1256) | def calcTheta(self):
    method calcRshout (line 1267) | def calcRshout(self):
    method calcAgg (line 1297) | def calcAgg(self):
    method calcTarget (line 1318) | def calcTarget(self):
    method getorder (line 1375) | def getorder(self, time, countdown, lob,verbose):
    method respond (line 1431) | def respond(self, time, lob, trade, verbose):
  class Trader_IAAB (line 1515) | class Trader_IAAB(Trader):
    method __init__ (line 1517) | def __init__(self, ttype, tid, balance, time):
    method add_cust_order (line 1571) | def add_cust_order(self, order, verbose):
    method del_cust_order (line 1591) | def del_cust_order(self, cust_order_id, verbose):
    method revise_cust_order (line 1604) | def revise_cust_order(self, cust_order_id, revised_order, verbose):
    method del_exch_order (line 1628) | def del_exch_order(self, oid, verbose):
    method bookkeep (line 1639) | def bookkeep(self, msg, time, verbose):
    method calcEq (line 1787) | def calcEq(self): ##clear and correct
    method calcAlpha (line 1801) | def calcAlpha(self): ##correct. but calcAlpha in snashall's version is...
    method calcTheta (line 1808) | def calcTheta(self): ## clear and correct
    method calcRshout (line 1823) | def calcRshout(self): ## unclear in Vytelingum's paper
    method calcAgg (line 1856) | def calcAgg(self):
    method calcTarget (line 1877) | def calcTarget(self):
    method getorder (line 1935) | def getorder(self, time, countdown, lob,verbose):
    method respond (line 2046) | def respond(self, time, lob, trade, verbose):

FILE: ZhenZhang/source/GDX.py
  class Trader_GDX (line 13) | class Trader_GDX(Trader):
    method __init__ (line 15) | def __init__(self, ttype, tid, balance, time):
    method getorder (line 47) | def getorder(self, time, countdown, lob, verbose):
    method calc_p_bid (line 74) | def calc_p_bid(self, m, n):
    method calc_p_ask (line 104) | def calc_p_ask(self, m, n):
    method belief_sell (line 134) | def belief_sell(self, price):
    method belief_buy (line 152) | def belief_buy(self, price):
    method respond (line 169) | def respond(self, time, lob, trade, verbose):

FILE: ZhenZhang/source/IAA_MLOFI.py
  class Trader_IAA_MLOFI (line 11) | class Trader_IAA_MLOFI(Trader):
    method __init__ (line 13) | def __init__(self, ttype, tid, balance, time,m):
    method calc_level_n_e (line 62) | def calc_level_n_e(self, current_lob, n):
    method calc_es (line 120) | def calc_es(self, lob, m, verbose):
    method calc_ds (line 127) | def calc_ds(self, lob, m, verbose):
    method cal_depth_n (line 135) | def cal_depth_n(self, lob, n):
    method calcEq (line 148) | def calcEq(self):  ##clear and correct
    method calcAlpha (line 164) | def calcAlpha(self):  ##correct. but calcAlpha in snashall's version i...
    method calcTheta (line 171) | def calcTheta(self):  ## clear and correct
    method calcRshout (line 187) | def calcRshout(self):  ## unclear in Vytelingum's paper
    method calcAgg (line 220) | def calcAgg(self):
    method calcTarget (line 241) | def calcTarget(self):
    method getorder (line 299) | def getorder(self, time, countdown, lob, verbose):
    method respond (line 421) | def respond(self, time, lob, trade, verbose):

FILE: ZhenZhang/source/IAA_NEW.py
  class Trader_IAA_NEW (line 11) | class Trader_IAA_NEW(Trader):
    method __init__ (line 13) | def __init__(self, ttype, tid, balance, time,m):
    method is_imbalance_significant (line 65) | def is_imbalance_significant(self, m,threshold):
    method calc_bids_volume (line 115) | def calc_bids_volume(self, lob, m, verbose):
    method cal_bids_n (line 123) | def cal_bids_n(self, lob, n):
    method calc_asks_volume (line 132) | def calc_asks_volume(self, lob, m, verbose):
    method cal_asks_n (line 140) | def cal_asks_n(self, lob, n):
    method calc_level_n_e (line 148) | def calc_level_n_e(self, current_lob, n):
    method calc_es (line 206) | def calc_es(self, lob, m, verbose):
    method calc_ds (line 213) | def calc_ds(self, lob, m, verbose):
    method cal_depth_n (line 221) | def cal_depth_n(self, lob, n):
    method calcEq (line 234) | def calcEq(self):  ##clear and correct
    method calcAlpha (line 250) | def calcAlpha(self):  ##correct. but calcAlpha in snashall's version i...
    method calcTheta (line 257) | def calcTheta(self):  ## clear and correct
    method calcRshout (line 273) | def calcRshout(self):  ## unclear in Vytelingum's paper
    method calcAgg (line 306) | def calcAgg(self):
    method calcTarget (line 327) | def calcTarget(self):
    method getorder (line 385) | def getorder(self, time, countdown, lob, verbose):
    method respond (line 505) | def respond(self, time, lob, trade, verbose):

FILE: ZhenZhang/source/IGDX_MLOFI.py
  class Trader_IGDX_MLOFI (line 13) | class Trader_IGDX_MLOFI(Trader):
    method __init__ (line 15) | def __init__(self, ttype, tid, balance, time,m):
    method is_imbalance_significant (line 55) | def is_imbalance_significant(self, m, threshold):
    method calc_bids_volume (line 100) | def calc_bids_volume(self, lob, m, verbose):
    method cal_bids_n (line 108) | def cal_bids_n(self, lob, n):
    method calc_asks_volume (line 117) | def calc_asks_volume(self, lob, m, verbose):
    method cal_asks_n (line 125) | def cal_asks_n(self, lob, n):
    method calc_level_n_e (line 133) | def calc_level_n_e(self, current_lob, n):
    method calc_es (line 191) | def calc_es(self, lob, m, verbose):
    method calc_ds (line 198) | def calc_ds(self, lob, m, verbose):
    method cal_depth_n (line 206) | def cal_depth_n(self, lob, n):
    method getorder (line 220) | def getorder(self, time, countdown, lob, verbose):
    method calc_p_bid (line 335) | def calc_p_bid(self, m, n):
    method calc_p_ask (line 365) | def calc_p_ask(self, m, n):
    method belief_sell (line 395) | def belief_sell(self, price):
    method belief_buy (line 413) | def belief_buy(self, price):
    method respond (line 430) | def respond(self, time, lob, trade, verbose):

FILE: ZhenZhang/source/IZIP_MLOFI.py
  class Trader_IZIP_MLOFI (line 10) | class Trader_IZIP_MLOFI(Trader):
    method __init__ (line 17) | def __init__(self, ttype, tid, balance, time, m):
    method is_imbalance_significant (line 55) | def is_imbalance_significant(self, m,threshold):
    method calc_bids_volume (line 105) | def calc_bids_volume(self, lob, m, verbose):
    method cal_bids_n (line 113) | def cal_bids_n(self, lob, n):
    method calc_asks_volume (line 122) | def calc_asks_volume(self, lob, m, verbose):
    method cal_asks_n (line 130) | def cal_asks_n(self, lob, n):
    method calc_level_n_e (line 138) | def calc_level_n_e(self, current_lob, n):
    method calc_es (line 196) | def calc_es(self, lob, m, verbose):
    method calc_ds (line 203) | def calc_ds(self, lob, m, verbose):
    method cal_depth_n (line 211) | def cal_depth_n(self, lob, n):
    method __str__ (line 226) | def __str__(self):
    method getorder (line 241) | def getorder(self, time, countdown, lob, verbose):
    method respond (line 374) | def respond(self, time, lob, trade, verbose):

FILE: ZhenZhang/source/Simple_MLOFI.py
  class Trader_Simple_MLOFI (line 9) | class Trader_Simple_MLOFI(Trader):
    method __init__ (line 11) | def __init__(self, ttype, tid, balance, time):
    method cal_level_n_e (line 24) | def cal_level_n_e(self, current_lob, n):
    method cal_e (line 82) | def cal_e(self, time, lob, trade, verbose):
    method cal_depth (line 96) | def cal_depth(self, lob):
    method cal_depth_n (line 109) | def cal_depth_n(self, lob, n):
    method getorder (line 123) | def getorder(self, time, countdown, lob, verbose):
    method respond (line 246) | def respond(self, time, lob, trade, verbose):

FILE: ZhenZhang/source/ZZISHV.py
  class Trader_ZZISHV (line 10) | class Trader_ZZISHV(Trader):
    method __init__ (line 12) | def __init__(self, ttype, tid, balance, time,m):
    method is_imbalance_significant (line 31) | def is_imbalance_significant(self, m,threshold):
    method calc_bids_volume (line 81) | def calc_bids_volume(self, lob, m, verbose):
    method cal_bids_n (line 89) | def cal_bids_n(self, lob, n):
    method calc_asks_volume (line 98) | def calc_asks_volume(self, lob, m, verbose):
    method cal_asks_n (line 106) | def cal_asks_n(self, lob, n):
    method calc_level_n_e (line 114) | def calc_level_n_e(self, current_lob, n):
    method calc_es (line 172) | def calc_es(self, lob, m, verbose):
    method calc_ds (line 179) | def calc_ds(self, lob, m, verbose):
    method cal_depth_n (line 187) | def cal_depth_n(self, lob, n):
    method respond (line 200) | def respond(self, time, lob, trade, verbose):
    method getorder (line 211) | def getorder(self, time, countdown, lob, verbose):

FILE: snashall2019.py
  class Order (line 60) | class Order:
    method __init__ (line 62) | def __init__(self, tid, otype, price, qty, time, qid):
    method __str__ (line 70) | def __str__(self):
  class Orderbook_half (line 78) | class Orderbook_half:
    method __init__ (line 80) | def __init__(self, booktype, worstprice):
    method anonymize_lob (line 97) | def anonymize_lob(self):
    method build_lob (line 106) | def build_lob(self):
    method book_add (line 141) | def book_add(self, order):
    method book_del (line 160) | def book_del(self, order):
    method delete_best (line 172) | def delete_best(self):
  class Orderbook (line 207) | class Orderbook(Orderbook_half):
    method __init__ (line 209) | def __init__(self):
  class Exchange (line 219) | class Exchange(Orderbook):
    method add_order (line 221) | def add_order(self, order, verbose):
    method del_order (line 240) | def del_order(self, time, order, verbose):
    method process_order2 (line 272) | def process_order2(self, time, order, verbose):
    method tape_dump (line 332) | def tape_dump(self, fname, fmode, tmode):
    method publish_lob (line 344) | def publish_lob(self, time, verbose):
  class Trader (line 376) | class Trader:
    method __init__ (line 378) | def __init__(self, ttype, tid, balance, time):
    method __str__ (line 393) | def __str__(self):
    method add_order (line 398) | def add_order(self, order, verbose):
    method del_order (line 412) | def del_order(self, order):
    method bookkeep (line 418) | def bookkeep(self, trade, order, verbose, time):
    method respond (line 446) | def respond(self, time, lob, trade, verbose):
    method mutate (line 451) | def mutate(self, time, lob, trade, verbose):
  class Trader_Giveaway (line 459) | class Trader_Giveaway(Trader):
    method getorder (line 461) | def getorder(self, time, countdown, lob):
  class Trader_AA (line 475) | class Trader_AA(Trader):
    method __init__ (line 477) | def __init__(self, ttype, tid, balance, time):
    method calcEq (line 527) | def calcEq(self):
    method calcAlpha (line 541) | def calcAlpha(self):
    method calcTheta (line 548) | def calcTheta(self):
    method calcRshout (line 559) | def calcRshout(self):
    method calcAgg (line 589) | def calcAgg(self):
    method calcTarget (line 610) | def calcTarget(self):
    method getorder (line 667) | def getorder(self, time, countdown, lob):
    method respond (line 720) | def respond(self, time, lob, trade, verbose):
  class Trader_ZIC (line 795) | class Trader_ZIC(Trader):
    method getorder (line 797) | def getorder(self, time, countdown, lob):
  class Trader_Shaver (line 820) | class Trader_Shaver(Trader):
    method getorder (line 822) | def getorder(self, time, countdown, lob):
  class Trader_Sniper (line 851) | class Trader_Sniper(Trader):
    method getorder (line 853) | def getorder(self, time, countdown, lob):
  class Trader_ASAD (line 886) | class Trader_ASAD(Trader):
    method __init__ (line 893) | def __init__(self, ttype, tid, balance, time):
    method getorder (line 925) | def getorder(self, time, countdown, lob):
    method respond (line 949) | def respond(self, time, lob, trade, verbose):
  class Trader_GDX (line 1134) | class Trader_GDX(Trader):
    method __init__ (line 1136) | def __init__(self, ttype, tid, balance, time):
    method getorder (line 1174) | def getorder(self, time, countdown, lob):
    method calc_p_bid (line 1196) | def calc_p_bid(self, m, n):
    method calc_p_ask (line 1226) | def calc_p_ask(self, m, n):
    method belief_sell (line 1256) | def belief_sell(self, price):
    method belief_buy (line 1274) | def belief_buy(self, price):
    method respond (line 1291) | def respond(self, time, lob, trade, verbose):
  class Trader_ZIP (line 1372) | class Trader_ZIP(Trader):
    method __init__ (line 1379) | def __init__(self, ttype, tid, balance, time):
    method getorder (line 1409) | def getorder(self, time, countdown, lob):
    method respond (line 1432) | def respond(self, time, lob, trade, verbose):
  function trade_stats (line 1604) | def trade_stats(expid, traders, dumpfile, time, lob):
  function populate_market (line 1670) | def populate_market(traders_spec, traders, shuffle, verbose):
  function customer_orders (line 1767) | def customer_orders(time, last_update, traders, trader_stats, os, pendin...
  function market_session (line 1942) | def market_session(sess_id, starttime, endtime, trader_spec, order_sched...
  function schedule_offsetfn (line 2063) | def schedule_offsetfn(t):
Condensed preview — 76 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,283K chars).
[
  {
    "path": ".gitignore",
    "chars": 47,
    "preview": ".DS_Store\n.ipynb_checkpoints\n__pycache__\n*.csv\n"
  },
  {
    "path": ".idea/.name",
    "chars": 6,
    "preview": "BSE.py"
  },
  {
    "path": ".idea/BristolStockExchange.iml",
    "chars": 352,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"PYTHON_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager"
  },
  {
    "path": ".idea/inspectionProfiles/profiles_settings.xml",
    "chars": 174,
    "preview": "<component name=\"InspectionProjectProfileManager\">\n  <settings>\n    <option name=\"USE_PROJECT_PROFILE\" value=\"false\" />\n"
  },
  {
    "path": ".idea/misc.xml",
    "chars": 208,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectRootManager\" version=\"2\" project-"
  },
  {
    "path": ".idea/modules.xml",
    "chars": 292,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n   "
  },
  {
    "path": ".idea/vcs.xml",
    "chars": 180,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping dire"
  },
  {
    "path": ".idea/workspace.xml",
    "chars": 5072,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"AutoImportSettings\">\n    <option name=\"a"
  },
  {
    "path": "BSE.py",
    "chars": 159086,
    "preview": "# -*- coding: utf-8 -*-\n#\n# BSE: The Bristol Stock Exchange\n#\n# Version 1.91: November 2024 fixed PT1 + PT2 parameter pa"
  },
  {
    "path": "BSE_VernonSmith1962_demo.ipynb",
    "chars": 607713,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"pycharm\": {\n     \"name\": \"#%% md\\n\"\n    },\n    \"tags"
  },
  {
    "path": "LICENSE.md",
    "chars": 1164,
    "preview": "\n\nCopyright (c) 2012 Dave Cliff\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this so"
  },
  {
    "path": "Offsets_BTC_USD/empty_file",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "README.md",
    "chars": 2316,
    "preview": "<i>NB: in Q3 of 2025, 13 years after BSE was first launched, we'll be making BSE2 available in a separate repo. BSE2 is "
  },
  {
    "path": "Trader_AA.py",
    "chars": 13400,
    "preview": "'''\nCreated on 1 Dec 2012\n\n@author: Ash Booth\n\nAA order execution strategy as described in: \"Perukrishnen, Cliff and Jen"
  },
  {
    "path": "ZhenZhang/README.md",
    "chars": 620,
    "preview": "This folder holds the sourcecode developed by Zhen Zhang as part of his MSc project, supervised by Dave Cliff, at the Un"
  },
  {
    "path": "ZhenZhang/source/BSE2.py",
    "chars": 104318,
    "preview": "# -*- coding: utf-8 -*-\n#\n# BSE: The Bristol Stock Exchange\n#\n# Version 2.0Beta: Nov 20th, 2018.\n# Version 1.4: August 3"
  },
  {
    "path": "ZhenZhang/source/BSE2_msg_classes.py",
    "chars": 3628,
    "preview": "# Assignment:\n# The details of a customer's order/request, assigned to a trader\nclass Assignment:\n\n        def __init__("
  },
  {
    "path": "ZhenZhang/source/BSE_trader_agents.py",
    "chars": 112904,
    "preview": "\nfrom BSE2_msg_classes import Assignment, Order, Exch_msg\n\n\n##################--Traders below here--#############\nimport"
  },
  {
    "path": "ZhenZhang/source/GDX.py",
    "chars": 11933,
    "preview": "# Trader subclass ZIP\r\n# After Cliff 1997\r\n\r\n\r\nfrom BSE2_msg_classes import Assignment, Order, Exch_msg\r\nfrom BSE_trader"
  },
  {
    "path": "ZhenZhang/source/IAA_MLOFI.py",
    "chars": 20759,
    "preview": "\r\nfrom BSE2_msg_classes import Assignment, Order, Exch_msg\r\nfrom BSE_trader_agents import Trader;\r\nimport random\r\nimport"
  },
  {
    "path": "ZhenZhang/source/IAA_NEW.py",
    "chars": 23073,
    "preview": "\r\nfrom BSE2_msg_classes import Assignment, Order, Exch_msg\r\nfrom BSE_trader_agents import Trader;\r\nimport random\r\nimport"
  },
  {
    "path": "ZhenZhang/source/IGDX_MLOFI.py",
    "chars": 21069,
    "preview": "# Trader subclass ZIP\r\n# After Cliff 1997\r\n\r\n\r\nfrom BSE2_msg_classes import Assignment, Order, Exch_msg\r\nfrom BSE_trader"
  },
  {
    "path": "ZhenZhang/source/IZIP_MLOFI.py",
    "chars": 26440,
    "preview": "\r\nfrom BSE2_msg_classes import Assignment, Order, Exch_msg\r\nfrom BSE_trader_agents import Trader;\r\nimport random\r\nimport"
  },
  {
    "path": "ZhenZhang/source/Simple_MLOFI.py",
    "chars": 8925,
    "preview": "from BSE2_msg_classes import Assignment, Order, Exch_msg\r\nfrom BSE_trader_agents import Trader;\r\nimport random\r\nimport m"
  },
  {
    "path": "ZhenZhang/source/ZZISHV.py",
    "chars": 9575,
    "preview": "\r\nfrom BSE2_msg_classes import Assignment, Order, Exch_msg\r\nfrom BSE_trader_agents import Trader;\r\nimport random\r\nimport"
  },
  {
    "path": "ZhenZhang/source/dataAnalysis/box_analysis.py",
    "chars": 7786,
    "preview": "\r\n\r\nimport numpy as np\r\nimport matplotlib.pyplot as plt\r\nimport csv\r\nfrom pylab import *\r\n# Fixing random state for repr"
  },
  {
    "path": "ZhenZhang/source/dataAnalysis/hypothesisTest.py",
    "chars": 1245,
    "preview": "\r\n\r\n\r\nimport scipy.stats as stats\r\nimport csv\r\ncsv_file = open(\"../Mybalances.csv\",\"r\")\r\ncsv_reader = csv.reader(csv_fil"
  },
  {
    "path": "ZhenZhang/source/dataAnalysis/matplotlib4.py",
    "chars": 1466,
    "preview": "\r\n\r\nfrom matplotlib import pyplot as plot\r\nimport csv\r\nimport random\r\n\r\n\r\n\r\n\r\n\r\n\r\ncsv_file = open(\"../Mybalances.csv\",\"r"
  },
  {
    "path": "clean.sh",
    "chars": 9,
    "preview": "rm *.csv\n"
  },
  {
    "path": "snashall2019.py",
    "chars": 108804,
    "preview": "# -*- coding: utf-8 -*-\r\n#\r\n# BSE: The Bristol Stock Exchange\r\n#\r\n# Version 1.3; July 21st, 2018.\r\n# Version 1.2; Novemb"
  }
]

// ... and 46 more files (download for full content)

About this extraction

This page contains the full source code of the davecliff/BristolStockExchange GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 76 files (1.2 MB), approximately 545.1k tokens, and a symbol index with 340 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!