diff --git a/docs/source/reference/agents.rst b/docs/source/reference/agents.rst index 16c25364..29a63d62 100644 --- a/docs/source/reference/agents.rst +++ b/docs/source/reference/agents.rst @@ -9,4 +9,5 @@ Agents agents.Agent agents.HighFrequencyAgent agents.FCNAgent - agents.ArbitrageAgent \ No newline at end of file + agents.ArbitrageAgent + agents.MarketShareFCNAgent diff --git a/docs/source/user_guide/config.rst b/docs/source/user_guide/config.rst index 25cd92af..6c0b4e6b 100644 --- a/docs/source/user_guide/config.rst +++ b/docs/source/user_guide/config.rst @@ -94,6 +94,10 @@ Json config "timeWindowSize": JsonRandomFormat, "orderMargin": JsonRandomFormat, "marginType": "fixed" or "normal" (Optional; default fixed) + }, + "MarketShareFCNAgents": { + "class": "MarketShareFCNAgent", + "extends": "FCNAgent" }, "ArbitrageAgent": { "class": "ArbitrageAgent", @@ -102,4 +106,11 @@ Json config "orderThresholdPrice": float, "orderTimeLength": int (Optional, default 1), }, + "MarketMakerAgent": { + "class": "MarketMakerAgent", + "extends": "Agents", + "targetMarket": string required, + "netInterestSpread": float required, + "orderTimeLength": int optional; default 2, + } } diff --git a/examples/fat_finger.ipynb b/examples/fat_finger.ipynb new file mode 100644 index 00000000..f1b0b5c5 --- /dev/null +++ b/examples/fat_finger.ipynb @@ -0,0 +1,191 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "58xSRq9jpa-2" + }, + "source": [ + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](http://colab.research.google.com/github/masanorihirano/pams/blob/main/examples/fat_finger.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "OwaI8_xbpa-5", + "outputId": "1df64bd6-6e6b-4365-8f03-c6f3fc583f5e" + }, + "outputs": [], + "source": [ + "# Please remove comment-out if necessary\n", + "#! pip install pams matplotlib" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ixLeaU7Epa-5" + }, + "outputs": [], + "source": [ + "config = {\n", + "\t\"simulation\": {\n", + "\t\t\"markets\": [\"Market\"],\n", + "\t\t\"agents\": [\"FCNAgents\"],\n", + "\t\t\"sessions\": [\n", + "\t\t\t{\t\"sessionName\": 0,\n", + "\t\t\t\t\"iterationSteps\": 100,\n", + "\t\t\t\t\"withOrderPlacement\": True,\n", + "\t\t\t\t\"withOrderExecution\": False,\n", + "\t\t\t\t\"withPrint\": True\n", + "\t\t\t},\n", + "\t\t\t{\t\"sessionName\": 1,\n", + "\t\t\t\t\"iterationSteps\": 500,\n", + "\t\t\t\t\"withOrderPlacement\": True,\n", + "\t\t\t\t\"withOrderExecution\": True,\n", + "\t\t\t\t\"withPrint\": True,\n", + "\t\t\t\t\"events\": [\"OrderMistakeShock\"]\n", + "\t\t\t}\n", + "\t\t]\n", + "\t},\n", + "\n", + "\t\"OrderMistakeShock\": {\n", + "\t\t\"class\": \"OrderMistakeShock\",\n", + "\t\t\"target\": \"Market\",\n", + "\t\t\"triggerTime\": 100, \"MEMO\": \"At the 100th step of the session 2\",\n", + "\t\t\"priceChangeRate\": -0.05, \"MEMO\": \"Sign: negative for down; positive for up; zero for no change\",\n", + "\t\t\"orderVolume\": 10000, \"MEMO\": \"Very much\",\n", + "\t\t\"orderTimeLength\": 10000, \"MEMO\": \"Very long\",\n", + "\t\t\"enabled\": True\n", + "\t},\n", + "\n", + "\t\"Market\": {\n", + "\t\t\"class\": \"Market\",\n", + "\t\t\"tickSize\": 0.00001,\n", + "\t\t\"marketPrice\": 300.0,\n", + "\t\t\"outstandingShares\": 25000\n", + "\t},\n", + "\n", + "\t\"FCNAgents\": {\n", + "\t\t\"class\": \"FCNAgent\",\n", + "\t\t\"numAgents\": 100,\n", + "\n", + "\t\t\"MEMO\": \"Agent class\",\n", + "\t\t\"markets\": [\"Market\"],\n", + "\t\t\"assetVolume\": 50,\n", + "\t\t\"cashAmount\": 10000,\n", + "\n", + "\t\t\"MEMO\": \"FCNAgent class\",\n", + "\t\t\"fundamentalWeight\": {\"expon\": [1.0]},\n", + "\t\t\"chartWeight\": {\"expon\": [0.0]},\n", + "\t\t\"noiseWeight\": {\"expon\": [1.0]},\n", + "\t\t\"noiseScale\": 0.001,\n", + "\t\t\"timeWindowSize\": [100, 200],\n", + "\t\t\"orderMargin\": [0.0, 0.1]\n", + "\t}\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xUUUfulSpa-6" + }, + "outputs": [], + "source": [ + "import random\n", + "import matplotlib.pyplot as plt\n", + "from pams.runners import SequentialRunner\n", + "from pams.logs.market_step_loggers import MarketStepSaver" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "3QXSkEw2pa-6", + "outputId": "fb454d0c-57b3-4cb6-d6ad-4512dbe429c1" + }, + "outputs": [], + "source": [ + "saver = MarketStepSaver()\n", + "\n", + "runner = SequentialRunner(\n", + " settings=config,\n", + " prng=random.Random(42),\n", + " logger=saver,\n", + ")\n", + "runner.main()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "P5_pyTa9pa-6" + }, + "outputs": [], + "source": [ + "market_prices = dict(sorted(map(lambda x: (x[\"market_time\"], x[\"market_price\"]), saver.market_step_logs)))\n", + "fundamental_prices = dict(sorted(map(lambda x: (x[\"market_time\"], x[\"fundamental_price\"]), saver.market_step_logs)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 449 + }, + "id": "c__AgWzapa-7", + "outputId": "456ed7cb-0b14-4774-845c-da50b68a7031", + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "plt.plot(list(market_prices.keys()), list(market_prices.values()))\n", + "plt.plot(list(fundamental_prices.keys()), list(fundamental_prices.values()), color='black')\n", + "plt.xlabel(\"ticks\")\n", + "plt.ylabel(\"market price\")\n", + "plt.ylim([270, 330])\n", + "plt.show()" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/examples/market_share.ipynb b/examples/market_share.ipynb new file mode 100644 index 00000000..7f508b65 --- /dev/null +++ b/examples/market_share.ipynb @@ -0,0 +1,439 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "58xSRq9jpa-2" + }, + "source": [ + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](http://colab.research.google.com/github/masanorihirano/pams/blob/main/examples/market_share.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "OwaI8_xbpa-5", + "outputId": "1df64bd6-6e6b-4365-8f03-c6f3fc583f5e" + }, + "outputs": [], + "source": [ + "# Please remove comment-out if necessary\n", + "#! pip install pams matplotlib" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pams import Market\n", + "from typing import Any\n", + "from typing import Dict\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Only MarketShareFCNAgents" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "class ExtendedMarket(Market):\n", + " def setup(self, settings: Dict[str, Any], *args, **kwargs) -> None:\n", + " super(ExtendedMarket, self).setup(settings, *args, **kwargs)\n", + " if \"tradeVolume\" in settings:\n", + " if not isinstance(settings[\"tradeVolume\"], int):\n", + " raise ValueError(\"tradeVolume must be int\")\n", + " self._executed_volumes = [int(settings[\"tradeVolume\"])]" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "from pams.logs import MarketStepEndLog\n", + "from pams.logs.market_step_loggers import MarketStepSaver\n", + "\n", + "class MarketStepSaverForMarketShare(MarketStepSaver):\n", + " def process_market_step_end_log(self, log: MarketStepEndLog) -> None:\n", + " self.market_step_logs.append(\n", + " {\n", + " \"session_id\": log.session.session_id,\n", + " \"market_time\": log.market.get_time(),\n", + " \"market_id\": log.market.market_id,\n", + " \"market_name\": log.market.name,\n", + " \"market_price\": log.market.get_market_price(),\n", + " \"fundamental_price\": log.market.get_fundamental_price(),\n", + "\t\t\t\t\"executed_volume\": log.market.get_executed_volume(),\n", + " }\n", + " )" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ixLeaU7Epa-5" + }, + "outputs": [], + "source": [ + "config1 = {\n", + "\t\"simulation\": {\n", + "\t\t\"markets\": [\"Market-A\", \"Market-B\"],\n", + "\t\t\"agents\": [\"MarketShareFCNAgents\"],\n", + "\t\t\"sessions\": [\n", + "\t\t\t{\t\"sessionName\": 0,\n", + "\t\t\t\t\"iterationSteps\": 100,\n", + "\t\t\t\t\"withOrderPlacement\": True,\n", + "\t\t\t\t\"withOrderExecution\": False,\n", + "\t\t\t\t\"withPrint\": True\n", + "\t\t\t},\n", + "\t\t\t{\t\"sessionName\": 1,\n", + "\t\t\t\t\"iterationSteps\": 2000,\n", + "\t\t\t\t\"withOrderPlacement\": True,\n", + "\t\t\t\t\"withOrderExecution\": True,\n", + "\t\t\t\t\"withPrint\": True,\n", + "\t\t\t\t\"maxHighFrequencyOrders\": 1\n", + "\t\t\t}\n", + "\t\t]\n", + "\t},\n", + "\n", + "\t\"Market-A\": {\n", + "\t\t\"class\": \"ExtendedMarket\",\n", + "\t\t\"tickSize\": 10.0,\n", + "\t\t\"marketPrice\": 300.0,\n", + "\t\t\"outstandingShares\": 25000,\n", + "\n", + "\t\t\"MEMO\": \"Required only here\",\n", + "\t\t\"tradeVolume\": 90\n", + "\t},\n", + "\n", + "\t\"Market-B\": {\n", + "\t\t\"class\": \"ExtendedMarket\",\n", + "\t\t\"tickSize\": 1.0,\n", + "\t\t\"marketPrice\": 300.0,\n", + "\t\t\"outstandingShares\": 25000,\n", + "\n", + "\t\t\"MEMO\": \"Required only here\",\n", + "\t\t\"tradeVolume\": 10\n", + "\t},\n", + "\n", + "\t\"MarketShareFCNAgents\": {\n", + "\t\t\"class\": \"MarketShareFCNAgent\",\n", + "\t\t\"numAgents\": 100,\n", + "\n", + "\t\t\"MEMO\": \"Agent class\",\n", + "\t\t\"markets\": [\"Market-A\", \"Market-B\"],\n", + "\t\t\"assetVolume\": 50,\n", + "\t\t\"cashAmount\": 10000,\n", + "\n", + "\t\t\"MEMO\": \"FCNAgent class\",\n", + "\t\t\"fundamentalWeight\": {\"expon\": [1.0]},\n", + "\t\t\"chartWeight\": {\"expon\": [0.2]},\n", + "\t\t\"noiseWeight\": {\"expon\": [1.0]},\n", + "\t\t\"noiseScale\": 0.0001,\n", + "\t\t\"timeWindowSize\": [100, 200],\n", + "\t\t\"orderMargin\": [0.0, 0.1],\n", + "\t\t\"marginType\": \"normal\"\n", + "\t}\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "3QXSkEw2pa-6", + "outputId": "fb454d0c-57b3-4cb6-d6ad-4512dbe429c1" + }, + "outputs": [], + "source": [ + "import random\n", + "import matplotlib.pyplot as plt\n", + "from pams.runners import SequentialRunner\n", + "\n", + "saver = MarketStepSaverForMarketShare()\n", + "\n", + "runner = SequentialRunner(\n", + " settings=config1,\n", + " prng=random.Random(42),\n", + " logger=saver,\n", + ")\n", + "runner.class_register(cls=ExtendedMarket)\n", + "runner.main()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "P5_pyTa9pa-6" + }, + "outputs": [], + "source": [ + "market_prices_market_a = dict(sorted(map(lambda x: (x[\"market_time\"], x[\"market_price\"]), filter(lambda x: x[\"market_name\"] == \"Market-A\", saver.market_step_logs))))\n", + "market_prices_market_b = dict(sorted(map(lambda x: (x[\"market_time\"], x[\"market_price\"]), filter(lambda x: x[\"market_name\"] == \"Market-B\", saver.market_step_logs))))\n", + "\n", + "fundamental_prices_market_a = dict(sorted(map(lambda x: (x[\"market_time\"], x[\"fundamental_price\"]), filter(lambda x: x[\"market_name\"] == \"Market-A\", saver.market_step_logs))))\n", + "fundamental_prices_market_b = dict(sorted(map(lambda x: (x[\"market_time\"], x[\"fundamental_price\"]), filter(lambda x: x[\"market_name\"] == \"Market-B\", saver.market_step_logs))))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 449 + }, + "id": "c__AgWzapa-7", + "outputId": "456ed7cb-0b14-4774-845c-da50b68a7031" + }, + "outputs": [], + "source": [ + "plt.plot(list(market_prices_market_a.keys()), list(market_prices_market_a.values()), label=\"Market-A\")\n", + "plt.plot(list(market_prices_market_b.keys()), list(market_prices_market_b.values()), label=\"Market-B\")\n", + "plt.xlabel(\"ticks\")\n", + "plt.ylabel(\"market price\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "executed_volumes_market_a = np.convolve(np.ones(100), np.asarray(list(map(lambda x: x[\"executed_volume\"], filter(lambda x: x[\"market_name\"] == \"Market-A\", saver.market_step_logs)))[101:]), mode=\"valid\")\n", + "executed_volumes_market_b = np.convolve(np.ones(100), np.asarray(list(map(lambda x: x[\"executed_volume\"], filter(lambda x: x[\"market_name\"] == \"Market-B\", saver.market_step_logs)))[101:]), mode=\"valid\")\n", + "executed_volumes_total = executed_volumes_market_a + executed_volumes_market_b" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "plt.plot(executed_volumes_market_a / executed_volumes_total, label=\"Market-A\")\n", + "plt.plot(executed_volumes_market_b / executed_volumes_total, label=\"Market-B\")\n", + "plt.xlabel(\"ticks\")\n", + "plt.ylabel(\"market share\")\n", + "plt.legend()\n", + "plt.show()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## MarketShareFCNAgents + MarketMakerAgent" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "config2 = {\n", + "\t\"simulation\": {\n", + "\t\t\"markets\": [\"Market-A\", \"Market-B\"],\n", + "\t\t\"agents\": [\"MarketShareFCNAgents\", \"MarketMakerAgent\"],\n", + "\t\t\"sessions\": [\n", + "\t\t\t{\t\"sessionName\": 0,\n", + "\t\t\t\t\"iterationSteps\": 100,\n", + "\t\t\t\t\"withOrderPlacement\": True,\n", + "\t\t\t\t\"withOrderExecution\": False,\n", + "\t\t\t\t\"withPrint\": True\n", + "\t\t\t},\n", + "\t\t\t{\t\"sessionName\": 1,\n", + "\t\t\t\t\"iterationSteps\": 2000,\n", + "\t\t\t\t\"withOrderPlacement\": True,\n", + "\t\t\t\t\"withOrderExecution\": True,\n", + "\t\t\t\t\"withPrint\": True,\n", + "\t\t\t\t\"maxHighFrequencyOrders\": 1\n", + "\t\t\t}\n", + "\t\t]\n", + "\t},\n", + "\n", + "\t\"Market-A\": {\n", + "\t\t\"class\": \"ExtendedMarket\",\n", + "\t\t\"tickSize\": 0.00001,\n", + "\t\t\"marketPrice\": 300.0,\n", + "\t\t\"outstandingShares\": 25000,\n", + "\n", + "\t\t\"MEMO\": \"Required only here\",\n", + "\t\t\"tradeVolume\": 50\n", + "\t},\n", + "\n", + "\t\"Market-B\": {\n", + "\t\t\"class\": \"ExtendedMarket\",\n", + "\t\t\"tickSize\": 0.00001,\n", + "\t\t\"marketPrice\": 300.0,\n", + "\t\t\"outstandingShares\": 25000,\n", + "\n", + "\t\t\"MEMO\": \"Required only here\",\n", + "\t\t\"tradeVolume\": 50\n", + "\t},\n", + "\n", + "\t\"MarketShareFCNAgents\": {\n", + "\t\t\"class\": \"MarketShareFCNAgent\",\n", + "\t\t\"numAgents\": 100,\n", + "\n", + "\t\t\"MEMO\": \"Agent class\",\n", + "\t\t\"markets\": [\"Market-A\", \"Market-B\"],\n", + "\t\t\"assetVolume\": 50,\n", + "\t\t\"cashAmount\": 10000,\n", + "\n", + "\t\t\"MEMO\": \"FCNAgent class\",\n", + "\t\t\"fundamentalWeight\": {\"expon\": [1.0]},\n", + "\t\t\"chartWeight\": {\"expon\": [0.0]},\n", + "\t\t\"noiseWeight\": {\"expon\": [1.0]},\n", + "\t\t\"noiseScale\": 0.001,\n", + "\t\t\"timeWindowSize\": [100, 200],\n", + "\t\t\"orderMargin\": [0.0, 0.1]\n", + "\t},\n", + "\n", + "\t\"MarketMakerAgent\": {\n", + "\t\t\"class\": \"MarketMakerAgent\",\n", + "\t\t\"numAgents\": 1,\n", + "\n", + "\t\t\"markets\": [\"Market-B\"],\n", + "\t\t\"assetVolume\": 50,\n", + "\t\t\"cashAmount\": 10000,\n", + "\n", + "\t\t\"targetMarket\": \"Market-B\",\n", + "\t\t\"netInterestSpread\": 0.02,\n", + "\t\t\"orderTimeLength\": 100\n", + "\t}\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "saver = MarketStepSaverForMarketShare()\n", + "\n", + "runner = SequentialRunner(\n", + " settings=config2,\n", + " prng=random.Random(42),\n", + " logger=saver,\n", + ")\n", + "runner.class_register(cls=ExtendedMarket)\n", + "runner.main()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "market_prices_market_a = dict(sorted(map(lambda x: (x[\"market_time\"], x[\"market_price\"]), filter(lambda x: x[\"market_name\"] == \"Market-A\", saver.market_step_logs))))\n", + "market_prices_market_b = dict(sorted(map(lambda x: (x[\"market_time\"], x[\"market_price\"]), filter(lambda x: x[\"market_name\"] == \"Market-B\", saver.market_step_logs))))\n", + "\n", + "fundamental_prices_market_a = dict(sorted(map(lambda x: (x[\"market_time\"], x[\"fundamental_price\"]), filter(lambda x: x[\"market_name\"] == \"Market-A\", saver.market_step_logs))))\n", + "fundamental_prices_market_b = dict(sorted(map(lambda x: (x[\"market_time\"], x[\"fundamental_price\"]), filter(lambda x: x[\"market_name\"] == \"Market-B\", saver.market_step_logs))))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.plot(list(market_prices_market_a.keys()), list(market_prices_market_a.values()), label=\"Market-A\")\n", + "plt.plot(list(market_prices_market_b.keys()), list(market_prices_market_b.values()), label=\"Market-B\")\n", + "#plt.plot(list(fundamental_prices_market_a.keys()), list(fundamental_prices_market_a.values()), color='black')\n", + "#plt.plot(list(fundamental_prices_market_b.keys()), list(fundamental_prices_market_b.values()), color='black')\n", + "plt.xlabel(\"ticks\")\n", + "plt.ylabel(\"market price\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "executed_volumes_market_a = np.convolve(np.ones(100), np.asarray(list(map(lambda x: x[\"executed_volume\"], filter(lambda x: x[\"market_name\"] == \"Market-A\", saver.market_step_logs)))[101:]), mode=\"valid\")\n", + "executed_volumes_market_b = np.convolve(np.ones(100), np.asarray(list(map(lambda x: x[\"executed_volume\"], filter(lambda x: x[\"market_name\"] == \"Market-B\", saver.market_step_logs)))[101:]), mode=\"valid\")\n", + "executed_volumes_total = executed_volumes_market_a + executed_volumes_market_b" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "plt.plot(executed_volumes_market_a / executed_volumes_total, label=\"Market-A\")\n", + "plt.plot(executed_volumes_market_b / executed_volumes_total, label=\"Market-B\")\n", + "plt.xlabel(\"ticks\")\n", + "plt.ylabel(\"market share\")\n", + "plt.legend()\n", + "plt.show()" + ], + "metadata": { + "collapsed": false + } + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.13" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/pams/agents/__init__.py b/pams/agents/__init__.py index 49c7dc44..bf4c5743 100644 --- a/pams/agents/__init__.py +++ b/pams/agents/__init__.py @@ -2,4 +2,6 @@ from .base import Agent from .fcn_agent import FCNAgent from .high_frequency_agent import HighFrequencyAgent +from .market_maker_agent import MarketMakerAgent +from .market_share_fcn_agent import MarketShareFCNAgent from .test_agent import TestAgent diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py new file mode 100644 index 00000000..11b7fe39 --- /dev/null +++ b/pams/agents/market_maker_agent.py @@ -0,0 +1,131 @@ +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import Union + +from pams.agents.high_frequency_agent import HighFrequencyAgent +from pams.market import Market +from pams.order import LIMIT_ORDER +from pams.order import Cancel +from pams.order import Order +from pams.utils.json_random import JsonRandom + + +class MarketMakerAgent(HighFrequencyAgent): + """Market Maker Agent class + + This class inherits from the :class:`pams.agents.Agent` class. + + This agent submits orders to the target market at the following price: + :math:`\left\{\max_i(P^b_i) + \min_i(P^a_i) \pm P_f \times \theta\right\} / 2` + where :math:`P^b_i` and :math:`P^a_i` are the best bid and ask prices of the :math:`i`-th target market, + and :math:`P_f` is the fundamental price of the target market. + """ # NOQA + + target_market: Market + net_interest_spread: float + order_time_length: int + + def setup( + self, + settings: Dict[str, Any], + accessible_markets_ids: List[int], + *args: Any, + **kwargs: Any, + ) -> None: + """agent setup. Usually be called from simulator/runner automatically. + + Args: + settings (Dict[str, Any]): agent configuration. + This must include the parameters "targetMarket" and "netInterestSpread". + This can include the parameters "orderTimeLength". + accessible_markets_ids (List[int]): list of market IDs. + + Returns: + None + """ + super().setup(settings=settings, accessible_markets_ids=accessible_markets_ids) + if "targetMarket" not in settings: + raise ValueError("targetMarket is required for MarketMakerAgent.") + if not isinstance(settings["targetMarket"], str): + raise ValueError("targetMarket must be string") + self.target_market = self.simulator.name2market[settings["targetMarket"]] + if "netInterestSpread" not in settings: + raise ValueError("netInterestSpread is required for MarketMakerAgent.") + json_random: JsonRandom = JsonRandom(prng=self.prng) + self.net_interest_spread = json_random.random( + json_value=settings["netInterestSpread"] + ) + self.order_time_length = ( + int(json_random.random(json_value=settings["orderTimeLength"])) + if "orderTimeLength" in settings + else 2 + ) + + def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: + """submit orders. + + .. seealso:: + - :func:`pams.agents.Agent.submit_orders` + """ + orders: List[Union[Order, Cancel]] = [] + base_price: Optional[float] = self.get_base_price(markets=markets) + if base_price is None: + base_price = self.target_market.get_market_price() + price_margin: float = ( + self.target_market.get_fundamental_price() * self.net_interest_spread * 0.5 + ) + order_volume: int = 1 + orders.append( + Order( + agent_id=self.agent_id, + market_id=self.target_market.market_id, + is_buy=True, + kind=LIMIT_ORDER, + volume=order_volume, + price=base_price - price_margin, + ttl=self.order_time_length, + ) + ) + orders.append( + Order( + agent_id=self.agent_id, + market_id=self.target_market.market_id, + is_buy=False, + kind=LIMIT_ORDER, + volume=order_volume, + price=base_price + price_margin, + ttl=self.order_time_length, + ) + ) + return orders + + def get_base_price(self, markets: List[Market]) -> Optional[float]: + """get base price of markets. + + Args: + markets (List[:class:`pams.Market`]): markets. + + Returns: + float, Optional: average of the max and min prices. + """ + max_buy: float = -float("inf") + for market in markets: + best_buy_price: Optional[float] = market.get_best_buy_price() + if ( + self.is_market_accessible(market_id=market.market_id) + and best_buy_price is not None + ): + max_buy = max(max_buy, best_buy_price) + min_sell: float = float("inf") + for market in markets: + best_sell_price: Optional[float] = market.get_best_sell_price() + if ( + self.is_market_accessible(market_id=market.market_id) + and best_sell_price is not None + ): + min_sell = min(min_sell, best_sell_price) + if max_buy == -float("inf") or min_sell == float("inf"): + return None + return (max_buy + min_sell) / 2.0 diff --git a/pams/agents/market_share_fcn_agent.py b/pams/agents/market_share_fcn_agent.py new file mode 100644 index 00000000..00e218ee --- /dev/null +++ b/pams/agents/market_share_fcn_agent.py @@ -0,0 +1,49 @@ +from typing import List +from typing import Union + +from pams.agents.fcn_agent import FCNAgent +from pams.market import Market +from pams.order import Cancel +from pams.order import Order + + +class MarketShareFCNAgent(FCNAgent): + """Market Share FCN Agent class + + This agent submits orders based on market shares. + This class inherits from the :class:`pams.agents.FCNAgent` class. + """ + + def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: + """submit orders based on FCN-based calculation and market shares. + + .. seealso:: + - :func:`pams.agents.FCNAgent.submit_orders` + """ + filter_markets: List[Market] = [ + market + for market in markets + if self.is_market_accessible(market_id=market.market_id) + ] + if len(filter_markets) == 0: + raise AssertionError("filter_markets in MarketShareFCNAgent is empty.") + weights: List[float] = [] + for market in filter_markets: + weights.append(float(self.get_sum_trade_volume(market=market)) + 1e-10) + return super().submit_orders_by_market( + market=self.get_prng().choices(filter_markets, weights=weights)[0] + ) + + def get_sum_trade_volume(self, market: Market) -> int: + """get sum of trade volume. + + Args: + market (:class:`pams.Market`): trading market. + + Returns: + int: total trade volume. + """ + t: int = market.get_time() + t_start: int = max(0, t - self.time_window_size) + volume: int = sum(market.get_executed_volumes(range(t_start, t + 1))) + return volume diff --git a/pams/market.py b/pams/market.py index 323b865a..3b9d7936 100644 --- a/pams/market.py +++ b/pams/market.py @@ -85,7 +85,7 @@ def setup(self, settings: Dict[str, Any], *args, **kwargs) -> None: # type: ign Args: settings (Dict[str, Any]): market configuration. Usually, automatically set from json config of simulator. This must include the parameters "tickSize" and either "marketPrice" or "fundamentalPrice". - This can include the parameter "outstandingShares". + This can include the parameter "outstandingShares" and "tradeVolume". Returns: None diff --git a/samples/market_share/__init__.py b/samples/market_share/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/samples/market_share/config-mm.json b/samples/market_share/config-mm.json new file mode 100644 index 00000000..98529fd6 --- /dev/null +++ b/samples/market_share/config-mm.json @@ -0,0 +1,72 @@ +{ + "simulation": { + "markets": ["Market-A", "Market-B"], + "agents": ["MarketShareFCNAgents", "MarketMakerAgent"], + "sessions": [ + { "sessionName": 0, + "iterationSteps": 100, + "withOrderPlacement": true, + "withOrderExecution": false, + "withPrint": true + }, + { "sessionName": 1, + "iterationSteps": 2000, + "withOrderPlacement": true, + "withOrderExecution": true, + "withPrint": true, + "maxHifreqOrders": 1 + } + ] + }, + + "Market-A": { + "class": "ExtendedMarket", + "tickSize": 0.00001, + "marketPrice": 300.0, + "outstandingShares": 25000, + + "MEMO": "Required only here", + "tradeVolume": 90 + }, + + "Market-B": { + "class": "ExtendedMarket", + "tickSize": 0.00001, + "marketPrice": 300.0, + "outstandingShares": 25000, + + "MEMO": "Required only here", + "tradeVolume": 10 + }, + + "MarketShareFCNAgents": { + "class": "MarketShareFCNAgent", + "numAgents": 100, + + "MEMO": "Agent class", + "markets": ["Market-A", "Market-B"], + "assetVolume": 50, + "cashAmount": 10000, + + "MEMO": "FCNAgent class", + "fundamentalWeight": {"expon": [1.0]}, + "chartWeight": {"expon": [0.0]}, + "noiseWeight": {"expon": [1.0]}, + "noiseScale": 0.001, + "timeWindowSize": [100, 200], + "orderMargin": [0.0, 0.1] + }, + + "MarketMakerAgent": { + "class": "MarketMakerAgent", + "numAgents": 1, + + "markets": ["Market-B"], + "assetVolume": 50, + "cashAmount": 10000, + + "targetMarket": "Market-B", + "netInterestSpread": 0.02, + "orderTimeLength": 2 + } +} diff --git a/samples/market_share/config.json b/samples/market_share/config.json new file mode 100644 index 00000000..65588532 --- /dev/null +++ b/samples/market_share/config.json @@ -0,0 +1,60 @@ +{ + "simulation": { + "markets": ["Market-A", "Market-B"], + "agents": ["MarketShareFCNAgents"], + "sessions": [ + { "sessionName": 0, + "iterationSteps": 100, + "withOrderPlacement": true, + "withOrderExecution": false, + "withPrint": true + }, + { "sessionName": 1, + "iterationSteps": 2000, + "withOrderPlacement": true, + "withOrderExecution": true, + "withPrint": true, + "maxHifreqOrders": 1 + } + ] + }, + + "Market-A": { + "class": "ExtendedMarket", + "tickSize": 10.0, + "marketPrice": 300.0, + "outstandingShares": 25000, + + "MEMO": "Required only here", + "tradeVolume": 90 + }, + + "Market-B": { + "class": "ExtendedMarket", + "tickSize": 1.0, + "marketPrice": 300.0, + "outstandingShares": 25000, + + "MEMO": "Required only here", + "tradeVolume": 10 + }, + + "MarketShareFCNAgents": { + "class": "MarketShareFCNAgent", + "numAgents": 100, + + "MEMO": "Agent class", + "markets": ["Market-A", "Market-B"], + "assetVolume": 50, + "cashAmount": 10000, + + "MEMO": "FCNAgent class", + "fundamentalWeight": {"expon": [1.0]}, + "chartWeight": {"expon": [0.2]}, + "noiseWeight": {"expon": [1.0]}, + "noiseScale": 0.0001, + "timeWindowSize": [100, 200], + "orderMargin": [0.0, 0.1], + "marginType": "normal" + } +} diff --git a/samples/market_share/main.py b/samples/market_share/main.py new file mode 100644 index 00000000..fd5ba818 --- /dev/null +++ b/samples/market_share/main.py @@ -0,0 +1,43 @@ +import argparse +import random +from typing import Any +from typing import Dict +from typing import Optional + +from pams import Market +from pams.logs.market_step_loggers import MarketStepPrintLogger +from pams.runners.sequential import SequentialRunner + + +class ExtendedMarket(Market): + def setup(self, settings: Dict[str, Any], *args, **kwargs) -> None: # type: ignore # NOQA + super(ExtendedMarket, self).setup(settings, *args, **kwargs) + if "tradeVolume" in settings: + if not isinstance(settings["tradeVolume"], int): + raise ValueError("tradeVolume must be int") + self._executed_volumes = [int(settings["tradeVolume"])] + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--config", "-c", type=str, required=True, help="config.json file" + ) + parser.add_argument( + "--seed", "-s", type=int, default=None, help="simulation random seed" + ) + args = parser.parse_args() + config: str = args.config + seed: Optional[int] = args.seed + + runner = SequentialRunner( + settings=config, + prng=random.Random(seed) if seed is not None else None, + logger=MarketStepPrintLogger(), + ) + runner.class_register(cls=ExtendedMarket) + runner.main() + + +if __name__ == "__main__": + main() diff --git a/tests/pams/agents/test_market_maker_agent.py b/tests/pams/agents/test_market_maker_agent.py new file mode 100644 index 00000000..d1594ed3 --- /dev/null +++ b/tests/pams/agents/test_market_maker_agent.py @@ -0,0 +1,143 @@ +import random +from typing import List +from typing import cast + +import pytest + +from pams import LIMIT_ORDER +from pams import Market +from pams import Order +from pams import Simulator +from pams.agents import MarketMakerAgent +from tests.pams.agents.test_base import TestAgent + + +class TestMarketMakerAgent(TestAgent): + def test_setup(self) -> None: + sim = Simulator(prng=random.Random(4)) + market1 = Market( + market_id=0, prng=random.Random(42), simulator=sim, name="market1" + ) + settings_market = { + "tickSize": 0.01, + "marketPrice": 300.0, + "outstandingShares": 2000, + } + market1.setup(settings=settings_market) + market1._update_time(next_fundamental_price=300.0) + sim._add_market(market=market1) + agent = MarketMakerAgent( + agent_id=1, prng=random.Random(42), simulator=sim, name="test_agent" + ) + settings1 = { + "assetVolume": 50, + "cashAmount": 10000, + "targetMarket": "market1", + "netInterestSpread": 0.05, + "orderTimeLength": 3, + } + agent.setup(settings=settings1, accessible_markets_ids=[0]) + agent = MarketMakerAgent( + agent_id=1, prng=random.Random(42), simulator=sim, name="test_agent" + ) + settings2 = { + "assetVolume": 50, + "cashAmount": 10000, + "netInterestSpread": 0.05, + "orderTimeLength": 3, + } + with pytest.raises(ValueError): + agent.setup(settings=settings2, accessible_markets_ids=[0]) + agent = MarketMakerAgent( + agent_id=1, prng=random.Random(42), simulator=sim, name="test_agent" + ) + settings3 = { + "assetVolume": 50, + "cashAmount": 10000, + "targetMarket": 1, + "netInterestSpread": 0.05, + "orderTimeLength": 3, + } + with pytest.raises(ValueError): + agent.setup(settings=settings3, accessible_markets_ids=[0]) + agent = MarketMakerAgent( + agent_id=1, prng=random.Random(42), simulator=sim, name="test_agent" + ) + settings4 = { + "assetVolume": 50, + "cashAmount": 10000, + "targetMarket": "market1", + "orderTimeLength": 3, + } + with pytest.raises(ValueError): + agent.setup(settings=settings4, accessible_markets_ids=[0]) + + def test_submit_orders(self) -> None: + sim = Simulator(prng=random.Random(4)) + market1 = Market( + market_id=0, prng=random.Random(42), simulator=sim, name="market1" + ) + settings_market = { + "tickSize": 0.01, + "marketPrice": 300.0, + "outstandingShares": 2000, + } + market1.setup(settings=settings_market) + market1._update_time(next_fundamental_price=300.0) + sim._add_market(market=market1) + agent = MarketMakerAgent( + agent_id=1, prng=random.Random(42), simulator=sim, name="test_agent" + ) + settings1 = { + "assetVolume": 50, + "cashAmount": 10000, + "targetMarket": "market1", + "netInterestSpread": 0.05, + "orderTimeLength": 3, + } + agent.setup(settings=settings1, accessible_markets_ids=[0]) + orders = cast(List[Order], agent.submit_orders(markets=[market1])) + for order in orders: + assert isinstance(order, Order) + order_buy = list(filter(lambda x: x.is_buy, orders))[0] + order_sell = list(filter(lambda x: not x.is_buy, orders))[0] + assert order_buy.price == 300.0 * 0.975 + assert order_sell.price == 300.0 * 1.025 + order = Order( + agent_id=0, + market_id=0, + is_buy=True, + kind=LIMIT_ORDER, + volume=1, + placed_at=None, + price=290.0, + order_id=None, + ttl=None, + ) + market1._add_order(order=order) + orders = cast(List[Order], agent.submit_orders(markets=[market1])) + for order in orders: + assert isinstance(order, Order) + order_buy = list(filter(lambda x: x.is_buy, orders))[0] + order_sell = list(filter(lambda x: not x.is_buy, orders))[0] + assert order_buy.price == 300.0 * 0.975 + assert order_sell.price == 300.0 * 1.025 + order = Order( + agent_id=0, + market_id=0, + is_buy=False, + kind=LIMIT_ORDER, + volume=1, + placed_at=None, + price=320.0, + order_id=None, + ttl=None, + ) + market1._add_order(order=order) + orders = cast(List[Order], agent.submit_orders(markets=[market1])) + for order in orders: + assert isinstance(order, Order) + order_buy = list(filter(lambda x: x.is_buy, orders))[0] + order_sell = list(filter(lambda x: not x.is_buy, orders))[0] + assert order_buy.price == 305.0 - 300.0 * 0.025 + assert order_sell.price == 305.0 + 300.0 * 0.025 diff --git a/tests/pams/agents/test_market_share_fcn_agent.py b/tests/pams/agents/test_market_share_fcn_agent.py new file mode 100644 index 00000000..59233306 --- /dev/null +++ b/tests/pams/agents/test_market_share_fcn_agent.py @@ -0,0 +1,63 @@ +import random +from typing import List +from typing import cast + +import pytest + +from pams import Market +from pams import Order +from pams import Simulator +from pams.agents import MarketShareFCNAgent +from tests.pams.agents.test_base import TestAgent + + +class TestMarketShareFCNAgent(TestAgent): + @pytest.mark.parametrize("seed", [1, 42, 100, 200]) + def test_submit_orders(self, seed: int) -> None: + sim = Simulator(prng=random.Random(seed + 1)) + _prng = random.Random(seed) + agent = MarketShareFCNAgent( + agent_id=1, prng=_prng, simulator=sim, name="test_agent" + ) + settings1 = { + "assetVolume": 50, + "cashAmount": 10000, + "fundamentalWeight": 1.0, + "chartWeight": 2.0, + "noiseWeight": 3.0, + "noiseScale": 0.001, + "timeWindowSize": 100, + "orderMargin": 0.1, + "marginType": "fixed", + "meanReversionTime": 200, + } + agent.setup(settings=settings1, accessible_markets_ids=[0, 1, 2]) + market1 = Market( + market_id=0, prng=random.Random(seed - 1), simulator=sim, name="market1" + ) + market1._update_time(next_fundamental_price=300.0) + market2 = Market( + market_id=1, prng=random.Random(seed - 1), simulator=sim, name="market2" + ) + market2._update_time(next_fundamental_price=300.0) + market3 = Market( + market_id=2, prng=random.Random(seed - 1), simulator=sim, name="market3" + ) + market3._update_time(next_fundamental_price=300.0) + market_share_test = [0, 0, 0] + for _ in range(1000): + orders = cast( + List[Order], agent.submit_orders(markets=[market1, market2, market3]) + ) + order = orders[0] + market_share_test[order.market_id] += 1 + assert market_share_test[0] > 300 + assert market_share_test[1] > 300 + assert market_share_test[2] > 300 + + agent2 = MarketShareFCNAgent( + agent_id=1, prng=_prng, simulator=sim, name="test_agent" + ) + agent2.setup(settings=settings1, accessible_markets_ids=[]) + with pytest.raises(AssertionError): + agent2.submit_orders(markets=[market1, market2, market3]) diff --git a/tests/pams/utils/test_class_finder.py b/tests/pams/utils/test_class_finder.py index 6121692f..6d25f966 100644 --- a/tests/pams/utils/test_class_finder.py +++ b/tests/pams/utils/test_class_finder.py @@ -40,6 +40,8 @@ def test_find_class() -> None: find_class(name="HighFrequencyAgent") find_class(name="FCNAgent") find_class(name="ArbitrageAgent") + find_class(name="MarketShareFCNAgent") + find_class(name="MarketMakerAgent") find_class(name="JsonRandom") class DummyAgent(FCNAgent): diff --git a/tests/samples/test_market_share.py b/tests/samples/test_market_share.py new file mode 100644 index 00000000..cf9f34d7 --- /dev/null +++ b/tests/samples/test_market_share.py @@ -0,0 +1,58 @@ +import os.path +import random +from unittest import mock + +import pytest + +from pams import Simulator +from pams.logs import Logger +from samples.market_share.main import ExtendedMarket +from samples.market_share.main import main +from tests.pams.test_market import TestMarket + + +class TestExtendedMarket(TestMarket): + base_class = ExtendedMarket + + def test_setup(self) -> None: + m = self.base_class( + market_id=0, + prng=random.Random(42), + logger=Logger(), + simulator=Simulator(prng=random.Random(42)), + name="test", + ) + m.setup( + settings={ + "tickSize": 0.001, + "outstandingShares": 100, + "marketPrice": 300.0, + "fundamentalPrice": 500.0, + "tradeVolume": 100, + } + ) + with pytest.raises(ValueError): + m.setup( + settings={ + "tickSize": 0.001, + "outstandingShares": 100, + "marketPrice": 300.0, + "fundamentalPrice": 500.0, + "tradeVolume": "error", + } + ) + + +def test_market_share() -> None: + root_dir: str = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + + sample_dir = os.path.join(root_dir, "samples", "market_share") + with mock.patch( + "sys.argv", ["main.py", "--config", f"{sample_dir}/config.json", "--seed", "1"] + ): + main() + with mock.patch( + "sys.argv", + ["main.py", "--config", f"{sample_dir}/config-mm.json", "--seed", "1"], + ): + main()