3
\$\begingroup\$

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.

\$\endgroup\$
  • 3
    \$\begingroup\$ @dfhwze, yes, looks like I can \$\endgroup\$ – PPann Sep 14 at 16:19
  • 3
    \$\begingroup\$ Since the method isn't pure and uses a lot of properties or other class members, I'm changing my mind about this question and vote-to-close it for the lack of context. You should add the entire class. \$\endgroup\$ – t3chb0t Sep 14 at 16:47
  • 3
    \$\begingroup\$ @t3chb0t, added the relevant properties of the class and the class. the entire class is to big to include \$\endgroup\$ – PPann Sep 14 at 21:08
  • 6
    \$\begingroup\$ Just a tip for this kind of refactoring. Put the code away and head to the whiteboard. Draw some graphs. Explore the relationship the numbers have to each other. The hard part is figuring out how to express the solution as a single equation that can calculate the solution directly at any given point. Sometimes you just need to stare at it for a while before the right bit of math jumps out at you. Drawings and graphs can help this process immensely. \$\endgroup\$ – RubberDuck Sep 15 at 11:13
  • 2
    \$\begingroup\$ I have no idea why this was closed. The code works as intended. I’ve voted to reopen it so you can post that as an answer. \$\endgroup\$ – RubberDuck Sep 16 at 13:27
4
\$\begingroup\$

Some observations and guidelines to get you started refactoring the code.

Check condition

Checking a condition against a bool is rarely written like this in C#:

mayBeHigher == false

Prefer:

!mayBeHigher

Refactor variables to allow DRY code

If you want to get rid of those if-statements with almost identical bodies, you should refactor your code in a way a single body would suffice.

For instance, you sometimes call sum += pnlPerTick; other times sum -= pnlPerTick;. The same occurs with priceAtPnl += MinTick; and priceAtPnl -= MinTick;. Ideally, you would want to do something like this:

sum += offset;
priceAtPnl += priceDelta;

With offset calculated from pnlPerTick and priceDelta from MinTick given your parameters position.pos and pnl.


Refactor conditions for compactness

The pattern of the conditions inside the if-statements could also be written in a more compact matter. The pattern is (!a && x + n <= k) || (a && x <= k) which could be rewritten as x + (!a ? n : 0) <= k.

For instance,

while ((!mayBeHigher && sum + pnlPerTick <= pnl) || (mayBeHigher && sum <= pnl))

Could be rewritten as:

while (sum + (!mayBeHigher ? pnlPerTick : 0) <= pnl))

Or if you introduce a variable var padding = !mayBeHigher ? pnlPerTick : 0; to:

while (sum + padding <= pnl))
\$\endgroup\$
  • \$\begingroup\$ I'm still stuck with the loop, the idea is to remove the loop altogether \$\endgroup\$ – PPann Sep 15 at 6:51
  • \$\begingroup\$ Am I correct, your DRY code just adds complexity moves the logic to another location and 2 variables? DRY vs KISS. Also your while (sum + (!mayBeHigher ? pnlPerTick : 0) <= pnl) and while (sum + (!mayBeHigher ? pnlPerTick : 0) >= pnl) respecively will fail the unit tests \$\endgroup\$ – PPann Sep 15 at 7:17
  • \$\begingroup\$ PPann why do you want to remove the loop so badly? This request is btw off-topic. \$\endgroup\$ – t3chb0t Sep 15 at 7:18
  • 1
    \$\begingroup\$ If you want to remove the loop altogether, you could use multiplication and perhaps some modulo sum += magnitude * pnlPerTick; and calculate magnitude to avoid the while. \$\endgroup\$ – dfhwze Sep 15 at 7:58
  • \$\begingroup\$ @dfhwze, yes, that's the idea however I can't solve the equation without getting in troubles with the valid prices with the exchange, hence the loop and the priceAtPnl += MinTick; and priceAtPnl -= MinTick; respectively. The MinTick changes based on the exchange, contract and underlying instrument in the contract as well as the Real-Time price and can range from 100 to 0.00005 as written in the code comments \$\endgroup\$ – PPann Sep 15 at 8:33
2
\$\begingroup\$

Thanks's @RubberDuck

After playing a bit in excel I have found the following solution using math and removing the loops, here you see the method, the Contract class that hosts this method for those that would like to try is above.

/// <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) || (Position.avgCost == 0 || Position.pos == 0))
    {
        price = 0;
        return false;
    }


    //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 =Position.pos * MinTick;

    //calculate the distance in MinTicks that one needs to go for;
    var minTicksToMove = (pnl - (0- CommisionPerTrade)) / pnlPerTick;

    if (mayBeHigher && Math.Abs(minTicksToMove) > (int)Math.Abs(minTicksToMove))
    {
        if (pnl > 0)//positive Number then MinTicksMove will be up
            minTicksToMove = (int)minTicksToMove + 1;
        else//MinTicksMove will needs to go down to allow greater loss
            minTicksToMove = (int)minTicksToMove - 1;
    }

    //can't have floating point imprecision but need to return a double so cast the decimal to a double
    price = Convert.ToDouble(Position.avgCost+((int)minTicksToMove*MinTick));
    return true;
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.