[
{
"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"
}
]