Functional Weave
Code in Python

invest.rebalance@1.0.1

vectors.json

18,425 bytes · view raw

[
 {
  "name": "sell half of one fund to buy another",
  "args": [
   [
    {
     "id": "A",
     "units": 10,
     "unitPrice": {
      "minor": 1000,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    },
    {
     "id": "B",
     "units": 0,
     "unitPrice": {
      "minor": 500,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expect": {
   "trades": [
    {
     "id": "A",
     "units": -5,
     "value": {
      "minor": -5000,
      "currency": "GBP"
     },
     "unitsAfter": 5,
     "valueAfter": {
      "minor": 5000,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 5000
    },
    {
     "id": "B",
     "units": 10,
     "value": {
      "minor": 5000,
      "currency": "GBP"
     },
     "unitsAfter": 10,
     "valueAfter": {
      "minor": 5000,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 5000
    }
   ],
   "totalValue": {
    "minor": 10000,
    "currency": "GBP"
   },
   "bought": {
    "minor": 5000,
    "currency": "GBP"
   },
   "sold": {
    "minor": 5000,
    "currency": "GBP"
   },
   "cashAfter": {
    "minor": 0,
    "currency": "GBP"
   }
  }
 },
 {
  "name": "invest cash in whole units, leaving the odd penny",
  "args": [
   [
    {
     "id": "A",
     "units": 0,
     "unitPrice": {
      "minor": 333,
      "currency": "GBP"
     },
     "targetBasisPoints": 10000
    }
   ],
   {
    "minor": 1000,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expect": {
   "trades": [
    {
     "id": "A",
     "units": 3,
     "value": {
      "minor": 999,
      "currency": "GBP"
     },
     "unitsAfter": 3,
     "valueAfter": {
      "minor": 999,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 9990
    }
   ],
   "totalValue": {
    "minor": 1000,
    "currency": "GBP"
   },
   "bought": {
    "minor": 999,
    "currency": "GBP"
   },
   "sold": {
    "minor": 0,
    "currency": "GBP"
   },
   "cashAfter": {
    "minor": 1,
    "currency": "GBP"
   }
  }
 },
 {
  "name": "two buys both round up to 2 units; the first is trimmed so the cash covers them",
  "args": [
   [
    {
     "id": "A",
     "units": 0,
     "unitPrice": {
      "minor": 300,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    },
    {
     "id": "B",
     "units": 0,
     "unitPrice": {
      "minor": 300,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 1000,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expect": {
   "trades": [
    {
     "id": "A",
     "units": 1,
     "value": {
      "minor": 300,
      "currency": "GBP"
     },
     "unitsAfter": 1,
     "valueAfter": {
      "minor": 300,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 3000
    },
    {
     "id": "B",
     "units": 2,
     "value": {
      "minor": 600,
      "currency": "GBP"
     },
     "unitsAfter": 2,
     "valueAfter": {
      "minor": 600,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 6000
    }
   ],
   "totalValue": {
    "minor": 1000,
    "currency": "GBP"
   },
   "bought": {
    "minor": 900,
    "currency": "GBP"
   },
   "sold": {
    "minor": 0,
    "currency": "GBP"
   },
   "cashAfter": {
    "minor": 100,
    "currency": "GBP"
   }
  }
 },
 {
  "name": "one-unit trades below the minimum are not placed",
  "args": [
   [
    {
     "id": "A",
     "units": 100,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    },
    {
     "id": "B",
     "units": 98,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 500,
    "currency": "GBP"
   }
  ],
  "expect": {
   "trades": [
    {
     "id": "A",
     "units": 0,
     "value": {
      "minor": 0,
      "currency": "GBP"
     },
     "unitsAfter": 100,
     "valueAfter": {
      "minor": 10000,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 5051
    },
    {
     "id": "B",
     "units": 0,
     "value": {
      "minor": 0,
      "currency": "GBP"
     },
     "unitsAfter": 98,
     "valueAfter": {
      "minor": 9800,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 4949
    }
   ],
   "totalValue": {
    "minor": 19800,
    "currency": "GBP"
   },
   "bought": {
    "minor": 0,
    "currency": "GBP"
   },
   "sold": {
    "minor": 0,
    "currency": "GBP"
   },
   "cashAfter": {
    "minor": 0,
    "currency": "GBP"
   }
  }
 },
 {
  "name": "a trade exactly at the minimum is placed",
  "args": [
   [
    {
     "id": "A",
     "units": 10,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    },
    {
     "id": "B",
     "units": 0,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 500,
    "currency": "GBP"
   }
  ],
  "expect": {
   "trades": [
    {
     "id": "A",
     "units": -5,
     "value": {
      "minor": -500,
      "currency": "GBP"
     },
     "unitsAfter": 5,
     "valueAfter": {
      "minor": 500,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 5000
    },
    {
     "id": "B",
     "units": 5,
     "value": {
      "minor": 500,
      "currency": "GBP"
     },
     "unitsAfter": 5,
     "valueAfter": {
      "minor": 500,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 5000
    }
   ],
   "totalValue": {
    "minor": 1000,
    "currency": "GBP"
   },
   "bought": {
    "minor": 500,
    "currency": "GBP"
   },
   "sold": {
    "minor": 500,
    "currency": "GBP"
   },
   "cashAfter": {
    "minor": 0,
    "currency": "GBP"
   }
  }
 },
 {
  "name": "the sell that funds a rounded-up buy is too small, so the buy is dropped too",
  "args": [
   [
    {
     "id": "A",
     "units": 100,
     "unitPrice": {
      "minor": 50,
      "currency": "GBP"
     },
     "targetBasisPoints": 4100
    },
    {
     "id": "B",
     "units": 0,
     "unitPrice": {
      "minor": 1000,
      "currency": "GBP"
     },
     "targetBasisPoints": 900
    },
    {
     "id": "C",
     "units": 5,
     "unitPrice": {
      "minor": 1000,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 950,
    "currency": "GBP"
   }
  ],
  "expect": {
   "trades": [
    {
     "id": "A",
     "units": 0,
     "value": {
      "minor": 0,
      "currency": "GBP"
     },
     "unitsAfter": 100,
     "valueAfter": {
      "minor": 5000,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 5000
    },
    {
     "id": "B",
     "units": 0,
     "value": {
      "minor": 0,
      "currency": "GBP"
     },
     "unitsAfter": 0,
     "valueAfter": {
      "minor": 0,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 0
    },
    {
     "id": "C",
     "units": 0,
     "value": {
      "minor": 0,
      "currency": "GBP"
     },
     "unitsAfter": 5,
     "valueAfter": {
      "minor": 5000,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 5000
    }
   ],
   "totalValue": {
    "minor": 10000,
    "currency": "GBP"
   },
   "bought": {
    "minor": 0,
    "currency": "GBP"
   },
   "sold": {
    "minor": 0,
    "currency": "GBP"
   },
   "cashAfter": {
    "minor": 0,
    "currency": "GBP"
   }
  }
 },
 {
  "name": "without a minimum the sell goes ahead but the unfundable buy does not",
  "args": [
   [
    {
     "id": "A",
     "units": 100,
     "unitPrice": {
      "minor": 50,
      "currency": "GBP"
     },
     "targetBasisPoints": 4100
    },
    {
     "id": "B",
     "units": 0,
     "unitPrice": {
      "minor": 1000,
      "currency": "GBP"
     },
     "targetBasisPoints": 900
    },
    {
     "id": "C",
     "units": 5,
     "unitPrice": {
      "minor": 1000,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expect": {
   "trades": [
    {
     "id": "A",
     "units": -18,
     "value": {
      "minor": -900,
      "currency": "GBP"
     },
     "unitsAfter": 82,
     "valueAfter": {
      "minor": 4100,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 4100
    },
    {
     "id": "B",
     "units": 0,
     "value": {
      "minor": 0,
      "currency": "GBP"
     },
     "unitsAfter": 0,
     "valueAfter": {
      "minor": 0,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 0
    },
    {
     "id": "C",
     "units": 0,
     "value": {
      "minor": 0,
      "currency": "GBP"
     },
     "unitsAfter": 5,
     "valueAfter": {
      "minor": 5000,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 5000
    }
   ],
   "totalValue": {
    "minor": 10000,
    "currency": "GBP"
   },
   "bought": {
    "minor": 0,
    "currency": "GBP"
   },
   "sold": {
    "minor": 900,
    "currency": "GBP"
   },
   "cashAfter": {
    "minor": 900,
    "currency": "GBP"
   }
  }
 },
 {
  "name": "targets summing to 8000 leave 20% in cash",
  "args": [
   [
    {
     "id": "A",
     "units": 10,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 8000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expect": {
   "trades": [
    {
     "id": "A",
     "units": -2,
     "value": {
      "minor": -200,
      "currency": "GBP"
     },
     "unitsAfter": 8,
     "valueAfter": {
      "minor": 800,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 8000
    }
   ],
   "totalValue": {
    "minor": 1000,
    "currency": "GBP"
   },
   "bought": {
    "minor": 0,
    "currency": "GBP"
   },
   "sold": {
    "minor": 200,
    "currency": "GBP"
   },
   "cashAfter": {
    "minor": 200,
    "currency": "GBP"
   }
  }
 },
 {
  "name": "an exact half unit rounds towards the units already held",
  "args": [
   [
    {
     "id": "A",
     "units": 91,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 8950
    },
    {
     "id": "B",
     "units": 9,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 1050
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expect": {
   "trades": [
    {
     "id": "A",
     "units": -1,
     "value": {
      "minor": -100,
      "currency": "GBP"
     },
     "unitsAfter": 90,
     "valueAfter": {
      "minor": 9000,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 9000
    },
    {
     "id": "B",
     "units": 1,
     "value": {
      "minor": 100,
      "currency": "GBP"
     },
     "unitsAfter": 10,
     "valueAfter": {
      "minor": 1000,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 1000
    }
   ],
   "totalValue": {
    "minor": 10000,
    "currency": "GBP"
   },
   "bought": {
    "minor": 100,
    "currency": "GBP"
   },
   "sold": {
    "minor": 100,
    "currency": "GBP"
   },
   "cashAfter": {
    "minor": 0,
    "currency": "GBP"
   }
  }
 },
 {
  "name": "a zero target sells the holding out",
  "args": [
   [
    {
     "id": "OLD",
     "units": 7,
     "unitPrice": {
      "minor": 300,
      "currency": "GBP"
     },
     "targetBasisPoints": 0
    },
    {
     "id": "NEW",
     "units": 0,
     "unitPrice": {
      "minor": 700,
      "currency": "GBP"
     },
     "targetBasisPoints": 10000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expect": {
   "trades": [
    {
     "id": "OLD",
     "units": -7,
     "value": {
      "minor": -2100,
      "currency": "GBP"
     },
     "unitsAfter": 0,
     "valueAfter": {
      "minor": 0,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 0
    },
    {
     "id": "NEW",
     "units": 3,
     "value": {
      "minor": 2100,
      "currency": "GBP"
     },
     "unitsAfter": 3,
     "valueAfter": {
      "minor": 2100,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 10000
    }
   ],
   "totalValue": {
    "minor": 2100,
    "currency": "GBP"
   },
   "bought": {
    "minor": 2100,
    "currency": "GBP"
   },
   "sold": {
    "minor": 2100,
    "currency": "GBP"
   },
   "cashAfter": {
    "minor": 0,
    "currency": "GBP"
   }
  }
 },
 {
  "name": "a portfolio already on target trades nothing",
  "args": [
   [
    {
     "id": "A",
     "units": 60,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 6000
    },
    {
     "id": "B",
     "units": 40,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 4000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expect": {
   "trades": [
    {
     "id": "A",
     "units": 0,
     "value": {
      "minor": 0,
      "currency": "GBP"
     },
     "unitsAfter": 60,
     "valueAfter": {
      "minor": 6000,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 6000
    },
    {
     "id": "B",
     "units": 0,
     "value": {
      "minor": 0,
      "currency": "GBP"
     },
     "unitsAfter": 40,
     "valueAfter": {
      "minor": 4000,
      "currency": "GBP"
     },
     "weightAfterBasisPoints": 4000
    }
   ],
   "totalValue": {
    "minor": 10000,
    "currency": "GBP"
   },
   "bought": {
    "minor": 0,
    "currency": "GBP"
   },
   "sold": {
    "minor": 0,
    "currency": "GBP"
   },
   "cashAfter": {
    "minor": 0,
    "currency": "GBP"
   }
  }
 },
 {
  "name": "no holdings is an error",
  "args": [
   [],
   {
    "minor": 100,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expectError": "holdings must not be empty"
 },
 {
  "name": "the same id twice is an error",
  "args": [
   [
    {
     "id": "A",
     "units": 1,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    },
    {
     "id": "A",
     "units": 1,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expectError": "duplicate holding id \"A\""
 },
 {
  "name": "targets over 10000 are an error",
  "args": [
   [
    {
     "id": "A",
     "units": 1,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 6000
    },
    {
     "id": "B",
     "units": 1,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 4001
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expectError": "targets must sum to at most 10000 basis points, received 10001"
 },
 {
  "name": "a negative target is an error",
  "args": [
   [
    {
     "id": "A",
     "units": 1,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": -1
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expectError": "targetBasisPoints must be a whole number from 0 to 10000"
 },
 {
  "name": "a fractional unit holding is an error",
  "args": [
   [
    {
     "id": "A",
     "units": 1.5,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expectError": "units must be a whole number, 0 or more"
 },
 {
  "name": "negative units are an error",
  "args": [
   [
    {
     "id": "A",
     "units": -1,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 1000,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expectError": "units must be a whole number, 0 or more"
 },
 {
  "name": "a price of zero is an error",
  "args": [
   [
    {
     "id": "A",
     "units": 1,
     "unitPrice": {
      "minor": 0,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expectError": "unitPrice must be whole minor units greater than zero"
 },
 {
  "name": "negative cash is an error",
  "args": [
   [
    {
     "id": "A",
     "units": 1,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": -1,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expectError": "cash must be whole minor units, 0 or more"
 },
 {
  "name": "a negative minimum trade is an error",
  "args": [
   [
    {
     "id": "A",
     "units": 1,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": -1,
    "currency": "GBP"
   }
  ],
  "expectError": "minimumTrade must be whole minor units, 0 or more"
 },
 {
  "name": "a price in another currency is an error",
  "args": [
   [
    {
     "id": "A",
     "units": 1,
     "unitPrice": {
      "minor": 100,
      "currency": "USD"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expectError": "currency mismatch: GBP and USD"
 },
 {
  "name": "a portfolio worth nothing is an error",
  "args": [
   [
    {
     "id": "A",
     "units": 0,
     "unitPrice": {
      "minor": 100,
      "currency": "GBP"
     },
     "targetBasisPoints": 5000
    }
   ],
   {
    "minor": 0,
    "currency": "GBP"
   },
   {
    "minor": 0,
    "currency": "GBP"
   }
  ],
  "expectError": "the portfolio total must be greater than zero"
 }
]