I have to project the price for a publicly traded contract when the price should be approximately matching the targeted profit or loss. I have written the following code, runs under 0.02ms.. could be a faster and could be a bit more elegant as the code will get slower progressively depending on the iterations needed. I have documented the code, hope that it makes sense.
class Contract
{
public string Symbol { get; set; }
public string Currency { get; set; }
public decimal CommisionPerTrade { get; set; }
public decimal MinTick { get; set; }
public string PriceFormat { get; set; }
private (int pos, decimal avgCost) position;
public void AssignPossition(int shares, decimal averagePrice)
{
position = (shares, averagePrice);
}
public (int pos, decimal avgCost) Position => position;
/// <summary>
/// Get an exchange acceptable price for a contract for a given Profit & Loss value.
/// The price depends on the rounding of a given contract, possible minimum - ticks are 100, 10, 1.0, 0.1, 0.01, 0.001, 0.0005
/// </summary>
/// <param name="pnl">The P&L that is aimed for.</param>
/// <param name="mayBeHigher">if set to <c>true</c> then loss may be higher as well as profit to take the next acceptable
/// contract price else accept less rather them more.</param>
/// <returns>a acceptable price at a given Min-Tick range that would get a approximate P&L</returns>
/// <remarks>Please note that loss is a negative number, loss may be higher is actually a lower number as a loss of -100 is
/// higher than a loss of -50</remarks>
public bool TryGetPriceAtPnl(decimal pnl, bool mayBeHigher, out double price)
{
if ((Math.Abs(pnl) <= CommisionPerTrade && mayBeHigher == false)
|| (Position.avgCost == 0 || Position.pos == 0))
{
price = 0;
return false;
}
//the current price paid for the contract
decimal priceAtPnl = Position.avgCost;
//start with deducting the commission that will need to be paid
decimal sum = -CommisionPerTrade;
//get the Price for minimal price change (min-tick) in the current position on a contract
//positions when short contain negative values so best to take abs values
decimal pnlPerTick = (Math.Abs(Position.pos) * (Position.avgCost + MinTick))
- (Math.Abs(Position.pos) * Position.avgCost);
if (position.pos > 0)// long trade
{
if (pnl > 0) //profit target
{
//positive P&L
while ((!mayBeHigher && sum + pnlPerTick <= pnl) || (mayBeHigher && sum <= pnl))
{
sum += pnlPerTick;
priceAtPnl += MinTick;//next allowed price by adding the minimum price change
}
}
else //loss target
{
//when a negative P&L "may be higher" then this actually is, and must be, a lower value
while ((!mayBeHigher && sum - pnlPerTick >= pnl) || (mayBeHigher && sum >= pnl))
{
sum -= pnlPerTick;
priceAtPnl -= MinTick;//previous allowed price by subtracting the minimum price change
}
}
}
else // short trade
{
if (pnl > 0) //profit target
{
//positive P&L
while ((!mayBeHigher && sum + pnlPerTick >= pnl) || (mayBeHigher && sum >= pnl))
{
sum += pnlPerTick;
priceAtPnl -= MinTick;//next allowed price by adding the minimum price change
}
}
else //loss target
{
//when a negative P&L "may be higher" then this actually is, and must be, a lower value
while ((!mayBeHigher && sum - pnlPerTick >= pnl) || (mayBeHigher && sum >= pnl))
{
sum -= pnlPerTick;
priceAtPnl += MinTick;//previous allowed price by subtracting the minimum price change
}
}
}
//can't have floating point imprecision but need to return a double so cast the decimal to a double
price = Convert.ToDouble(priceAtPnl);
return true;
}
}
I have several unit tests and all look to be projecting the correct price for both long and short trades as well as profit targets and stop-loss prices.
Here are 2 unit tests
public void NativeTrainerTestPriceAtPnlGoodCaseShortLoss()
{
var contract = new Contract() { Symbol = "ABC", Currency = "ABC", CommisionPerTrade = 238.50M + 238.48M, MinTick = 0.001M, PriceFormat = "N2" };
contract.AssignPossition(shares: -100_000, averagePrice: 118.891M);
/* P&L table for short trades
* ______________________________________
* PRICE P&L Min-Ticks
* 118.901 -1476.98 10 P
* 118.900 -1376.98 9 R
* 118.899 -1276.98 8 I
* 118.898 -1176.98 7 C
* 118.897 -1076.98 6 E
* 118.896 -976.98 5
* 118.895 -876.98 4 U
* 118.894 -776.98 3 P
* 118.893 -676.98 2
* 118.892 -576.98 1
* 118.891 -476.98 0 --- BASE PRICE
* 118.890 -376.98 1
* 118.889 -276.98 2 P
* 118.888 -276.98 3 R
* 118.887 -176.98 4 I
* 118.886 -76.98 5 C
* 118.885 23.02 6 E
* 118.884 123.02 7
* 118.883 223.02 8 D
* 118.882 323.02 9 O
* 118.881 423.02 10 W
* 118.880 523.02 11 N
*/
//warm up .net to make sure we are jitted
contract.TryGetPriceAtPnl(700, false, out double _);
var sw = System.Diagnostics.Stopwatch.StartNew();
Assert.IsTrue(contract.TryGetPriceAtPnl(pnl: -700.00M, mayBeHigher: false, out double price), "Calculate price for P&L 700 must be possible, however returned false");
sw.Stop();
Assert.IsTrue(price > 0, "Price must always be more then 0");
Assert.AreEqual(118.893D, price);
if (sw.ElapsedMilliseconds > 0.02)
Assert.Inconclusive($"Non Functional requirement, price calculations must be under 2 ms, this server did it in {sw.Elapsed}");
}
[TestMethod]
[TestCategory("Trade Stop-Loss")]
public void NativeTrainerTestPriceAtPnlGoodCaseLongLoss()
{
/* P&L table for long trades
* ______________________________________
* PRICE P&L Min-Ticks
* 118.936 523.12 10
* 118.935 423.12 9
* 118.934 323.12 8
* 118.933 223.12 7
* 118.932 123.12 6
* 118.931 23.12 5
* 118.930 - 76.88 4
* 118.929 -176.88 3
* 118.928 -276.88 2
* 118.927 -376.88 1
* 118.926 -476.88 0 ---
* 118.925 -576.88 1
* 118.924 -676.88 2
* 118.923 -767.88 3
* 118.922 -867.88 4
* 118.921 -967.88 5
*/
var contract = new Contract() { Symbol = "ABC", Currency = "ABC", CommisionPerTrade = 238.43M + 238.45M, MinTick = 0.001M, PriceFormat = "N2" };
contract.AssignPossition(shares: 100_000, averagePrice: 118.926M);
//warm up .net to make sure we are jitted
contract.TryGetPriceAtPnl(700, false, out double _);
var sw = System.Diagnostics.Stopwatch.StartNew();
contract.TryGetPriceAtPnl(pnl: -700M, mayBeHigher: false, out double price);
sw.Stop();
Assert.AreEqual(118.924D, price);
contract.TryGetPriceAtPnl(pnl: -700M, mayBeHigher: true, out price);
Assert.AreEqual(118.923D, price);
if (sw.ElapsedMilliseconds > 0.02)
Assert.Inconclusive($"Non Functional requirement, price calculations must be under 2 ms, this server did it in {sw.Elapsed}");
}
Ideally I would like to project the price without the loops. Important is that one must project valid prices as else the exchange will reject the order.