Money

Back at the beginning of my professional development career I moved into a department where the main project had stalled for several months.  It was an application to allow timesheets to be submitted electronically, and the previous developer was failing to get bugs out of the application.

It took me ten minutes of looking at it to ask "why is this using floating point"? The errors the developer couldn't manage were rounding errors, and the use of floating point pretty well automatically ensured that there would be cumulative and serious rounding effects.

It took about a week to convert everything over to a fixed-point model, partly because this was pure C.  It took another week to convert what had been written as an iterative command-line utility reading and writing from the console into an application using curses to display interim results and provide rather better user feedback.  But the core problem had been the use of floating point.

I have been amazed, since that date, to find that almost every context in which I have worked in the financial sector has used floating point.  This has required extensive workarounds in some cases to manage precision, or to use delta tests rather than simple equality.  In one case we had a fixed point class but it had been introduced some years after the base level applications had been written and there was no way it was ever going to be used to replace the existing doubles.

What makes it even more odd is that in many financial matters, money doesn't require a full array of fixed-point capabilities.  We are, for example, rarely multiplying two floating-point values together, except occasionally in doing things like calculating commissions, and even then that can be converted, normally, into two easy stages of applying integer operations (get a percent by dividing by 100, then multiply by the numerator).  Money behaves like a very, very restricted vector space.  (What would it mean to multiply dollars by dollars?) We apply abstract numeric values to money to get money out, but even in FX, where we really are applying fairly extended decimal values in calculating exchange rates, those values ultimately come off a feed where they are in text format, and can be converted into integer operations without ever doing any conversion into floating point.  So money is a vector and other numbers to be multiplied with them behave as scalars.

In equities trading, the needs are even less broad. We usually want fixed point because of speed; share prices are at worst tenths of a cent in ticks (there used to be eighths, but that's now gone) and precision can be managed relatively easily. For the normal case, for which we want to optimize, we're dealing in boardlots of at least 100, which means that total prices are never of the sort where fixed-point calculations can let anything significant drop off.  If you manage three decimal places on conversion and have another three as a buffer for internal storage you'll never run into precision issues. (If there's a boardlot order for 1,000 shares of something at 2.18 per share, and you need to adjust the price for price improvement rules, 2,180.000 will go up to 2,190 at an extra penny per share. An oddlot order might be 23 shares of the same 2.18, 50.14 in total, and calculating a one per cent charge against it will give you 0.5014.  If you're maintaining six decimal places, you'd need to divide by another thousand (0.0005014) for the 4 to drop off leading to loss of information, even though the amount likely to be used outside the system would be 0.001 cents assuming you were propagating tenths of a cent.)

Values inevitably enter the system -- from feeds, as FIX messages, etc. -- as text, not encoded floating point.  You never have to get anywhere near it -- though in some cases it can be a simple way to do something which is not likely to be a bottleneck in the system. (Provide a 3.5% commission on some transaction, with a couple of extra small factors thrown in based on the client status and the order size: that's the sort of thing that it makes sense to do by "convert to floating point, get the commission, convert back to a string".)

What operations, then, does a Money class have to support? Well, obviously addition.  Multiplication by integers.  In some cases arbitrary division by integers; in many trading contexts, the only actual division can be reduced to division by 100.  For that matter, you can do decimal division by playing with strings and moving the decimal point.  You have to handle negatives.  Conversion to and from strings; conversion to (but not from) doubles.  (We disable direct conversion from doubles to discourage it.  Somebody who really wants to can convert a double to a string and then convert the string to the fixed-point class.)

If you provide a 32-bit integer for the integer (and your domain is never likely to have to deal with anything greater than a few tens of millions at worst) and a 32-bit integer for the mantissa but have an arbitrary cutoff at six or seven decimal places, there are, in practice, no places where you have to worry about overflow or underflow.  If you find that some calculations have higher or lower interim values, just extend the limits on your mantissa and use 64-bit integers instead of 32-bit ones.  It will save a lot of unnecessary bounds checking. (Desk-wide limits, which can be much bigger, can be handled in normal floating point, not treated as money. Or, better, can be handled as integral parts and managed as long longs.)

In other words, it doesn't have to be sophisticated.

The other thing is, it doesn't even have to happen at all, at least much of the time.  In may cases, you can just defer converting a field from a string in the first place, and just pass it on as a string.  If you aren't repricing an order, the fields can remain untouched.  If you're saving to a database, you can pass into the SQL as a string and let the database do the conversion (and even there, you don't want a floating point value! -- it might make sense to store the value there as a fixed point or string value).

All this being said, here's an example of a (tested, though not at extreme values) simple class.  It uses 32-bit ints and a six-figure mantissa; in production, I might be tempted to extend the mantissa and possibly use a long long for the integral part.  Still, it captures most of the essential operations.  In production, there would be more checks for values remaining within a "legitimate" range for the business.

class Money

{

public:

  Money(): m_integer(0), m_mantissa(0) {}

  Money(const int32_t inInteger, const int32_t inMantissa):

      m_integer(inInteger), m_mantissa(inMantissa)

  {

    if (m_mantissa > 999999)

      throw std::runtime_error("Invalid mantissa provided");

  }

  explicit Money(std::string_view inStr);

  Money operator+(const Money &inMoney) const;

  Money operator-(const Money &inMoney) const

  {

    return *this + (inMoney * -1);

  }

  Money &operator+=(const Money &inMoney);

  Money &operator-=(const Money &inMoney);

  Money operator*(const int32_t inAmount) const;

  bool isZero() const { return (m_integer == 0) && (m_mantissa == 0); }

  bool isNegative() const

  {

    return (m_integer < 0) || (m_integer == 0 && (m_mantissa < 0));

  }

  std::string toString() const;

  double toDouble() const;

  Money perCent() const;

  int32_t integralPart() const { return m_integer; }

private:

  int32_t m_integer;

  int32_t m_mantissa;

  void internMantissa(std::string_view inVal, const bool inIsNegative);

};

The longer functions are defined in the .cpp file:

Money::Money(std::string_view inStr)

{

  auto offset = inStr.find('.');

  if (offset == std::string_view::npos)

    {

      m_integer = std::stoi(std::string(inStr));

      m_mantissa = 0;

    }

  else if (offset == 0)

    {

      m_integer = 0;

      internMantissa(inStr.substr(1), (inStr[0] == '-'));

    }

  else

    {

      std::string i(inStr.substr(0, offset));

      m_integer = std::stoi(i);

      internMantissa(inStr.substr(offset + 1), (inStr[0] == '-'));

    }

}

void Money::internMantissa(std::string_view inVal, const bool inIsNegative)

{

  auto len = inVal.length();

  std::string m;

  if (len > 6)

    {

      // trim last digits

      inVal.remove_suffix(len - 6);

      m = std::string(inVal);

    }

  else if (len < 6)

    {

      m = std::string(inVal);

      m.append(6 - len, '0');

    }

  else

    m = std::string(inVal);

  m_mantissa = std::stoi(m);

  if (inIsNegative)

    m_mantissa *= -1;

}

Money Money::operator+(const Money &inMoney) const

{

  if (inMoney.isZero())

    return *this;

  int32_t intVal = m_integer + inMoney.m_integer;

  int32_t mVal = m_mantissa + inMoney.m_mantissa;

  if (mVal > 999999)

    {

      intVal += 1;

      mVal -= 1000000;

    }

  else if (mVal < -999999)

    {

      intVal -= 1;

      mVal += 1000000;

    }

  return Money(intVal, mVal);

}

Because the values are small compared to the integer size, we don't have to check for overflows.  In full-scale production code we'd have a contract setting the largest value allowed for a field in a constructor or as a Money integer -- long before you reached 2^31 you'd be into ultra-high-risk/fat fingers territory.

Money &Money::operator+=(const Money &inMoney)

{

  if (inMoney.isZero())

    return *this;

  *this = this->operator+(inMoney);

  return *this;

}

Money &Money::operator-=(const Money &inMoney)

{

  if (inMoney.isZero())

    return *this;

  *this = this->operator-(inMoney);

  return *this;

}

Money Money::operator*(const int32_t inAmount) const

{

  if (inAmount == 0)

    {

      return {};

    }

  else if (inAmount == 1)

    {

      return *this;

    }

  int32_t intVal = m_integer * inAmount;

  int32_t mVal = m_mantissa * inAmount;

  auto val = std::abs(mVal);

  auto overage = val / 1000000;

  if (val > 999999)

    {

      mVal %= 1000000;

      if (m_mantissa > 0)

        {

          intVal += overage;

        }

      else

        {

          intVal -= overage;

        }

    }

  return Money(intVal, mVal);

}

Similar observations apply as to the addition case regarding limits.

std::string Money::toString() const

{

  std::ostringstream str;

  if ((m_integer == 0) && (m_mantissa < 0))

    str << "-";

  str << m_integer;

  if (m_mantissa != 0)

    {

      str << ".";

      auto val = std::abs(m_mantissa);

      if (val > 99999)

        str << val;

      else if (val > 9999)

        str << "0" << val;

      else if (val > 999)

        str << "00" << val;

      else if (val > 99)

        str << "000" << val;

      else if (val > 9)

        str << "0000" << val;

      else

        str << "00000" << val;

      return boost::trim_right_copy_if(str.str(), [](const char inVal) { return inVal == '0'; });

    }

  else

    return str.str();  

}

double Money::toDouble() const

{

  // Rare, so keep it simple and let the C library deal with it

  auto s = toString();

  return std::atof(s.c_str());

}

Money Money::perCent() const

{

  if (isZero())

    return {};

  

  auto intVal = m_integer / 100;

  auto adjust = (m_integer % 100) * 10000;

  auto mVal = m_mantissa / 100 + adjust;

  if (std::abs(m_mantissa % 100) > 49)

    {

      if (!isNegative())

mVal += 1;

      else

mVal -= 1;

    }

  return Money(intVal, mVal);

}

Note that this class also works effectively as a parameter class, to distinguish actual money amounts from other numbers.  This allows

Money CalculateCommission(const Money& inBase, const double inRate);

as opposed to

double CalculateCommission(const double inBase, const double inRate);

Note also that there are optimizations you can get by storing the two integers as the upper and lower parts of an int64_t and then doing arithmetic using the single integer.


 

Comments

Popular posts from this blog

Boundaries

Code Reviews

Overview