Last active
January 11, 2019 17:09
-
-
Save jaybuidl/979cb5392fbc560f0807573b45e68d38 to your computer and use it in GitHub Desktop.
Expected behaviour of order entry by units or amount, input validation and 2-ways conversion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.junit.jupiter.api.Test; | |
import java.math.BigDecimal; | |
import static org.junit.jupiter.api.Assertions.assertEquals; | |
import static org.junit.jupiter.api.Assertions.assertThrows; | |
public | |
class OrderTest | |
{ | |
private final BigDecimal LOT_SIZE = bd("0.1"); | |
private final BigDecimal LEVERAGE = bd("100"); | |
private final BigDecimal BID_PRICE = bd("99.9999"); // assuming tick size = 0.0001 | |
private final BigDecimal ASK_PRICE = bd("100.0001"); | |
private enum Side {BUY, SELL}; | |
private BigDecimal bd(String s) | |
{ | |
return new BigDecimal(s); | |
} | |
//****************************************************************************************************************** | |
// Volume Input Validation | |
//****************************************************************************************************************** | |
BigDecimal validateVolume(BigDecimal volume) | |
{ | |
if (volume.compareTo(LOT_SIZE) < 0) | |
{ | |
throw new IllegalArgumentException(); | |
} | |
return volume.subtract(volume.remainder(LOT_SIZE)); | |
} | |
@Test | |
public | |
void testValidateVolume() | |
{ | |
// volume >= LOT_SIZE | |
assertThrows(IllegalArgumentException.class, () -> validateVolume(bd("-4.44"))); | |
assertThrows(IllegalArgumentException.class, () -> validateVolume(bd("0"))); | |
assertThrows(IllegalArgumentException.class, () -> validateVolume(bd("0.09"))); | |
// volume must be a multiple of LOT_SIZE | |
// volume is rounded down | |
assertEquals(bd("0.10"), validateVolume(bd("0.10"))); | |
assertEquals(bd("0.10"), validateVolume(bd("0.11"))); | |
assertEquals(bd("0.10"), validateVolume(bd("0.15"))); | |
assertEquals(bd("0.10"), validateVolume(bd("0.19"))); | |
assertEquals(bd("0.20"), validateVolume(bd("0.20"))); | |
} | |
//****************************************************************************************************************** | |
// Volume Conversion to $ Amount | |
//****************************************************************************************************************** | |
BigDecimal volumeToAmount(BigDecimal volume, Side side) | |
{ | |
if (side == Side.BUY) | |
{ | |
return validateVolume(volume).multiply(ASK_PRICE).multiply(LEVERAGE); | |
} | |
else if (side == Side.SELL) | |
{ | |
return validateVolume(volume).multiply(BID_PRICE).multiply(LEVERAGE); | |
} | |
throw new IllegalArgumentException("illegal side value"); | |
} | |
@Test | |
void testVolumeToAmountBuy() | |
{ | |
var side = Side.BUY; | |
// volume > 0 | |
assertThrows(IllegalArgumentException.class, () -> volumeToAmount(bd("-0.4"), side)); | |
assertThrows(IllegalArgumentException.class, () -> volumeToAmount(bd("0"), side)); | |
// conversion is rounded down | |
assertEquals(bd("1000.001000"), volumeToAmount(bd("0.10"), side)); | |
assertEquals(bd("1000.001000"), volumeToAmount(bd("0.16"), side)); | |
assertEquals(bd("11000.011000"), volumeToAmount(bd("1.10"), side)); | |
assertEquals(bd("15000.015000"), volumeToAmount(bd("1.51"), side)); | |
} | |
@Test | |
void testVolumeToAmountSell() | |
{ | |
final var side = Side.SELL; | |
// volume > 0 | |
assertThrows(IllegalArgumentException.class, () -> volumeToAmount(bd("-0.4"), side)); | |
assertThrows(IllegalArgumentException.class, () -> volumeToAmount(bd("0"), side)); | |
// conversion is rounded down | |
assertEquals(bd("999.999000"), volumeToAmount(bd("0.10"), side)); | |
assertEquals(bd("999.999000"), volumeToAmount(bd("0.16"), side)); | |
assertEquals(bd("10999.989000"), volumeToAmount(bd("1.10"), side)); | |
assertEquals(bd("14999.985000"), volumeToAmount(bd("1.51"), side)); | |
} | |
//****************************************************************************************************************** | |
// $ Amount Input Validation | |
//****************************************************************************************************************** | |
BigDecimal validateAmount(BigDecimal amount, Side side) | |
{ | |
final BigDecimal lotSizeValue; | |
if (side == Side.BUY) | |
{ | |
lotSizeValue = LOT_SIZE.multiply(ASK_PRICE).multiply(LEVERAGE); | |
} | |
else if (side == Side.SELL) | |
{ | |
lotSizeValue = LOT_SIZE.multiply(BID_PRICE).multiply(LEVERAGE); | |
} | |
else | |
{ | |
throw new IllegalArgumentException("illegal side value"); | |
} | |
if (amount.compareTo(lotSizeValue) < 0) | |
{ | |
throw new IllegalArgumentException(); | |
} | |
return amount.subtract(amount.remainder(lotSizeValue)); | |
} | |
@Test | |
void testValidateAmountBuy() | |
{ | |
final var side = Side.BUY; | |
// amount > 0 | |
assertThrows(IllegalArgumentException.class, () -> validateAmount(bd("-8.888"), side)); | |
assertThrows(IllegalArgumentException.class, () -> validateAmount(bd("0"), side)); | |
// less than 1 lot size value = $1000.001 | |
assertThrows(IllegalArgumentException.class, () -> validateAmount(bd("1000"), side)); | |
// must be a multiple of the "Lot Size Value" = LOT_SIZE * ASK_PRICE * LEVERAGE | |
// amount is rounded down | |
assertEquals(bd("1000.00100"), validateAmount(bd("1000.00100"), side)); | |
assertEquals(bd("1000.00100"), validateAmount(bd("1000.00110"), side)); | |
assertEquals(bd("1000.00100"), validateAmount(bd("1000.00200"), side)); | |
assertEquals(bd("1000.00100"), validateAmount(bd("2000.00"), side)); | |
assertEquals(bd("1000.00100"), validateAmount(bd("2000.00100"), side)); | |
assertEquals(bd("2000.00200"), validateAmount(bd("2000.00200"), side)); | |
assertEquals(bd("99000.09900"), validateAmount(bd("100000.00"), side)); | |
assertEquals(bd("100000.100"), validateAmount(bd("100987.65"), side)); | |
} | |
@Test | |
void testValidateAmountSell() | |
{ | |
final var side = Side.SELL; | |
// amount > 0 | |
assertThrows(IllegalArgumentException.class, () -> validateAmount(bd("-8.888"), side)); | |
assertThrows(IllegalArgumentException.class, () -> validateAmount(bd("0"), side)); | |
// less than 1 lot size value = $999.999 | |
assertThrows(IllegalArgumentException.class, () -> validateAmount(bd("900"), side)); | |
// must be a multiple of "Lot Size Value" = LOT_SIZE * ASK_PRICE * LEVERAGE | |
// amount is rounded down | |
assertEquals(bd("999.99900"), validateAmount(bd("999.999"), side)); | |
assertEquals(bd("999.99900"), validateAmount(bd("1000.000"), side)); | |
assertEquals(bd("999.99900"), validateAmount(bd("1000.001"), side)); | |
assertEquals(bd("999.99900"), validateAmount(bd("1999.997"), side)); | |
assertEquals(bd("1999.99800"), validateAmount(bd("1999.998"), side)); | |
assertEquals(bd("98999.90100"), validateAmount(bd("99999.888"), side)); | |
assertEquals(bd("99999.900"), validateAmount(bd("100999.898"), side)); | |
} | |
//****************************************************************************************************************** | |
// $ Amount Conversion to Volume | |
//****************************************************************************************************************** | |
BigDecimal amountToVolume(BigDecimal amount, Side side) | |
{ | |
if (side == Side.BUY) | |
{ | |
return validateAmount(amount, side).divide(ASK_PRICE.multiply(LEVERAGE)); | |
} | |
else if (side == Side.SELL) | |
{ | |
return validateAmount(amount, side).divide(BID_PRICE.multiply(LEVERAGE)); | |
} | |
else throw new IllegalArgumentException("illegal side value"); | |
} | |
@Test | |
void testAmountToVolumeBuy() | |
{ | |
var side = Side.BUY; | |
// amount > 0 | |
assertThrows(IllegalArgumentException.class, () -> amountToVolume(bd("-8.888"), side)); | |
assertThrows(IllegalArgumentException.class, () -> amountToVolume(bd("0"), side)); | |
// less than 1 LOT_SIZE = 1000.001 | |
assertThrows(IllegalArgumentException.class, () -> amountToVolume(bd("1000"), side)); | |
// conversion is rounded down | |
assertEquals(bd("0.1"), amountToVolume(bd("1000.001"), side)); | |
assertEquals(bd("0.1"), amountToVolume(bd("1001.0000"), side)); | |
assertEquals(bd("0.9"), amountToVolume(bd("10000.0000"), side)); | |
} | |
@Test | |
void testAmountToVolumeSell() | |
{ | |
var side = Side.SELL; | |
// amount > 0 | |
assertThrows(IllegalArgumentException.class, () -> amountToVolume(bd("-8.888"), side)); | |
assertThrows(IllegalArgumentException.class, () -> amountToVolume(bd("0"), side)); | |
// less than 1 LOT_SIZE = 999.999 | |
assertThrows(IllegalArgumentException.class, () -> amountToVolume(bd("900"), side)); | |
// conversion is rounded down | |
assertEquals(bd("0.1"), amountToVolume(bd("999.999"), side)); | |
assertEquals(bd("0.1"), amountToVolume(bd("1000"), side)); | |
assertEquals(bd("0.9"), amountToVolume(bd("9000.0000"), side)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment