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