{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "578f363a-0fca-4e36-a253-384692b313cf",
   "metadata": {},
   "source": [
    "# CRUCIAL Agora example trading code"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2b602ae3-9838-414f-834a-a8e599612441",
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "import random\n",
    "import os\n",
    "import shutil\n",
    "import subprocess\n",
    "\n",
    "import numpy as np\n",
    "import scipy as sp\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "daf720c8-0fe9-4e96-96c2-a66b0e59bf59",
   "metadata": {},
   "source": [
    "## Setting up API authentication"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bdeb6015-2578-4d14-bc2c-f3f6b697c227",
   "metadata": {},
   "source": [
    "Download the `agora-client` executable for your platform and put it in the same folder as this notebook."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "64b0ca41-bf68-4132-ac72-8ff8cff13218",
   "metadata": {},
   "source": [
    "On the CRUCIAL Agora platform, go to the Profile page.\n",
    "\n",
    "Click the 'Download biscuit' button, and put the downloaded file in the same folder as this notebook.\n",
    "\n",
    "Set the variable below to the filename."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "525b9615-d551-4de7-b388-290b13c7eb08",
   "metadata": {},
   "outputs": [],
   "source": [
    "biscuit_filename = \"agora_biscuit_2020-01-01T00_00_00.000Z.txt\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ad7ecc5a-5f51-4cbf-ba7e-2b78b86b42dd",
   "metadata": {},
   "source": [
    "We use the `agora-client` executable to generate a public/private key pair, and display the public key.\n",
    "\n",
    "If you have already generated a key pair, it will show the one it previously created"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "780cd2ef-76a6-4832-bae4-44894a5be9c9",
   "metadata": {},
   "source": [
    "Linux specific:\n",
    "\n",
    "Setting this environment variable causes `agora-client` to save the key pair in `~/.config/agora-client/`.  Otherwise `agora-client` will use Linux's \"secret storage service\", which I struggled to get working"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4314a94e-6f6d-4595-9e6e-1b16fad9ed61",
   "metadata": {},
   "outputs": [],
   "source": [
    "os.environ[\"AGORA_FORCE_PRIVATE_KEY_FILE\"] = \"true\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "47a1b30d-99d9-4979-95f4-af3a77085bb6",
   "metadata": {},
   "source": [
    "Mac specific:\n",
    "Your OS may prevent `agora-client` from running, and display a message \"Apple cannot verify agora-client is free of malware...\"\n",
    "\n",
    "To allow `agora-client` to run in the future, go to System Settings > Privacy & Security, then in the Security section click 'Open Anyway'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fee47eff-b5ea-4c2a-9b55-0dd25ea11671",
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    ans = subprocess.check_output([\"./agora-client\", \"--biscuit-file\", biscuit_filename, \"pubkey\", \"new\"], text=True)\n",
    "    print(ans)\n",
    "except subprocess.CalledProcessError as e:\n",
    "    print(f\"Command failed: {e.returncode}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "28459a2e-dccc-42db-afc9-6790f8b255d3",
   "metadata": {},
   "source": [
    "On the Profile page, copy the above string (starting with `ed25519/`) below the instruction to \"Enter public key...\", then press the \"Add public key\" button.\n",
    "\n",
    "If this is successful then the new public key should appear, with an option to Expire key.  If this does not happen, it was unsuccessful (see below)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "40648279-8c58-47a0-af03-96591b10b633",
   "metadata": {},
   "source": [
    "#### Note\n",
    "If you have done this before and your login session has since expired on the Agora platform (roughly every 7 days), you will need to remove the existing public/private key pair from your system and generate a new one using the code above."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b0447f20-b9a7-4bf3-b172-ad1639e43f19",
   "metadata": {},
   "source": [
    "For Linux: in the next box we define a function that deletes the key pair (but we do not run it)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24cd6690-319a-4518-a953-4a8a25667818",
   "metadata": {},
   "outputs": [],
   "source": [
    "def delete_existing_key_pair():\n",
    "    agora_pubkey_folder = os.path.join(os.path.expanduser('~'), \".config\", \"agora-client\", \"api.crucialab.net\")\n",
    "    shutil.rmtree(agora_pubkey_folder)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cab72332-295e-4027-a910-f10cfc0c1577",
   "metadata": {},
   "source": [
    "If you want to run this, uncomment and run the following line of code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cb337d35-c6c6-4c8e-823b-1810fbfcc406",
   "metadata": {},
   "outputs": [],
   "source": [
    "# delete_existing_key_pair()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7ae99098-909b-4311-a2d6-b9c4aa896a96",
   "metadata": {},
   "source": [
    "For Mac you will need to delete the saved key pair on your system."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c823bda1-9e9e-4ea4-82c5-479e13ca2b41",
   "metadata": {},
   "source": [
    "## Market authentication test"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d3e742d3-95de-4438-878d-895f8cd75c21",
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    ans = subprocess.check_output([\"./agora-client\", \"--biscuit-file\", biscuit_filename, \"get\", \"me\"], text=True)\n",
    "    print(ans)\n",
    "except subprocess.CalledProcessError as e:\n",
    "    print(f\"Command failed: {e.returncode}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "63235539-e47a-4109-8512-a2a8c70f7ee9",
   "metadata": {},
   "source": [
    "If things are set up correctly, the above code should show your user ID and the email address you log in to the Agora platform with."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0fff1a5e-c8f7-4999-be19-b9c9718812db",
   "metadata": {},
   "source": [
    "## List visible markets\n",
    "The below code lists the markets visible to the user"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2996c5d8-5179-45a8-bb2f-cd2d01575bc4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def view_all_markets():\n",
    "    try:\n",
    "        ans = subprocess.check_output([\"./agora-client\", \"--biscuit-file\", biscuit_filename, \"get\", \"market\"], text=True)\n",
    "    except subprocess.CalledProcessError as e:\n",
    "        print(f\"Command failed: {e.returncode}\")\n",
    "    return json.loads(ans)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "31c9eaee-693d-4a90-ae2c-e08c3e4ab4b0",
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "view_all_markets()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41d9351c-965d-4dcd-80d9-8d11ef60f749",
   "metadata": {},
   "source": [
    "## Get prices from a market"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e8a1ef5e-509a-493c-be4d-d05277dc6410",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_market_prices(market_id):\n",
    "    try:\n",
    "        ans = subprocess.check_output([\"./agora-client\", \"--biscuit-file\", biscuit_filename, \"get\", \"price\", \"--market-id\", str(market_id)], text=True)\n",
    "        return ans\n",
    "    except subprocess.CalledProcessError as e:\n",
    "        print(f\"Command failed: {e.returncode}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ab2df261-732e-4e9c-a979-2d6bae1c9203",
   "metadata": {},
   "source": [
    "We will get the prices from the demo \"2050 CO2 concentration\" market, which has ID 106"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6f78a798-f88d-48a0-93f1-950e45194277",
   "metadata": {},
   "outputs": [],
   "source": [
    "MKT_ID = 106\n",
    "market_price_json = json.loads(get_market_prices(MKT_ID))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "681a12c1-f9ce-4a62-8b11-278e992b94c4",
   "metadata": {},
   "source": [
    "The order these are returned in is random, but if we sort by `outcome_id` these end up in order from left to right.\n",
    "\n",
    "_This behaviour might not be guaranteed?_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "28adcd09-a4aa-400f-87a4-6d12e00fd605",
   "metadata": {},
   "outputs": [],
   "source": [
    "market_price_json = sorted(market_price_json, key=lambda x: x['outcome_id'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a83fa818-e9ed-4061-b07e-fd0b37220a47",
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "market_price_json"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "50eb5405-a663-4f01-9de2-b05b294b9b72",
   "metadata": {},
   "outputs": [],
   "source": [
    "market_prices = np.array([foo['price'] for foo in market_price_json])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84412df6-32cb-4197-bf5b-1d450013ef3e",
   "metadata": {},
   "source": [
    "`market_prices` is a NumPy array of the current market prices in the specified market"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "81e6ca4c-21b8-4b1d-9223-91d87be15ce5",
   "metadata": {},
   "outputs": [],
   "source": [
    "market_prices"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "835a56c4-5e3c-4433-a7c4-54d34ced5518",
   "metadata": {},
   "source": [
    "## Constructing our own fair prices\n",
    "\n",
    "Recall that each unit of an outcome pays 1 credit if the outcome occurs, and 0 if it doesn't.  The price of an outcome therefore represents a market consensus on the probability of an outcome occurring.\n",
    "\n",
    "Your fair prices are what you, an expert participant, think the probability is for each outcome in the market.\n",
    "\n",
    "For this example, we will create some artificial fair prices.  However, for a real market, you would likely construct these somewhere else and save them to a file."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "07dad70b-18b5-4957-a116-f560a8255857",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_pretend_CO2_fair_prices():\n",
    "    \"\"\"\n",
    "    We use a normal distribution with a randomly-generated mean and standard deviation.\n",
    "\n",
    "    This function saves the prices out to file at fake_CO2_prices.txt\n",
    "    \"\"\"\n",
    "    MEAN = random.uniform(400, 700)\n",
    "    STDEV = random.uniform(50, 100)\n",
    "    print(f\"Generating fair prices for normal distribution with mean {MEAN:.1f} and stdev {STDEV:.1f}\")\n",
    "\n",
    "    nd = sp.stats.norm(loc=MEAN, scale=STDEV)\n",
    "    cdfs = [0.0]\n",
    "    for foo in range(300, 810, 50):\n",
    "        cdfs.append(nd.cdf(foo))\n",
    "    cdfs.append(1.0)\n",
    "    cdfs = np.array(cdfs)\n",
    "\n",
    "    prices = cdfs[1:] - cdfs[:-1]\n",
    "    np.savetxt(\"fake_CO2_prices.txt\", prices)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11e77bcc-181e-4da7-b98d-2688bbf3cc59",
   "metadata": {},
   "outputs": [],
   "source": [
    "generate_pretend_CO2_fair_prices()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c53270d-b2a3-4471-b825-b5fa0cd5231d",
   "metadata": {},
   "source": [
    "We now load in the fair prices that the function saved.  For a real market, you would load in your own fair prices instead."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "52434110-9abc-4d3c-8fb9-c30f1bbf03e5",
   "metadata": {},
   "outputs": [],
   "source": [
    "fair_prices = np.loadtxt(\"fake_CO2_prices.txt\")\n",
    "print(fair_prices)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a76557ca-8106-4153-b6bd-0d97927e3698",
   "metadata": {},
   "source": [
    "## Plotting fair prices and current market prices"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "29e9e421-0023-4f87-803c-550f2461172b",
   "metadata": {},
   "outputs": [],
   "source": [
    "xvals = np.arange(275, 825.01, 50)\n",
    "plt.plot(xvals, market_prices, label=\"market prices\")\n",
    "plt.plot(xvals, fair_prices, label=\"fair prices\")\n",
    "plt.legend()\n",
    "plt.xlabel(\"CO2 concentration\")\n",
    "plt.ylabel(\"Price\")\n",
    "plt.grid()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "86fb23fd-da3c-4aaf-9462-f637977bcd8c",
   "metadata": {},
   "source": [
    "## Preparing a trade using the market prices and our fair prices"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf7eb1d7-8724-4301-b1c6-1d9c9d0ce123",
   "metadata": {},
   "source": [
    "Given an array of current market prices and an array of our fair prices, how should we act?\n",
    "\n",
    "A reasonable starting point is to make a trade that maximises expected value (according to our fair prices) for the amount of credits that we are willing to spend.\n",
    "\n",
    "There are caveats with this approach, which we will discuss later, but this is what we will implement in code."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "495cb778-1f3d-4d4f-bc61-e3be33152e8c",
   "metadata": {},
   "source": [
    "To aid us, we will define some functions that implement the market pricing algorithm.  For more details on this, see the AGORA Trading Guide."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b5ed3722-ad31-41cb-921a-1ebeb63b2eaa",
   "metadata": {},
   "outputs": [],
   "source": [
    "def cost(b, q):\n",
    "    \"\"\"\n",
    "    C(q) as defined in the Trading Guide, where q is the array of exposure of the AMM to the market outcomes.\n",
    "\n",
    "    This is calculated as b*log(sum_i(e^(q_i/b))).\n",
    "    \n",
    "    This implementation includes the extra step from CRUCIAL's R example code to avoid overflow effects when q/b is large.\n",
    "    \"\"\"\n",
    "    x = q/b\n",
    "    x_max = x.max()\n",
    "    x -= x_max\n",
    "    return b * (x_max + np.log(np.exp(x).sum()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7339e175-0a57-4e54-9c64-3c688b27ad48",
   "metadata": {},
   "outputs": [],
   "source": [
    "def prices_to_q(b, pri):\n",
    "    \"\"\"\n",
    "    Converts an array of prices into q, the array of exposure of the AMM to outcomes.\n",
    "\n",
    "    Note that this can only be done up to a constant, since a participant buying 1 of each outcome does not change the prices.\n",
    "\n",
    "    This function assumes the lowest-priced outcome has q_i = 0.\n",
    "    \"\"\"\n",
    "    return b*np.log(pri/pri.min())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6d91ed5f-cd85-4354-9cb0-c08004e98781",
   "metadata": {},
   "source": [
    "We define a function that retrieves the liquidity factor for a specific market.  The liquidity factor determines how quickly the price moves in response to outcomes being bought (larger liquidity factor = less price movement)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2b0a6a64-9ec3-47c7-8288-5f7ec66199f9",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_liquidity_factor(mkt_id):\n",
    "    \"\"\"Returns the liquidity factor (b) for the appropriate market\"\"\"\n",
    "    all_market_info = view_all_markets()\n",
    "    for market in all_market_info:\n",
    "        if market['market_id'] == mkt_id:\n",
    "            return market['liquidity_factor']\n",
    "    raise RuntimeError(f\"Market ID {mkt_id} not found\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7fd5d55-2d87-461a-b593-c5c918f4bf4c",
   "metadata": {},
   "source": [
    "The `make_trade_amounts` algorithm produces trade quantities that maximise expected value for the given budget spend.\n",
    "\n",
    "Intuitively, this is equivalent to greedily buying the outcomes with the largest ratio of (fair price / current price) until the budget is used up."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "67ea1b10-e043-4fdf-b27b-7581bf2b4b8c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_trade_amounts(b, current_prices, fair_prices, budget):\n",
    "    \"\"\"\n",
    "    Implementing the optimal-EV trade algorithm as in CRUCIAL's R example code\n",
    "\n",
    "    b: liquidity factor of the market\n",
    "    current_prices: current market prices\n",
    "    fair_prices: your model prices\n",
    "    budget: max budget to spend\n",
    "    \"\"\"\n",
    "    assert np.isclose(current_prices.sum(), 1.0), \"Current prices must sum to 1\"\n",
    "    assert np.isclose(fair_prices.sum(), 1.0), \"Fair prices must sum to 1\"\n",
    "    assert fair_prices.min() > 0.0, \"Fair prices must be strictly positive\"\n",
    "\n",
    "    q_current = prices_to_q(b, current_prices)\n",
    "    q_target = prices_to_q(b, fair_prices)\n",
    "    delta_q = q_target - q_current\n",
    "    delta_q -= delta_q.min()  # make non-negative\n",
    "\n",
    "    full_cost = cost(b, q_current + delta_q) - cost(b, q_current)\n",
    "\n",
    "    def fn(offset):\n",
    "        \"\"\"Function used by the root-finding algorithm.  The desired root is when this function outputs 0.\n",
    "\n",
    "        The input scalar 'offset' is subtracted from every outcome, with a floor of 0 imposed\n",
    "        \"\"\"\n",
    "        w = delta_q - offset\n",
    "        w[w < 0.0] = 0.0\n",
    "        return cost(b, q_current + w) - cost(b, q_current) - budget\n",
    "\n",
    "    if budget < full_cost:\n",
    "        sol = sp.optimize.root_scalar(fn, bracket=[0, delta_q.max()])\n",
    "        if not sol.converged:\n",
    "            raise RuntimeError(f\"Root finding routine did not converge. {sol}\")\n",
    "        w = delta_q - sol.root\n",
    "        w[w < 0.0] = 0.0\n",
    "    else:\n",
    "        w = delta_q\n",
    "\n",
    "    w = w.astype(np.int32)  # buy quantities must be integers\n",
    "\n",
    "    actual_cost = cost(b, q_current + w) - cost(b, q_current)\n",
    "    print(f\"Actual cost: {actual_cost:.3f}\")\n",
    "    return w"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "15e6c550-f148-4949-aab9-f363e5e7923d",
   "metadata": {},
   "source": [
    "Let's see what quantities are proposed if we are willing to spend 20 credits on the demo market..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "02264f61-0d0c-40ae-ae37-d5190d4752bb",
   "metadata": {},
   "outputs": [],
   "source": [
    "BUDGET = 20.0\n",
    "trade_quantities = make_trade_amounts(get_liquidity_factor(MKT_ID),\n",
    "                                      market_prices,\n",
    "                                      fair_prices,\n",
    "                                      BUDGET)\n",
    "print(trade_quantities)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e1e42016-bd2b-43be-a149-06487d5c53fc",
   "metadata": {},
   "source": [
    "## Performing the trade on the market"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "192a66a4-95eb-4710-ba16-d3f8206468e2",
   "metadata": {},
   "source": [
    "We now want to buy these quantities of the outcomes on the market.\n",
    "\n",
    "On the CRUCIAL AGORA platform, we don't buy/sell outcomes directly.  Instead we must create contracts covering one or more outcomes, and buy/sell contracts.\n",
    "\n",
    "Since the `make_trade_amounts` function proposes different quantities of each outcome, we will make separate contracts covering one outcome each."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dbd92512-6c28-4625-8a1d-47a641ab69c6",
   "metadata": {},
   "source": [
    "A simple version of this script could simply make the contracts and then buy them.  This has the downside that, if we were to run the script several times, multiple contracts covering the same outcome would be generated, which is quite unwieldy.\n",
    "\n",
    "We will introduce some extra complexity by fetching the current contracts held by the user on the market.  If we have previously made a relevant contract then we will re-use it."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f872601-4145-4853-84ba-a7a2a4efdbcb",
   "metadata": {},
   "source": [
    "### Defining more utility functions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "127ed482-e083-4dde-9945-b27d0837b9aa",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_outcomes(market_id):\n",
    "    \"\"\"\n",
    "    This is a utility function that, for a given market, generates a dictionary\n",
    "    mapping outcome IDs to reasonable contract names.  This is used when creating\n",
    "    contracts to trade.\n",
    "    \"\"\"\n",
    "    mp_json = json.loads(get_market_prices(market_id))\n",
    "    mp_json = sorted(mp_json, key=lambda x: x['outcome_id'])\n",
    "    ids2names = {}\n",
    "    for x in mp_json:\n",
    "        onames = []\n",
    "        # x['values'] has length 1 for 1D markets, 2 for 2D markets\n",
    "        for xval in x['values']:\n",
    "            # The variable can be either categorical or continuous\n",
    "            if isinstance(xval, str):\n",
    "                # categorical\n",
    "                onames.append(xval)\n",
    "            elif isinstance(xval, list):\n",
    "                # continuous\n",
    "                if xval[0] is None:\n",
    "                    oname = f\"Under {xval[1]}\"\n",
    "                elif xval[1] is None:\n",
    "                    oname = f\"Over {xval[0]}\"\n",
    "                else:\n",
    "                    oname = f\"{xval[0]} to {xval[1]}\"\n",
    "                onames.append(oname)\n",
    "            else:\n",
    "                raise Exception(f\"Unsure how to handle x['values'][0] of type {type(xval)}\")\n",
    "        # for 1D markets, use the given name for the contract\n",
    "        # for 2D markets, insert a comma between the given sub-names\n",
    "        ids2names[x['outcome_id']] = \"XYZ \" + \", \".join(onames)\n",
    "    return ids2names"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0ce4aa86-6ff5-4994-9633-31df6c629f3e",
   "metadata": {},
   "outputs": [],
   "source": [
    "id2cname = generate_outcomes(MKT_ID)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "248094ee-23f1-498a-ba7d-ac11c9e52ae7",
   "metadata": {},
   "outputs": [],
   "source": [
    "id2cname"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0cdc0a2b-2976-463a-853d-4115efeace3e",
   "metadata": {},
   "outputs": [],
   "source": [
    "def convert_value_to_tuple(value):\n",
    "    \"\"\"\n",
    "    Agora outcome 'values' are (nested) lists, which are not valid dictionary keys\n",
    "    in Python.  This function turns them into (nested) tuples.\n",
    "    \"\"\"\n",
    "    return tuple(tuple(foo) if isinstance(foo, list) else foo for foo in value)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "71cd774e-fc8d-457b-84e7-f84f88ef19c0",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_values_to_ids(market_id):\n",
    "    \"\"\"\n",
    "    Utility function returning a dictionary mapping 'tupled' values to outcome IDs\n",
    "    \"\"\"\n",
    "    mp_json = json.loads(get_market_prices(market_id))\n",
    "    mp_json = sorted(mp_json, key=lambda x: x['outcome_id'])\n",
    "    values2ids = {}\n",
    "    for x in mp_json:\n",
    "        tuple_value = convert_value_to_tuple(x['values'])\n",
    "        values2ids[tuple_value] = x['outcome_id']\n",
    "    return values2ids"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2be8724a-eca6-45ac-b009-54cdfe5b56e3",
   "metadata": {},
   "outputs": [],
   "source": [
    "values2ids = generate_values_to_ids(MKT_ID)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84e52e17-0a66-4cea-92d3-4c0bfedccaa8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# values2ids"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1a9cae8b-69e7-4135-a4ee-546ce71d8730",
   "metadata": {},
   "outputs": [],
   "source": [
    "outcome_ids = [foo['outcome_id'] for foo in market_price_json]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e34902d8-9bb2-4be5-a0fb-1a01520fa5af",
   "metadata": {},
   "outputs": [],
   "source": [
    "# outcome_ids"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e8235ee3-144b-4c86-a2c8-9dfaac013fee",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_contracts(market_id):\n",
    "    \"\"\"Function to get contracts that have been defined on a given market\"\"\"\n",
    "    try:\n",
    "        ans = subprocess.check_output([\"./agora-client\", \"--biscuit-file\", biscuit_filename, \"get\", \"contract\", \"--market-id\", str(market_id)], text=True)\n",
    "        return json.loads(ans)\n",
    "    except subprocess.CalledProcessError as e:\n",
    "        print(f\"Command failed: {e.returncode}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0e2d6d5d-5c8c-4192-9353-d31e93a756ae",
   "metadata": {},
   "outputs": [],
   "source": [
    "existing_contracts = get_contracts(MKT_ID)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fce42b0c-f8e8-42ad-a929-923c64b7f799",
   "metadata": {},
   "outputs": [],
   "source": [
    "existing_contracts"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "65f9c483-3cd1-4acb-bde9-fdbd17b73467",
   "metadata": {},
   "outputs": [],
   "source": [
    "contracts_to_use = {}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6bc784e9-8b0a-41ed-a44c-2a051d1dc0b9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# loop over existing contracts; if we can use it instead of creating a new contract\n",
    "# then add to contracts_to_use dictionary\n",
    "for contract in existing_contracts:\n",
    "    if len(contract['outcomes']) > 1:\n",
    "        print(f\"contract_id {contract['contract_id']} has more than 1 outcome, skipping...\")\n",
    "        continue\n",
    "    tupled_outcome = convert_value_to_tuple(contract['outcomes'][0])\n",
    "    outcome_id = values2ids[tupled_outcome]\n",
    "    if contract['contract'] == id2cname[outcome_id]:\n",
    "        contracts_to_use[outcome_id] = contract['contract_id']\n",
    "        print(f\"contract_id {contract['contract_id']} corresponds to '{id2cname[outcome_id]}', reusing...\")\n",
    "    else:\n",
    "        print(f\"contract_id {contract['contract_id']} has name '{contract['contract']}' not '{id2cname[outcome_id]}', skipping...\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ea42c367-6cae-470d-9c46-937c2e329938",
   "metadata": {},
   "outputs": [],
   "source": [
    "contracts_to_use"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d5816b17-ed4b-4fc4-9c5f-16fb4d28e198",
   "metadata": {},
   "source": [
    "Any other outcomes that we want to buy, we must create contracts for them"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "782a97d3-a855-4154-bf3b-0caaca70d3ab",
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_contract(market_id, contract_name, outcome_ids):\n",
    "    \"\"\"Function to create contract covering the provided outcome IDs\"\"\"\n",
    "    contract = {'contract': contract_name,\n",
    "                'market_id': market_id,\n",
    "                'outcomes': outcome_ids}\n",
    "    contract_json = json.dumps(contract)\n",
    "    try:\n",
    "        ans = subprocess.check_output([\"./agora-client\", \"--biscuit-file\", biscuit_filename, \"post\", \"contract\"], input=contract_json, text=True)\n",
    "        return json.loads(ans)\n",
    "    except subprocess.CalledProcessError as e:\n",
    "        print(f\"Command failed: {e.returncode}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b1f582f5-54fa-4c61-9bb1-e9705dc494f7",
   "metadata": {},
   "outputs": [],
   "source": [
    "for oid, quantity in zip(outcome_ids, trade_quantities):\n",
    "    if quantity == 0:\n",
    "        # no need to create contract\n",
    "        continue\n",
    "    fn_out = create_contract(MKT_ID, id2cname[oid], [oid])\n",
    "    print(f\"Created contract for outcome '{id2cname[oid]}'\")\n",
    "    contracts_to_use[oid] = fn_out[0]['contract_id']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "97f723b6-6703-4742-80cd-9a1a686be2e3",
   "metadata": {},
   "outputs": [],
   "source": [
    "contracts_to_use"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc49774b-c026-47dc-a77a-9eba8cda4dd4",
   "metadata": {},
   "source": [
    "Now we are finally ready to make the trade...\n",
    "\n",
    "Firstly we define a function to interact with the market, but we don't execute it yet"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0585717a-5ed3-4c3b-bb57-b4083e7d1e6d",
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_trade(market_id, trades):\n",
    "    \"\"\"\n",
    "    Function to send trade to the market.\n",
    "    \n",
    "    market_id is the market ID\n",
    "    trades is a list of (contract_id, amount to buy or sell) tuples\n",
    "    \"\"\"\n",
    "    trade_list = []\n",
    "    for trade in trades:\n",
    "        cid, amount = trade\n",
    "        trade_list.append({'contract_id': cid,\n",
    "                           'contracts': amount,\n",
    "                           'market_id': market_id})\n",
    "    trade_json = json.dumps(trade_list)\n",
    "    try:\n",
    "        ans = subprocess.check_output([\"./agora-client\", \"--biscuit-file\", biscuit_filename, \"post\", \"execution\"], input=trade_json, text=True)\n",
    "        return json.loads(ans)\n",
    "    except subprocess.CalledProcessError as e:\n",
    "        print(f\"Command failed: {e.returncode}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ab2c151a-02af-47c9-8fcd-c4fbd3cae5da",
   "metadata": {},
   "outputs": [],
   "source": [
    "trades_to_make = []\n",
    "for oid, quantity in zip(outcome_ids, trade_quantities):\n",
    "    if quantity == 0:\n",
    "        continue\n",
    "    trades_to_make.append((contracts_to_use[oid], int(quantity)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bc6605a1-97a4-463e-93d8-dc0a70cac89b",
   "metadata": {},
   "outputs": [],
   "source": [
    "trades_to_make"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "016035c6-ac79-4288-9f9e-b265740b0694",
   "metadata": {},
   "source": [
    "This is a list of (contract ID, amount to buy) for the trade we are about to make\n",
    "\n",
    "## Execute the below line of code to send the trade to the market."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1083396d-e730-4e36-93d4-47ee058a64eb",
   "metadata": {},
   "outputs": [],
   "source": [
    "make_trade(MKT_ID, trades_to_make)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37a94bb8-b6af-461b-9e46-ef8e0cab0ee3",
   "metadata": {},
   "source": [
    "The function output gives information about the execution of the trade."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9cbf9eaa-6e28-48e4-9fa9-bd97a775b7ab",
   "metadata": {},
   "source": [
    "### A more condensed version would be..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2f5145ac-d66c-4d09-8900-b8010c2b3884",
   "metadata": {},
   "outputs": [],
   "source": [
    "# biscuit_filename = \"...\"\n",
    "# os.environ[\"AGORA_FORCE_PRIVATE_KEY_FILE\"] = \"true\"\n",
    "# MKT_ID = ...\n",
    "# BUDGET = ...\n",
    "\n",
    "# market_price_json = json.loads(get_market_prices(MKT_ID))\n",
    "# market_price_json = sorted(market_price_json, key=lambda x: x['outcome_id'])\n",
    "# market_prices = np.array([foo['price'] for foo in market_price_json])\n",
    "\n",
    "# fair_prices = np.loadtxt(\"...\")\n",
    "\n",
    "# trade_quantities = make_trade_amounts(get_liquidity_factor(MKT_ID), market_prices, fair_prices, BUDGET)\n",
    "\n",
    "# id2cname = generate_outcomes(MKT_ID)\n",
    "# values2ids = generate_values_to_ids(MKT_ID)\n",
    "# outcome_ids = [foo['outcome_id'] for foo in market_price_json]\n",
    "# existing_contracts = get_contracts(MKT_ID)\n",
    "\n",
    "# contracts_to_use = {}\n",
    "# # loop over existing contracts; if we can use it instead of creating a new contract\n",
    "# for contract in existing_contracts:\n",
    "#     if len(contract['outcomes']) > 1:\n",
    "#         print(f\"contract_id {contract['contract_id']} has more than 1 outcome, skipping...\")\n",
    "#         continue\n",
    "#     tupled_outcome = convert_value_to_tuple(contract['outcomes'][0])\n",
    "#     outcome_id = values2ids[tupled_outcome]\n",
    "#     if contract['contract'] == id2cname[outcome_id]:\n",
    "#         contracts_to_use[outcome_id] = contract['contract_id']\n",
    "#         print(f\"contract_id {contract['contract_id']} corresponds to '{id2cname[outcome_id]}', reusing...\")\n",
    "#     else:\n",
    "#         print(f\"contract_id {contract['contract_id']} has name '{contract['contract']}' not '{id2cname[outcome_id]}', skipping...\")\n",
    "\n",
    "# for oid, quantity in zip(outcome_ids, trade_quantities):\n",
    "#     if quantity == 0:\n",
    "#         # no need to create contract\n",
    "#         continue\n",
    "#     fn_out = create_contract(MKT_ID, id2cname[oid], [oid])\n",
    "#     print(f\"Created contract for outcome '{id2cname[oid]}'\")\n",
    "#     contracts_to_use[oid] = fn_out[0]['contract_id']\n",
    "\n",
    "# trades_to_make = []\n",
    "# for oid, quantity in zip(outcome_ids, trade_quantities):\n",
    "#     if quantity == 0:\n",
    "#         continue\n",
    "#     trades_to_make.append((contracts_to_use[oid], int(quantity)))\n",
    "\n",
    "# make_trade(MKT_ID, trades_to_make)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "781bea7c-0489-4686-8f84-e38b67ab4b42",
   "metadata": {},
   "source": [
    "# Closing comments"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f23148dd-a859-42d0-92b4-c826a1b1f1dd",
   "metadata": {},
   "source": [
    "## 1. Fair prices\n",
    "\n",
    "Advice: don't blindly trust your model that creates your fair prices.\n",
    "\n",
    "If they are wildly different to the market prices, either you are a genius and can make a lot of money.  Or your code is wrong or you have misunderstood what you are trading on.  Tread carefully!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25348060-a03c-41b7-95d0-9a668300ae6b",
   "metadata": {},
   "source": [
    "## 2. Risk / credit allocation\n",
    "\n",
    "Maximising expected value is okay when you are only trading small fractions of your balance, but in general you may want to take a more nuanced approach.\n",
    "\n",
    "Informally, it's unwise to make trades where there's a meaningful chance of losing a large proportion of your balance, even if the upside is large.  You may want to read up on concepts like the \"Kelly criterion\"."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d8fa356b-5bab-4a1c-82aa-52a3a1ed4112",
   "metadata": {},
   "source": [
    "## 3. Selling\n",
    "\n",
    "In this tutorial we defined a function that put credits into the market.  You may also want to sell over-priced holdings.\n",
    "\n",
    "If you want to implement this, the utility functions in this notebook may be useful.  The `get_portfolio()` function may also be helpful: this returns how many of each contract you hold (although unfortunately this cannot be restricted to a specific market)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3e6e0e0b-05c7-4d64-816b-5b9fb7d1549b",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_portfolio():\n",
    "    \"\"\"Function to get account holdings from CRUCIAL AGORA\"\"\"\n",
    "    try:\n",
    "        ans = subprocess.check_output([\"./agora-client\", \"--biscuit-file\", biscuit_filename, \"get\", \"portfolio\"], text=True)\n",
    "        return json.loads(ans)\n",
    "    except subprocess.CalledProcessError as e:\n",
    "        print(f\"Command failed: {e.returncode}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d5d64a87-9830-4f9f-a509-c60a65b42a0d",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_balance():\n",
    "    \"\"\"Function to get account balance from CRUCIAL AGORA\"\"\"\n",
    "    try:\n",
    "        ans = subprocess.check_output([\"./agora-client\", \"--biscuit-file\", biscuit_filename, \"get\", \"balance\"], text=True)\n",
    "        return json.loads(ans)\n",
    "    except subprocess.CalledProcessError as e:\n",
    "        print(f\"Command failed: {e.returncode}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "159771b8-66b0-4caf-8cea-ef03d13695e0",
   "metadata": {},
   "source": [
    "## 4. 2D markets\n",
    "\n",
    "This code works also works for 2D markets.  You will likely want to pass back and forth between a 2D array and 1D flattened array, using NumPy's `.flatten()` and `.reshape()`.  The `market_prices` produced by this code are a 1D flattened array."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0e3dc992-209c-4243-a7bf-8b9545912711",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "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.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
