Skip to content

Commit

Permalink
Merge pull request #32 from alexpung/FutureContractSupport
Browse files Browse the repository at this point in the history
Future contract support
  • Loading branch information
alexpung authored Nov 10, 2023
2 parents c6a0ed8 + a20b744 commit b62d62f
Show file tree
Hide file tree
Showing 40 changed files with 968 additions and 442 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@using Syncfusion.Blazor.Grids;
@using Model.TaxEvents;
@using Model.UkTaxModel;
@using Model.UkTaxModel.Stocks;

@inject TradeCalculationResult tradeCalculationResult

Expand Down Expand Up @@ -35,8 +36,8 @@
<GridColumn Field=@nameof(TradeMatch.TradeMatchType) HeaderText="Match Type"> </GridColumn>
<GridColumn Field=@nameof(TradeMatch.MatchAcquisitionQty) HeaderText="Match Acquisition Quantity"></GridColumn>
<GridColumn Field=@nameof(TradeMatch.MatchDisposalQty) HeaderText="Match Disposal Quantity"></GridColumn>
<GridColumn Field=@nameof(TradeMatch.BaseCurrencyMatchDisposalValue) HeaderText="Match Disposal Value"></GridColumn>
<GridColumn Field=@nameof(TradeMatch.BaseCurrencyMatchAcquisitionValue) HeaderText="Match Acquisition Value"></GridColumn>
<GridColumn Field=@nameof(TradeMatch.BaseCurrencyMatchDisposalProceed) HeaderText="Match Disposal Value"></GridColumn>
<GridColumn Field=@nameof(TradeMatch.BaseCurrencyMatchAllowableCost) HeaderText="Match Acquisition Value"></GridColumn>
<GridColumn Field=@nameof(TradeMatch.AdditionalInformation) HeaderText="Additional Information"></GridColumn>
</GridColumns>
</SfGrid>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Enum;

public enum FuturePositionType
{
OPENLONG,
OPENSHORT,
CLOSELONG,
CLOSESHORT
}
6 changes: 3 additions & 3 deletions BlazorApp-Investment Tax Calculator/Model/DescribedMoney.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ namespace Model;

public record DescribedMoney : ITextFilePrintable
{
public string Description { get; set; } = "";
public required WrappedMoney Amount { get; set; }
public decimal FxRate { get; set; } = 1;
public string Description { get; init; } = "";
public required WrappedMoney Amount { get; init; }
public decimal FxRate { get; init; } = 1;

public WrappedMoney BaseCurrencyAmount => new(Amount.Amount * FxRate);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Model.UkTaxModel;
using Model.UkTaxModel.Stocks;

namespace Model.Interfaces;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
using Enum;

using Model.TaxEvents;
using Model.UkTaxModel;
using Model.UkTaxModel.Stocks;

namespace Model.Interfaces;
public interface ITradeTaxCalculation : ITextFilePrintable, IAssetDatedEvent
{
TradeType BuySell { get; init; }
bool CalculationCompleted { get; }
List<TradeMatch> MatchHistory { get; init; }
WrappedMoney TotalNetMoneyPaidOrReceived { get; }
WrappedMoney TotalCostOrProceed { get; }
decimal TotalQty { get; }
List<Trade> TradeList { get; init; }
WrappedMoney UnmatchedNetMoneyPaidOrReceived { get; }
WrappedMoney UnmatchedCostOrProceed { get; }
decimal UnmatchedQty { get; }
WrappedMoney TotalProceeds { get; }
WrappedMoney TotalAllowableCost { get; }
WrappedMoney Gain { get; }
WrappedMoney GetNetAmount(decimal qty) => TotalNetMoneyPaidOrReceived / TotalQty * qty;

(decimal matchedQty, WrappedMoney matchedValue) MatchAll();
(decimal matchedQty, WrappedMoney matchedValue) MatchQty(decimal demandedQty);
WrappedMoney GetProportionedCostOrProceed(decimal qty) => TotalCostOrProceed / TotalQty * qty;
void MatchWithSection104(UkSection104 ukSection104);
void MatchQty(decimal demandedQty);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
using Enum;

using Model;
using Model.TaxEvents;

using System.Collections.Immutable;

namespace TaxEvents;

public record FutureContractTrade : Trade
{
public required DescribedMoney ContractValue { get; set; }
public override AssetCatagoryType AssetType { get; set; } = AssetCatagoryType.FUTURE;
public FuturePositionType FuturePositionType { get; set; }

/// <summary>
/// Create a copy of the trade with quantity that is part of the whole trade
/// e.g. You buy 5 contract of a future. You can split it by using SplitTrade(2m) and SplitTrade(3m)
/// </summary>
/// <param name="qty"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public FutureContractTrade SplitTrade(decimal qty)
{
decimal splitRatio = qty / Quantity;
if (qty > Quantity) throw new ArgumentException($"Proportioned trade quantity {qty} must be less than total quantity {Quantity}");
return this with
{
Quantity = qty,
Description = $"Part of a trade with quantity {Quantity}",
GrossProceed = GrossProceed with { Amount = GrossProceed.Amount * splitRatio },
ContractValue = ContractValue with { Amount = ContractValue.Amount * splitRatio },
Expenses = Expenses.Select(expense => expense with { Amount = expense.Amount * splitRatio }).ToImmutableList()
};
}

public override string PrintToTextFile()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Enum;
using Model.Interfaces;
using Model.UkTaxModel;
using Model.UkTaxModel.Stocks;

namespace Model.TaxEvents;

Expand All @@ -19,12 +20,12 @@ public void ChangeTradeMatching(ITradeTaxCalculation trade1, ITradeTaxCalculatio
if (earlierTrade.BuySell == TradeType.BUY)
{
tradeMatch.MatchAcquisitionQty = tradeMatch.MatchAcquisitionQty * NumberBeforeSplit / NumberAfterSplit;
tradeMatch.BaseCurrencyMatchAcquisitionValue = earlierTrade.GetNetAmount(tradeMatch.MatchAcquisitionQty);
tradeMatch.BaseCurrencyMatchAllowableCost = earlierTrade.GetProportionedCostOrProceed(tradeMatch.MatchAcquisitionQty);
}
else
{
tradeMatch.MatchDisposalQty = tradeMatch.MatchDisposalQty * NumberBeforeSplit / NumberAfterSplit;
tradeMatch.BaseCurrencyMatchDisposalValue = earlierTrade.GetNetAmount(tradeMatch.MatchDisposalQty);
tradeMatch.BaseCurrencyMatchDisposalProceed = earlierTrade.GetProportionedCostOrProceed(tradeMatch.MatchDisposalQty);
}
tradeMatch.AdditionalInformation += $"Stock split occurred at {Date.Date} with ratio of {NumberAfterSplit} for {NumberBeforeSplit}\n";
}
Expand Down
12 changes: 11 additions & 1 deletion BlazorApp-Investment Tax Calculator/Model/TaxEvents/Trade.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Enum;

using Model.Interfaces;

using System.Collections.Immutable;
using System.Text;

namespace Model.TaxEvents;
Expand All @@ -11,7 +14,7 @@ public record Trade : TaxEvent, ITextFilePrintable
public virtual required decimal Quantity { get; set; }
public virtual required DescribedMoney GrossProceed { get; set; }
public string Description { get; set; } = string.Empty;
public List<DescribedMoney> Expenses { get; set; } = new List<DescribedMoney>();
public ImmutableList<DescribedMoney> Expenses { get; init; } = ImmutableList<DescribedMoney>.Empty;
public virtual WrappedMoney NetProceed
{
get
Expand All @@ -22,6 +25,13 @@ public virtual WrappedMoney NetProceed
}
}

public decimal RawQuantity => BuySell switch
{
TradeType.BUY => Quantity,
TradeType.SELL => Quantity * -1,
_ => throw new NotImplementedException($"Unknown trade type {BuySell}"),
};

protected string GetExpensesExplanation()
{
if (!Expenses.Any()) return string.Empty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Model.Interfaces;

using System.Collections.Concurrent;

namespace Model;

public class TradeCalculationResult
Expand All @@ -11,12 +13,20 @@ public TradeCalculationResult(ITaxYear taxYear)
_taxYear = taxYear;
}

public List<ITradeTaxCalculation> CalculatedTrade { get; set; } = new();
public ConcurrentBag<ITradeTaxCalculation> CalculatedTrade { get; set; } = new();
public IEnumerable<ITradeTaxCalculation> GetDisposals => CalculatedTrade.Where(trade => trade.BuySell == Enum.TradeType.SELL);

public void Clear()
{
CalculatedTrade.Clear();
}

public void SetResult(List<ITradeTaxCalculation> tradeTaxCalculations)
{
CalculatedTrade = tradeTaxCalculations;
foreach (var trade in tradeTaxCalculations)
{
CalculatedTrade.Add(trade);
}
}

private bool IsTradeInSelectedTaxYear(IEnumerable<int> selectedYears, ITradeTaxCalculation taxCalculation)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using Enum;

using Model.Interfaces;
using Model.UkTaxModel.Stocks;

using System.Text;

namespace Model.UkTaxModel;
namespace Model.UkTaxModel.Futures;

public record FutureTradeMatch : TradeMatch
{
Expand All @@ -29,7 +32,7 @@ public static FutureTradeMatch CreateSection104Match(decimal qty, WrappedMoney m
/// </summary>
/// <returns></returns>
public static FutureTradeMatch CreateTradeMatch(TaxMatchType taxMatchType, decimal qty, WrappedMoney matchDisposalContractValue, WrappedMoney matchAcquisitionContractValue, decimal fxRate,
WrappedMoney baseCurrencyTotalDealingExpense, ITradeTaxCalculation? matchedGroup = null)
WrappedMoney baseCurrencyTotalDealingExpense, ITradeTaxCalculation? matchedGroup = null)
{
WrappedMoney baseCurrencyMatchDisposalValue = WrappedMoney.GetBaseCurrencyZero();
WrappedMoney baseCurrencyMatchAcquisitionValue = WrappedMoney.GetBaseCurrencyZero();
Expand All @@ -51,8 +54,8 @@ public static FutureTradeMatch CreateTradeMatch(TaxMatchType taxMatchType, decim
ClosingFxRate = fxRate,
MatchDisposalContractValue = matchDisposalContractValue,
MatchAcquisitionContractValue = matchAcquisitionContractValue,
BaseCurrencyMatchAcquisitionValue = baseCurrencyTotalDealingExpense + baseCurrencyMatchAcquisitionValue,
BaseCurrencyMatchDisposalValue = baseCurrencyMatchDisposalValue,
BaseCurrencyMatchAllowableCost = baseCurrencyTotalDealingExpense + baseCurrencyMatchAcquisitionValue,
BaseCurrencyMatchDisposalProceed = baseCurrencyMatchDisposalValue,
MatchedBuyTrade = matchedGroup
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Enum;

using Model.UkTaxModel.Stocks;

using TaxEvents;

namespace Model.UkTaxModel.Futures;

public class FutureTradeTaxCalculation : TradeTaxCalculation
{
public override TradeType BuySell => PositionType is FuturePositionType.OPENLONG or FuturePositionType.OPENSHORT ? TradeType.BUY : TradeType.SELL;
public FuturePositionType PositionType => ((FutureContractTrade)TradeList[0]).FuturePositionType;
public WrappedMoney TotalContractValue { get; private set; }
public decimal ContractFxRate { get; private init; }
public WrappedMoney UnmatchedContractValue { get; private set; }
public WrappedMoney GetProportionedContractValue(decimal qty) => TotalContractValue * qty / TotalQty;
public FutureTradeTaxCalculation(IEnumerable<FutureContractTrade> trades) : base(trades)
{
TotalContractValue = trades.Sum(trade => trade.ContractValue.Amount);
ContractFxRate = trades.First().ContractValue.FxRate;
UnmatchedContractValue = TotalContractValue;
// This special case require modification as Future contract start from 0 cost
// normally commission are deducted from money received in a sell trade
// In case of open short is a buy trade and TotalCostOrProceed is cost of getting the contract commissions are added instead
// The opposite is true for CLOSELONG
if (PositionType is FuturePositionType.OPENSHORT or FuturePositionType.CLOSELONG)
{
TotalCostOrProceed *= -1;
UnmatchedCostOrProceed *= -1;
}
}

public override void MatchQty(decimal demandedQty)
{
base.MatchQty(demandedQty);
UnmatchedContractValue -= TotalContractValue * demandedQty / TotalQty;
}

public override void MatchWithSection104(UkSection104 ukSection104)
{
if (BuySell is TradeType.BUY)
{
Section104History section104History = ukSection104.AddAssets(this, UnmatchedQty, UnmatchedCostOrProceed, UnmatchedContractValue);
MatchHistory.Add(TradeMatch.CreateSection104Match(UnmatchedQty, UnmatchedCostOrProceed, WrappedMoney.GetBaseCurrencyZero(), section104History));
MatchQty(UnmatchedQty);
}
else if (BuySell is TradeType.SELL)
{
if (ukSection104.Quantity == 0m) return;
decimal matchQty = Math.Min(UnmatchedQty, ukSection104.Quantity);
Section104History section104History = ukSection104.RemoveAssets(this, UnmatchedQty);
WrappedMoney contractGain = GetProportionedContractValue(matchQty) + section104History.ContractValueChange;
WrappedMoney contractGainInBaseCurrency = new((contractGain * ContractFxRate).Amount);
WrappedMoney acquisitionValue = (section104History.ValueChange * -1) + GetProportionedCostOrProceed(matchQty);
WrappedMoney disposalValue = WrappedMoney.GetBaseCurrencyZero();
if (contractGainInBaseCurrency.Amount > 0)
{
disposalValue += contractGainInBaseCurrency;
}
else
{
acquisitionValue += contractGainInBaseCurrency * -1;
}
MatchHistory.Add(TradeMatch.CreateSection104Match(matchQty, acquisitionValue, disposalValue, section104History));
MatchQty(matchQty);
}
}
}
Loading

0 comments on commit b62d62f

Please sign in to comment.