Friday, February 1, 2013

Advanced Sorting in .Net Using Comparison and IComparer


Advertisement

Advertisement
In my last post I covered some of the basics with using IComparer and Comparison for sorting collections in .Net that implement the Sort() method. In this post, we will take a look at some more advanced sorting scenarios where multi-level sorting needs to be used.

First, what is multi-level sorting?

Multi-level sorting requires sorting each record of a set by more than one parameter. Each successive parameter in the sort is used if the preceding parameters are equal. Take the following example showing 5 unsorted first and last names:

First Name Last Name
John Smith
Jane Smith
Benjamin Franklin
George Washington

Now, let's say we want this example to be sorted by last name (first parameter) and then by first name (second parameter).

First Name Last Name
Benjamin Franklin
Jane Smith
John Smith
George Washington

For Benjamin Franklin and George Washington, the second sort parameter (first name) doesn't matter because there are no other people with the last names Washington or Franklin in the set. For John and Jane, the second sort parameter matters because they have the same last name.

Now on to an applied example from the world of finance... Let's take an example where we have a set of investment positions (both long and short) across commodities, foreign exchange (forex), and the equities (stock) markets. We'll start out with the following basic definitions:


    public enum PositionType
    {
        /// <summary>
        /// A long position is generally taken when an asset
        /// is expected to increase in value. An investor would
        /// profit by buying an asset (stock,  bond, foreign currency,
        /// mutual fund, exchange traded fund (ETF), commodity futures
        /// contract, equity option, etc.) at a low price and then
        /// selling it at a higher price at some time in the future
        /// </summary>
        Long,

        /// <summary>
        /// A short position is generally taken when an asset
        /// is expected to decrease in value. An investor would
        /// profit by borrowing and selling an asset (stock,  bond,
        /// foreign currency, mutual fund, exchange traded fund (ETF),
        /// commodity futures contract, equity option, etc.) at a high price
        /// and then buying it back at a lower price at some time in the future
        /// </summary>
        Short
    }


    public enum InvestmentType
    {
        Bond,
        Commodity,
        Private_Equity,
        Foreign_Exchange,
        Public_Equity,
        Mutual_Fund,
        Equity_Option,
        Other_Derivative
    }

    public class InvestmentPosition
    {
        public InvestmentType InvestmentType;
        public PositionType PositionType;
        public string Description;
        public int Quantity;
        public decimal Initial_Price;
        public decimal Final_Price;
    } --> --> -->

Now, we'll define a problem where we have a number of investment positions that we want to sort by dollar-value gain/loss and within each gain/loss category we want to order by description (note that I've artificially set up the prices to have a few instances of equal gains/losses so that we end up sorting on the Description field). The reason why I included the InvestmentType enumeration in this example is because gain/loss is calculated differently for foreign exchange than it is for most other investments and we'll end up needing to take this into account. How to calculate the gain or loss on a foreign exchange position or a typical asset is below:

Foreign Exchange Trades Most Other Investments



Like my previous example, we will start out by creating a new unit test and create some investment positions.


        [TestMethod]
        public void TestSort_DollarValue_Description()
        {
            InvestmentPosition i1 = new InvestmentPosition()
            {
                InvestmentType = BlogExamples.InvestmentType.Public_Equity,
                PositionType = BlogExamples.PositionType.Short,
                Description = "Equity - Apple (AAPL) - Short",
                // Gain of $20,000
                Initial_Price = 700,
                Final_Price = 500,
                Quantity = 100
               
            };

            InvestmentPosition i2 = new InvestmentPosition()
            {
                InvestmentType = BlogExamples.InvestmentType.Public_Equity,
                PositionType = BlogExamples.PositionType.Short,
                Description = "Equity - Facebook (FB) - Short",
                // Gain of $20,000
                Initial_Price = 30,
                Final_Price = 20,
                Quantity = 2000
            };

            InvestmentPosition i3 = new InvestmentPosition()
            {
                InvestmentType = BlogExamples.InvestmentType.Public_Equity,
                PositionType = BlogExamples.PositionType.Long,
                Description = "Equity - Microsoft (MSFT) - Long",
                //Gain of 1000
                Initial_Price = 25,
                Final_Price = 30,
                Quantity = 200
            };

            InvestmentPosition i4 = new InvestmentPosition()
            {
                InvestmentType = BlogExamples.InvestmentType.Foreign_Exchange,
                PositionType = BlogExamples.PositionType.Short,
                Description = "Forex - EUR/USD - Short",
                //Gain of 100 PIPS ~ $48.31 gain
                Initial_Price = 1.3500m,
                Final_Price = 1.3400m,
                Quantity = 10000
            };

            InvestmentPosition i5 = new InvestmentPosition()
            {
                InvestmentType = BlogExamples.InvestmentType.Foreign_Exchange,
                PositionType = BlogExamples.PositionType.Long,
                Description = "Forex - AUD/USD - Long",
                //Loss of 25 PIPS ~ $24.10 loss
                Initial_Price = 1.0400M,
                Final_Price = 1.0375M,
                Quantity = 10000
            };
-->

Then add them to the List<T> that we are going to sort...


           List<InvestmentPosition> objects = new List<InvestmentPosition>();

           objects.AddRange(new InvestmentPosition[] { i5, i1, i4, i3, i2 });
-->

Now we define a sort order (first on dollar value gain, then on description) and sort them...


        objects.Sort((x, y) =>
        {
            int ret = -1;

            //First, what are the gains and losses?
            //Normally, this would probably be encapsulated in the investment class,
            //but I put it in the sort function instead...
            decimal gain_x =
                x.InvestmentType == InvestmentType.Foreign_Exchange ?
                ( //Calculate foreign exchange gain or loss
                     x.PositionType == PositionType.Long ?
                        (decimal)x.Quantity * (1m - (x.Initial_Price / x.Final_Price))
                     : //Or for short
                        (decimal)x.Quantity * ((x.Initial_Price / x.Final_Price) - 1m)
                ) : //Otherwise, calculate as a regular asset
                ( // Calculate gain/loss as a regular asset
                     x.PositionType == PositionType.Long ?
                        (decimal)x.Quantity * (x.Final_Price - x.Initial_Price)
                     : // For shorting a regular asset
                        (decimal)x.Quantity * (x.Initial_Price - x.Final_Price)
                );

            decimal gain_y =
                y.InvestmentType == InvestmentType.Foreign_Exchange ?
                ( //Calculate foreign eychange gain or loss
                     y.PositionType == PositionType.Long ?
                        (decimal)y.Quantity * (1m - (y.Initial_Price / y.Final_Price))
                     : //Or for short
                        (decimal)y.Quantity * ((y.Initial_Price / y.Final_Price) - 1m)
                ) : //Otherwise, calculate as a regular asset
                ( // Calculate gain/loss as a regular asset
                     y.PositionType == PositionType.Long ?
                        (decimal)y.Quantity * (y.Final_Price - y.Initial_Price)
                     : // For shorting a regular asset
                        (decimal)y.Quantity * (y.Initial_Price - y.Final_Price)
                );

            //First parameter of sort operation - Gain/Loss
            if (gain_y > gain_x)
            {
                ret = 1;
            }
            //gain_y < gain_x case handled above with the default value of ret
            else if (gain_y == gain_x)
            {
                //Second parameter of sort - based on description
                //If the objects are not of type IComparable or
                //if you need more parameters for your sort, then
                //you would need to add a case for x.Description == y.Description
                ret = x.Description.CompareTo(y.Description);
            }
 
            return ret;
        });
-->
And check if they end up in the right order...


            //Correct order should be i1,i2,i3,i4,i5

            Assert.AreEqual(i1, objects[0]);
            Assert.AreEqual(i2, objects[1]);
            Assert.AreEqual(i3, objects[2]);
            Assert.AreEqual(i4, objects[3]);
            Assert.AreEqual(i5, objects[4]);
Long story short, multi-level sorting breaks down to the following pattern:



    public class SpecialComparison : IComparer
    {
        public int Compare(SomeObject x, SomeObject y)
        {
            //First layer of sort (using parameter1)
            int ret = x.parameter1.CompareTo(y.parameter1);

            if (ret == 0)
            {
                //Second layer of sort (using parameter2)
                ret = x.parameter2.CompareTo(y.parameter2);

                if (ret == 0)
                {
                    //Third layer of sort (using parameter3)
                    ret = x.parameter3.CompareTo(y.parameter3);

                }
            }
        }
    }
-->



No comments:

Post a Comment