//////////////////////////////////////////////////////////////////////////////////////////////////
//TradeMap
//Visually maps large trade events by combining trade records and displaying volume bubbles.
//Developed from the original study code by Sierrachart User385376 from 5.6.2026.
//Other sources: https://www.sierrachart.com/SupportBoard.php?ThreadID=85463&Page=1
//License: Free. But, if you modify/improve the study, don't be a douche. Post it for all users.
/////////////////////////////////////////////////////////////////////////////////////////////////

#include "sierrachart.h"
#include <vector>
#include <cmath>
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")

SCDLLName("TradeMap")

const int PKEY_LAST_SEQUENCE = 1;
const int PKEY_AGG_PRICE     = 2;
const int PKEY_AGG_BAR       = 3;
const int PKEY_AGG_VOLUME    = 4;
const int PKEY_AGG_SIDE      = 5;
const int PKEY_MARKER_INDEX  = 6;
const int PKEY_EVENTS_VECTOR = 7;
const int PKEY_HIST_BUILT    = 8;
const int PKEY_AGG_DATETIME  = 9;
const int PKEY_NEEDS_REDRAW   = 10;
const int PKEY_REBUILD_SIGNATURE = 12;
const int PKEY_AGG_MS = 13;
const int PKEY_AGG_ASKVOL = 14;
const int PKEY_AGG_BIDVOL = 15;
const int PKEY_AGG_PV = 16;
const int PKEY_PENDING_TIME_AGGS = 17;

const int TRADEMAP_MODE_PRICE_TYPE = 0;
const int TRADEMAP_MODE_TIME_PRICE = 1;
const int TRADEMAP_MODE_TIME_TYPE = 2;
const int TRADEMAP_MODE_TIME_TYPE_PRICE = 3;
const int TRADEMAP_MODE_BAR_PRICE = 4;
const int TRADEMAP_MODE_BAR_PRICE_TYPE = 5;
const int TRADEMAP_MODE_MILLISECONDS = 6;

struct TradeMapLargeTradeEvent
{
    int BarIndex;
    int MarkerIndex;
    SCDateTimeMS DateTime;
    float Price;
    int Volume;
    int Side;
    int CachedRadius;
    int CachedSizeSignature;
    COLORREF CachedColor;
    int CachedColorSignature;
};

struct TradeMapPendingAggregate
{
    int BarIndex;
    SCDateTimeMS DateTime;
    int64_t TimeKeyMS;
    float Price;
    int Volume;
    int Side;
    int AskVolume;
    int BidVolume;
    int EventVectorIndex;
};

void TradeMapDrawGDIBubbles(HWND WindowHandle, HDC DeviceContext, SCStudyInterfaceRef sc);

inline int TradeMapClampInt(int Value, int MinValue, int MaxValue)
{
    if (Value < MinValue) return MinValue;
    if (Value > MaxValue) return MaxValue;
    return Value;
}

inline void TradeMapNormalizeMarkerSizes(int& MinSize, int& MaxSize)
{
    MinSize = TradeMapClampInt(MinSize, 1, 500);
    MaxSize = TradeMapClampInt(MaxSize, 1, 500);

    if (MaxSize < MinSize)
        MaxSize = MinSize;
}

inline float TradeMapGetMarkerSize(int Volume, int Threshold, int MinSize, int MaxSize)
{
    TradeMapNormalizeMarkerSizes(MinSize, MaxSize);

    if (Threshold <= 0)
        return (float)MinSize;

    const float Ratio = (float)Volume / (float)Threshold;

    float Size = (float)MinSize * powf(Ratio, 1.55f);

    if (Size < (float)MinSize)
        Size = (float)MinSize;

    if (Size > (float)MaxSize)
        Size = (float)MaxSize;

    return Size;
}

inline bool TradeMapShouldRenderBar(
    SCStudyInterfaceRef sc,
    int BarIndex,
    int RenderLookbackBars)
{
    if (RenderLookbackBars <= 0)
        return true;

    const int FirstRenderBar =
        sc.ArraySize - RenderLookbackBars;

    return BarIndex >= FirstRenderBar;
}

inline int TradeMapDetermineIntradaySide(const s_IntradayRecord& Record)
{
    if (Record.AskVolume > Record.BidVolume)
        return SC_TS_ASK;

    if (Record.BidVolume > Record.AskVolume)
        return SC_TS_BID;

    return 0;
}

inline int TradeMapGetDominantSide(int AskVolume, int BidVolume, int LastSide)
{
    if (AskVolume > BidVolume)
        return SC_TS_ASK;

    if (BidVolume > AskVolume)
        return SC_TS_BID;

    return LastSide;
}

inline int TradeMapGetBarPriceDisplayVolume(
    int AskVolume,
    int BidVolume,
    bool UseDominantSideVolume)
{
    if (UseDominantSideVolume)
        return AskVolume >= BidVolume ? AskVolume : BidVolume;

    return AskVolume + BidVolume;
}

inline int64_t TradeMapGetMillisecondTimeKey(SCDateTimeMS DateTime)
{
    return ((const SCDateTime&)DateTime).GetInternalDateTime() / 1000;
}

inline bool TradeMapShouldMerge(
    int CombineMode,
    SCDateTimeMS CurrentDT,
    SCDateTimeMS AggDT,
    float Price,
    double AggPrice,
    int Side,
    int AggSide)
{
    const bool SameTimestampMS =
        TradeMapGetMillisecondTimeKey(CurrentDT)
        == TradeMapGetMillisecondTimeKey(AggDT);

    switch (CombineMode)
    {
        case TRADEMAP_MODE_PRICE_TYPE:
            return Price == AggPrice
                && Side == AggSide;

        case TRADEMAP_MODE_TIME_PRICE:
            return SameTimestampMS
                && Price == AggPrice;

        case TRADEMAP_MODE_TIME_TYPE:
            return SameTimestampMS
                && Side == AggSide;

        case TRADEMAP_MODE_TIME_TYPE_PRICE:
            return SameTimestampMS
                && Side == AggSide
                && Price == AggPrice;
    }

    return false;
}

inline bool TradeMapShouldMergeFixedMSBucket(
    SCDateTimeMS CurrentDT,
    int AggBucketID,
    int BucketSizeMS)
{
    if (BucketSizeMS <= 0)
        return false;

    const int CurrentBucketID =
        CurrentDT.GetTimeInMilliseconds() / BucketSizeMS;

    return CurrentBucketID == AggBucketID;
}


void TradeMapStoreEvent(
    SCStudyInterfaceRef sc,
    std::vector<TradeMapLargeTradeEvent>* Events,
    int& MarkerIndex,
    int BarIndex,
    SCDateTimeMS EventDateTime,
    float Price,
    int Volume,
    int Side,
    int Threshold)
{
    if (Volume < Threshold)
        return;

    if (BarIndex < 0 || BarIndex >= sc.ArraySize)
        return;

    ++MarkerIndex;

    if (MarkerIndex >= 1000000000)
        MarkerIndex = 0;

    if (Events != NULL)
    {
        TradeMapLargeTradeEvent Event;

        Event.BarIndex = BarIndex;
        Event.MarkerIndex = MarkerIndex;
        Event.DateTime = EventDateTime;
        Event.Price = static_cast<float>(Price);
        Event.Volume = Volume;
        Event.Side = Side;
        Event.CachedRadius = 0;
        Event.CachedSizeSignature = 0;
        Event.CachedColor = RGB(0, 0, 0);
        Event.CachedColorSignature = 0;

        Events->push_back(Event);
    }
}

inline int TradeMapGetSizeSignature(
    int Threshold,
    int MinSize,
    int MaxSize)
{
    int Signature = 17;
    Signature = Signature * 31 + Threshold;
    Signature = Signature * 31 + MinSize;
    Signature = Signature * 31 + MaxSize;
    return Signature;
}

inline int TradeMapGetCachedRadius(
    TradeMapLargeTradeEvent& Event,
    int SizeSignature,
    int Threshold,
    int MinSize,
    int MaxSize)
{
    if (Event.CachedSizeSignature != SizeSignature
        || Event.CachedRadius <= 0)
    {
        const float MarkerSize =
            TradeMapGetMarkerSize(
                Event.Volume,
                Threshold,
                MinSize,
                MaxSize);

        Event.CachedRadius =
            TradeMapClampInt((int)(MarkerSize * 0.5f), 2, 80);

        Event.CachedSizeSignature =
            SizeSignature;
    }

    return Event.CachedRadius;
}

inline int TradeMapGetColorSignature(
    int Threshold,
    int MinSize,
    int MaxSize,
    COLORREF BuyMinColor,
    COLORREF BuyMaxColor,
    COLORREF SellMinColor,
    COLORREF SellMaxColor)
{
    int Signature = 23;
    Signature = Signature * 31 + Threshold;
    Signature = Signature * 31 + MinSize;
    Signature = Signature * 31 + MaxSize;
    Signature = Signature * 31 + (int)BuyMinColor;
    Signature = Signature * 31 + (int)BuyMaxColor;
    Signature = Signature * 31 + (int)SellMinColor;
    Signature = Signature * 31 + (int)SellMaxColor;
    return Signature;
}

inline int TradeMapInterpolateColorChannel(
    int MinValue,
    int MaxValue,
    float Intensity)
{
    return TradeMapClampInt(
        (int)((float)MinValue + ((float)(MaxValue - MinValue) * Intensity) + 0.5f),
        0,
        255);
}

inline COLORREF TradeMapInterpolateColor(
    COLORREF MinColor,
    COLORREF MaxColor,
    float Intensity)
{
    if (Intensity < 0.0f)
        Intensity = 0.0f;

    if (Intensity > 1.0f)
        Intensity = 1.0f;

    return RGB(
        TradeMapInterpolateColorChannel(GetRValue(MinColor), GetRValue(MaxColor), Intensity),
        TradeMapInterpolateColorChannel(GetGValue(MinColor), GetGValue(MaxColor), Intensity),
        TradeMapInterpolateColorChannel(GetBValue(MinColor), GetBValue(MaxColor), Intensity));
}

inline COLORREF TradeMapGetCachedGradientColor(
    TradeMapLargeTradeEvent& Event,
    int ColorSignature,
    int Threshold,
    int MinSize,
    int MaxSize,
    COLORREF BuyMinColor,
    COLORREF BuyMaxColor,
    COLORREF SellMinColor,
    COLORREF SellMaxColor)
{
    if (Event.CachedColorSignature != ColorSignature)
    {
        const float MarkerSize =
            TradeMapGetMarkerSize(
                Event.Volume,
                Threshold,
                MinSize,
                MaxSize);

        float Intensity = 1.0f;

        if (MaxSize > MinSize)
            Intensity = (MarkerSize - (float)MinSize) / (float)(MaxSize - MinSize);

        Event.CachedColor =
            Event.Side == SC_TS_ASK
            ? TradeMapInterpolateColor(BuyMinColor, BuyMaxColor, Intensity)
            : TradeMapInterpolateColor(SellMinColor, SellMaxColor, Intensity);

        Event.CachedColorSignature =
            ColorSignature;
    }

    return Event.CachedColor;
}

inline bool TradeMapIsTimestampKeyMode(int CombineMode)
{
    return CombineMode >= TRADEMAP_MODE_TIME_PRICE
        && CombineMode <= TRADEMAP_MODE_TIME_TYPE_PRICE;
}

inline bool TradeMapPendingAggregateMatches(
    const TradeMapPendingAggregate& Aggregate,
    int CombineMode,
    int BarIndex,
    int64_t TimeKeyMS,
    float Price,
    int Side)
{
    if (Aggregate.BarIndex != BarIndex || Aggregate.TimeKeyMS != TimeKeyMS)
        return false;

    switch (CombineMode)
    {
        case TRADEMAP_MODE_TIME_PRICE:
            return Aggregate.Price == Price;

        case TRADEMAP_MODE_TIME_TYPE:
            return Aggregate.Side == Side;

        case TRADEMAP_MODE_TIME_TYPE_PRICE:
            return Aggregate.Side == Side
                && Aggregate.Price == Price;
    }

    return false;
}

void TradeMapFlushPendingTimeAggregates(
    SCStudyInterfaceRef sc,
    std::vector<TradeMapLargeTradeEvent>* Events,
    std::vector<TradeMapPendingAggregate>* PendingAggregates,
    int& MarkerIndex,
    int Threshold)
{
    if (PendingAggregates == NULL)
        return;

    for (int i = 0; i < (int)PendingAggregates->size(); ++i)
    {
        const TradeMapPendingAggregate& Aggregate =
            (*PendingAggregates)[i];

        TradeMapStoreEvent(
            sc,
            Events,
            MarkerIndex,
            Aggregate.BarIndex,
            Aggregate.DateTime,
            Aggregate.Price,
            Aggregate.Volume,
            Aggregate.Side,
            Threshold);
    }

    PendingAggregates->clear();
}

void TradeMapAddTimeKeyAggregate(
    SCStudyInterfaceRef sc,
    std::vector<TradeMapLargeTradeEvent>* Events,
    std::vector<TradeMapPendingAggregate>* PendingAggregates,
    int& MarkerIndex,
    int Threshold,
    int CombineMode,
    int BarIndex,
    SCDateTimeMS DateTime,
    float Price,
    int Volume,
    int Side)
{
    if (PendingAggregates == NULL)
        return;

    const int64_t TimeKeyMS =
        TradeMapGetMillisecondTimeKey(DateTime);

    if (!PendingAggregates->empty())
    {
        const TradeMapPendingAggregate& FirstAggregate =
            PendingAggregates->front();

        if (FirstAggregate.BarIndex != BarIndex
            || FirstAggregate.TimeKeyMS != TimeKeyMS)
        {
            TradeMapFlushPendingTimeAggregates(
                sc,
                Events,
                PendingAggregates,
                MarkerIndex,
                Threshold);
        }
    }

    for (int i = 0; i < (int)PendingAggregates->size(); ++i)
    {
        TradeMapPendingAggregate& Aggregate =
            (*PendingAggregates)[i];

        if (TradeMapPendingAggregateMatches(
            Aggregate,
            CombineMode,
            BarIndex,
            TimeKeyMS,
            Price,
            Side))
        {
            Aggregate.Volume += Volume;
            Aggregate.DateTime = DateTime;

            if (Side == SC_TS_ASK)
                Aggregate.AskVolume += Volume;
            else if (Side == SC_TS_BID)
                Aggregate.BidVolume += Volume;

            Aggregate.Side =
                TradeMapGetDominantSide(
                    Aggregate.AskVolume,
                    Aggregate.BidVolume,
                    Side);

            return;
        }
    }

    TradeMapPendingAggregate Aggregate;
    Aggregate.BarIndex = BarIndex;
    Aggregate.DateTime = DateTime;
    Aggregate.TimeKeyMS = TimeKeyMS;
    Aggregate.Price = Price;
    Aggregate.Volume = Volume;
    Aggregate.Side = Side;
    Aggregate.AskVolume =
        Side == SC_TS_ASK ? Volume : 0;
    Aggregate.BidVolume =
        Side == SC_TS_BID ? Volume : 0;
    Aggregate.EventVectorIndex = -1;

    PendingAggregates->push_back(Aggregate);
}

void TradeMapAddOrUpdateBarPriceAggregate(
    SCStudyInterfaceRef sc,
    std::vector<TradeMapLargeTradeEvent>* Events,
    std::vector<TradeMapPendingAggregate>* PendingAggregates,
    int& MarkerIndex,
    int Threshold,
    bool UseDominantSideVolume,
    int BarIndex,
    SCDateTimeMS DateTime,
    float Price,
    int Volume,
    int Side)
{
    if (PendingAggregates == NULL)
        return;

    if (!PendingAggregates->empty()
        && PendingAggregates->front().BarIndex != BarIndex)
    {
        PendingAggregates->clear();
    }

    for (int i = 0; i < (int)PendingAggregates->size(); ++i)
    {
        TradeMapPendingAggregate& Aggregate =
            (*PendingAggregates)[i];

        if (Aggregate.BarIndex != BarIndex
            || Aggregate.Price != Price)
        {
            continue;
        }

        Aggregate.Volume += Volume;
        Aggregate.DateTime = DateTime;

        if (Side == SC_TS_ASK)
            Aggregate.AskVolume += Volume;
        else if (Side == SC_TS_BID)
            Aggregate.BidVolume += Volume;

        Aggregate.Side =
            TradeMapGetDominantSide(
                Aggregate.AskVolume,
                Aggregate.BidVolume,
                Side);

        const int DisplayVolume =
            TradeMapGetBarPriceDisplayVolume(
                Aggregate.AskVolume,
                Aggregate.BidVolume,
                UseDominantSideVolume);

        if (Aggregate.EventVectorIndex >= 0
            && Events != NULL
            && Aggregate.EventVectorIndex < (int)Events->size())
        {
            TradeMapLargeTradeEvent& Event =
                (*Events)[Aggregate.EventVectorIndex];

            Event.DateTime = Aggregate.DateTime;
            Event.Volume = DisplayVolume;
            Event.Side = Aggregate.Side;
            Event.CachedRadius = 0;
            Event.CachedSizeSignature = 0;
            Event.CachedColorSignature = 0;
        }
        else if (DisplayVolume >= Threshold)
        {
            const int EventIndex =
                Events != NULL
                ? (int)Events->size()
                : -1;

            TradeMapStoreEvent(
                sc,
                Events,
                MarkerIndex,
                Aggregate.BarIndex,
                Aggregate.DateTime,
                Aggregate.Price,
                DisplayVolume,
                Aggregate.Side,
                Threshold);

            Aggregate.EventVectorIndex = EventIndex;
        }

        return;
    }

    TradeMapPendingAggregate Aggregate;
    Aggregate.BarIndex = BarIndex;
    Aggregate.DateTime = DateTime;
    Aggregate.TimeKeyMS = 0;
    Aggregate.Price = Price;
    Aggregate.Volume = Volume;
    Aggregate.Side = Side;
    Aggregate.AskVolume =
        Side == SC_TS_ASK ? Volume : 0;
    Aggregate.BidVolume =
        Side == SC_TS_BID ? Volume : 0;
    Aggregate.EventVectorIndex = -1;

    const int DisplayVolume =
        TradeMapGetBarPriceDisplayVolume(
            Aggregate.AskVolume,
            Aggregate.BidVolume,
            UseDominantSideVolume);

    if (DisplayVolume >= Threshold)
    {
        const int EventIndex =
            Events != NULL
            ? (int)Events->size()
            : -1;

        TradeMapStoreEvent(
            sc,
            Events,
            MarkerIndex,
            Aggregate.BarIndex,
            Aggregate.DateTime,
            Aggregate.Price,
            DisplayVolume,
            Aggregate.Side,
            Threshold);

        Aggregate.EventVectorIndex = EventIndex;
    }

    PendingAggregates->push_back(Aggregate);
}

int TradeMapRebuildCurrentDateFromIntradayFile(
    SCStudyInterfaceRef sc,
    std::vector<TradeMapLargeTradeEvent>* Events,
    int& MarkerIndex,
    int Threshold,
    int CombineMode,
    int VolumeMinFilter,
    int VolumeMaxFilter)
{
    if (sc.ArraySize <= 0)
        return 0;

    if (Events != NULL)
        Events->clear();

    MarkerIndex = -1;

    SCInputRef MillisecondsWindow = sc.Input[2];

    int StartBar = 0;

    double AggPrice = 0.0f;
    int AggBar = -1;
    int AggVolume = 0;
    int AggSide = 0;
    SCDateTimeMS AggDateTime = 0;
    int AggMS = 0;
    int AggAskVolume = 0;
    int AggBidVolume = 0;
    double AggPriceVolume = 0.0;
    std::vector<TradeMapPendingAggregate> PendingTimeAggregates;

    for (int BarIndex = StartBar; BarIndex < sc.ArraySize; ++BarIndex)
    {
        s_IntradayRecord Record;

        int SubIndex = 0;
        bool FirstRead = true;

        while (true)
        {
            IntradayFileLockActionEnum LockAction =
                FirstRead
                ? IFLA_LOCK_READ_HOLD
                : IFLA_NO_CHANGE;

            FirstRead = false;

            if (!sc.ReadIntradayFileRecordForBarIndexAndSubIndex(
                BarIndex,
                SubIndex,
                Record,
                LockAction))
            {
                break;
            }

            ++SubIndex;

            const int Side =
                TradeMapDetermineIntradaySide(Record);

            if (Side != SC_TS_ASK && Side != SC_TS_BID)
                continue;

            const int Volume =
                (int)Record.TotalVolume;

            if (Volume < VolumeMinFilter)
                continue;

            if (VolumeMaxFilter > 0 && Volume > VolumeMaxFilter)
                continue;

            const float Price =
                (float)Record.Close * sc.RealTimePriceMultiplier;

            SCDateTimeMS DT =
                Record.DateTime + sc.TimeScaleAdjustment;

            if (TradeMapIsTimestampKeyMode(CombineMode))
            {
                TradeMapAddTimeKeyAggregate(
                    sc,
                    Events,
                    &PendingTimeAggregates,
                    MarkerIndex,
                    Threshold,
                    CombineMode,
                    BarIndex,
                    DT,
                    Price,
                    Volume,
                    Side);

                continue;
            }

            if (CombineMode == TRADEMAP_MODE_BAR_PRICE
                || CombineMode == TRADEMAP_MODE_BAR_PRICE_TYPE)
            {
                TradeMapAddOrUpdateBarPriceAggregate(
                    sc,
                    Events,
                    &PendingTimeAggregates,
                    MarkerIndex,
                    Threshold,
                    CombineMode == TRADEMAP_MODE_BAR_PRICE_TYPE,
                    BarIndex,
                    DT,
                    Price,
                    Volume,
                    Side);

                continue;
            }

            bool Merge = false;

            if (CombineMode == TRADEMAP_MODE_MILLISECONDS)
            {
                Merge =
                    AggVolume > 0
                    && TradeMapShouldMergeFixedMSBucket(
                        DT,
                        AggMS,
                        MillisecondsWindow.GetInt());
            }
            else
            {
                Merge =
                    AggVolume > 0
                    && AggBar == BarIndex
                    && TradeMapShouldMerge(
                        CombineMode,
                        DT,
                        AggDateTime,
                        Price,
                        static_cast<float>(AggPrice),
                        Side,
                        AggSide);
            }

            if (Merge)
            {
                AggVolume += Volume;

                if (CombineMode == TRADEMAP_MODE_MILLISECONDS)
                {
                    if (Side == SC_TS_ASK)
                        AggAskVolume += Volume;
                    else
                        AggBidVolume += Volume;

                    AggPriceVolume +=
                        (double)Price * (double)Volume;

                    AggPrice =
                        AggPriceVolume / static_cast<double>(AggVolume);

                    AggSide =
                        AggAskVolume >= AggBidVolume
                        ? SC_TS_ASK
                        : SC_TS_BID;
                }
            }
            else
            {
                TradeMapStoreEvent(
                    sc,
                    Events,
                    MarkerIndex,
                    AggBar,
                    AggDateTime,
                    static_cast<float>(AggPrice),
                    AggVolume,
                    AggSide,
                    Threshold);

                AggPrice = Price;
                AggBar = BarIndex;
                AggVolume = Volume;
                AggSide = Side;
                AggDateTime = DT;
                AggMS =
                    MillisecondsWindow.GetInt() > 0
                    ? (DT.GetTimeInMilliseconds() / MillisecondsWindow.GetInt())
                    : 0;

                AggAskVolume =
                    Side == SC_TS_ASK
                    ? Volume
                    : 0;

                AggBidVolume =
                    Side == SC_TS_BID
                    ? Volume
                    : 0;

                AggPriceVolume =
                    (double)Price * (double)Volume;
            }
        }

        sc.ReadIntradayFileRecordForBarIndexAndSubIndex(
            -1,
            -1,
            Record,
            IFLA_RELEASE_AFTER_READ);
    }

    if (TradeMapIsTimestampKeyMode(CombineMode))
    {
        TradeMapFlushPendingTimeAggregates(
            sc,
            Events,
            &PendingTimeAggregates,
            MarkerIndex,
            Threshold);
    }
    else if (CombineMode != TRADEMAP_MODE_BAR_PRICE
        && CombineMode != TRADEMAP_MODE_BAR_PRICE_TYPE)
    {
        TradeMapStoreEvent(
            sc,
            Events,
            MarkerIndex,
            AggBar,
            AggDateTime,
            static_cast<float>(AggPrice),
            AggVolume,
            AggSide,
            Threshold);
    }

    return Events != NULL
        ? (int)Events->size()
        : 0;
}


void TradeMapDrawGDIBubbles(HWND WindowHandle, HDC DeviceContext, SCStudyInterfaceRef sc)
{
    if (sc.HideStudy)
        return;

    SCInputRef MinimumVolumeThreshold = sc.Input[0];
    SCInputRef BuyColor = sc.Input[5];
    SCInputRef BuyMaxColor = sc.Input[6];
    SCInputRef SellColor = sc.Input[7];
    SCInputRef SellMaxColor = sc.Input[8];
    SCInputRef HollowMarker = sc.Input[9];
    SCInputRef Transparency = sc.Input[10];
    SCInputRef MinMarkerSize = sc.Input[11];
    SCInputRef MaxMarkerSize = sc.Input[12];
    SCInputRef ShowVolumeLabel = sc.Input[13];
    SCInputRef VolumeLabelMinVolume = sc.Input[14];
    SCInputRef VolumeLabelColor = sc.Input[15];
    SCInputRef VolumeLabelFontSize = sc.Input[16];
    SCInputRef RenderLookbackBars = sc.Input[20];

    std::vector<TradeMapLargeTradeEvent>* Events =
        (std::vector<TradeMapLargeTradeEvent>*)
        sc.GetPersistentPointer(PKEY_EVENTS_VECTOR);

    if (Events == NULL || Events->empty())
        return;

    const int FirstVisibleBar =
        sc.IndexOfFirstVisibleBar;

    const int LastVisibleBar =
        sc.IndexOfLastVisibleBar;

    const int RenderLookbackBarsValue =
        RenderLookbackBars.GetInt();

    int FirstRenderBar = FirstVisibleBar;

    if (RenderLookbackBarsValue > 0)
    {
        const int LookbackFirstBar =
            sc.ArraySize - RenderLookbackBarsValue;

        if (LookbackFirstBar > FirstRenderBar)
            FirstRenderBar = LookbackFirstBar;
    }

    if (LastVisibleBar < FirstRenderBar)
        return;

    const int ThresholdValue =
        MinimumVolumeThreshold.GetInt();

    int MinMarkerSizeValue =
        MinMarkerSize.GetInt();

    int MaxMarkerSizeValue =
        MaxMarkerSize.GetInt();

    TradeMapNormalizeMarkerSizes(
        MinMarkerSizeValue,
        MaxMarkerSizeValue);

    const int SizeSignature =
        TradeMapGetSizeSignature(
            ThresholdValue,
            MinMarkerSizeValue,
            MaxMarkerSizeValue);

    const bool HollowMarkerValue =
        HollowMarker.GetYesNo() != 0;

    const bool ShowVolumeLabelValue =
        ShowVolumeLabel.GetYesNo() != 0;

    const int VolumeLabelMinVolumeValue =
        VolumeLabelMinVolume.GetInt() > 0
        ? VolumeLabelMinVolume.GetInt()
        : ThresholdValue;

    const COLORREF BuyColorRef =
        BuyColor.GetColor();

    const COLORREF SellColorRef =
        SellColor.GetColor();

    const COLORREF BuyMaxColorRef =
        BuyMaxColor.GetColor();

    const COLORREF SellMaxColorRef =
        SellMaxColor.GetColor();

    const int ColorSignature =
        TradeMapGetColorSignature(
            ThresholdValue,
            MinMarkerSizeValue,
            MaxMarkerSizeValue,
            BuyColorRef,
            BuyMaxColorRef,
            SellColorRef,
            SellMaxColorRef);

    const int Alpha =
        TradeMapClampInt(
            255 - (Transparency.GetInt() * 255 / 100),
            0,
            255);

    RECT ClipRectangle;
    ClipRectangle.left = sc.StudyRegionLeftCoordinate;
    ClipRectangle.top = sc.StudyRegionTopCoordinate;
    ClipRectangle.right = sc.StudyRegionRightCoordinate;
    ClipRectangle.bottom = sc.StudyRegionBottomCoordinate;

    static ULONG_PTR GDIPlusToken = 0;

    if (GDIPlusToken == 0)
    {
        Gdiplus::GdiplusStartupInput GDIPlusStartupInput;
        Gdiplus::GdiplusStartup(&GDIPlusToken, &GDIPlusStartupInput, NULL);
    }

    Gdiplus::Graphics Graphics(DeviceContext);
    Graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
    Graphics.SetClip(Gdiplus::Rect(
        ClipRectangle.left,
        ClipRectangle.top,
        ClipRectangle.right - ClipRectangle.left,
        ClipRectangle.bottom - ClipRectangle.top));

    const float TextHeight =
        (float)TradeMapClampInt(VolumeLabelFontSize.GetInt() + 5, 7, 36);

    Gdiplus::FontFamily TextFontFamily(L"Arial");
    Gdiplus::Font TextFont(
        &TextFontFamily,
        TextHeight,
        Gdiplus::FontStyleBold,
        Gdiplus::UnitPixel);

    Gdiplus::StringFormat TextFormat;
    TextFormat.SetAlignment(Gdiplus::StringAlignmentCenter);
    TextFormat.SetLineAlignment(Gdiplus::StringAlignmentCenter);
    TextFormat.SetFormatFlags(Gdiplus::StringFormatFlagsNoWrap);

    COLORREF TextColorRef = VolumeLabelColor.GetColor();
    Gdiplus::SolidBrush TextBrush(
        Gdiplus::Color(
            255,
            GetRValue(TextColorRef),
            GetGValue(TextColorRef),
            GetBValue(TextColorRef)));

    for (int i = (int)Events->size() - 1; i >= 0; --i)
    {
        TradeMapLargeTradeEvent& Event = (*Events)[i];

        if (Event.BarIndex < FirstRenderBar)
            break;

        if (Event.BarIndex > LastVisibleBar)
            continue;

        const int Radius =
            TradeMapGetCachedRadius(
                Event,
                SizeSignature,
                ThresholdValue,
                MinMarkerSizeValue,
                MaxMarkerSizeValue);

        const int X =
            sc.BarIndexToXPixelCoordinate(Event.BarIndex);

        const int Y =
            sc.RegionValueToYPixelCoordinate(Event.Price, sc.GraphRegion);

        if (X + Radius < ClipRectangle.left
            || X - Radius > ClipRectangle.right
            || Y + Radius < ClipRectangle.top
            || Y - Radius > ClipRectangle.bottom)
        {
            continue;
        }

        const COLORREF EventColor =
            TradeMapGetCachedGradientColor(
                Event,
                ColorSignature,
                ThresholdValue,
                MinMarkerSizeValue,
                MaxMarkerSizeValue,
                BuyColorRef,
                BuyMaxColorRef,
                SellColorRef,
                SellMaxColorRef);

        Gdiplus::Color BubbleColor(
            Alpha,
            GetRValue(EventColor),
            GetGValue(EventColor),
            GetBValue(EventColor));

        if (HollowMarkerValue)
        {
            Gdiplus::Pen BubblePen(BubbleColor, 1.0f);
            Graphics.DrawEllipse(
                &BubblePen,
                X - Radius,
                Y - Radius,
                Radius * 2,
                Radius * 2);
        }
        else
        {
            Gdiplus::SolidBrush BubbleBrush(BubbleColor);
            Graphics.FillEllipse(
                &BubbleBrush,
                X - Radius,
                Y - Radius,
                Radius * 2,
                Radius * 2);
        }

        if (ShowVolumeLabelValue
            && Event.Volume >= VolumeLabelMinVolumeValue)
        {
            wchar_t WideLabel[32] = {};
            swprintf_s(WideLabel, 32, L"%d", Event.Volume);

            Gdiplus::RectF TextRectangle(
                (Gdiplus::REAL)(X - Radius),
                (Gdiplus::REAL)(Y - Radius),
                (Gdiplus::REAL)(Radius * 2),
                (Gdiplus::REAL)(Radius * 2));

            Graphics.DrawString(
                WideLabel,
                -1,
                &TextFont,
                TextRectangle,
                &TextFormat,
                &TextBrush);
        }
    }
}


SCSFExport scsf_TradeMap(SCStudyInterfaceRef sc)
{
    SCInputRef MinimumVolumeThreshold = sc.Input[0];
    SCInputRef CombineMode            = sc.Input[1];
    SCInputRef MillisecondsWindow     = sc.Input[2];
    SCInputRef VolumeMinFilter        = sc.Input[3];
    SCInputRef VolumeMaxFilter        = sc.Input[4];
    SCInputRef BuyColor               = sc.Input[5];
    SCInputRef BuyMaxColor            = sc.Input[6];
    SCInputRef SellColor              = sc.Input[7];
    SCInputRef SellMaxColor           = sc.Input[8];
    SCInputRef HollowMarker           = sc.Input[9];
    SCInputRef Transparency           = sc.Input[10];
    SCInputRef MinMarkerSize          = sc.Input[11];
    SCInputRef MaxMarkerSize          = sc.Input[12];
    SCInputRef ShowVolumeLabel        = sc.Input[13];
    SCInputRef VolumeLabelMinVolume   = sc.Input[14];
    SCInputRef VolumeLabelColor       = sc.Input[15];
    SCInputRef VolumeLabelFontSize    = sc.Input[16];
    SCInputRef RebuildCurrentDate     = sc.Input[17];
    SCInputRef RenderLookbackBars     = sc.Input[20];

    if (sc.SetDefaults)
    {
        sc.GraphName =
            "TradeMap";

        sc.StudyDescription =
            "Mapping of Large Trade Activity Using Combined Records";

        sc.AutoLoop = 0;
        sc.GraphRegion = 0;
        sc.UpdateAlways = 1;
        sc.MaintainAdditionalChartDataArrays = 1;

        MinimumVolumeThreshold.Name =
            "Minimum Bubble Volume";

        MinimumVolumeThreshold.SetInt(50);

        CombineMode.Name =
            "Combine Mode";

        CombineMode.SetCustomInputStrings(
            "Same Price/Type;"
            "Same Time/Price;"
            "Same Time/Type;"
            "Same Time/Type/Price;"
            "Same Bar/Price;"
            "Same Bar/Price/Type;"
            "Milliseconds");

        CombineMode.SetCustomInputIndex(2);

        MillisecondsWindow.Name =
            "Milliseconds Window";

        MillisecondsWindow.SetInt(500);

        MillisecondsWindow.SetIntLimits(1, 10000);

        VolumeMinFilter.Name =
            "Trade Volume Filter Minimum";

        VolumeMinFilter.SetInt(1);

        VolumeMaxFilter.Name =
            "Trade Volume Filter Maximum (0=Disabled)";

        VolumeMaxFilter.SetInt(0);

        BuyColor.Name =
            "Buy Aggression Min Color";

        BuyColor.SetColor(RGB(0,255,180));

        BuyMaxColor.Name =
            "Buy Aggression Max Color";

        BuyMaxColor.SetColor(RGB(0,96,67));

        SellColor.Name =
            "Sell Aggression Min Color";

        SellColor.SetColor(RGB(255,75,75));

        SellMaxColor.Name =
            "Sell Aggression Max Color";

        SellMaxColor.SetColor(RGB(164,0,0));

        HollowMarker.Name =
            "Use Hollow Bubbles";

        HollowMarker.SetYesNo(0);

        Transparency.Name =
            "Bubble Transparency";

        Transparency.SetInt(60);

        MinMarkerSize.Name =
            "Minimum Bubble Size";

        MinMarkerSize.SetInt(25);

        MaxMarkerSize.Name =
            "Maximum Bubble Size";

        MaxMarkerSize.SetInt(60);

        ShowVolumeLabel.Name =
            "Show Volume Label";

        ShowVolumeLabel.SetYesNo(1);

        VolumeLabelMinVolume.Name =
            "Minimum Volume To Show Label";

        VolumeLabelMinVolume.SetInt(100);

        VolumeLabelMinVolume.SetIntLimits(0, 1000000);

        VolumeLabelColor.Name =
            "Volume Label Color";

        VolumeLabelColor.SetColor(RGB(255,255,255));

        VolumeLabelFontSize.Name =
            "Volume Label Font Size";

        VolumeLabelFontSize.SetInt(8);

        RebuildCurrentDate.Name =
            "Rebuild Current Chart Date";

        RebuildCurrentDate.SetYesNo(1);

        RenderLookbackBars.Name =
            "Render Lookback Bars";

        RenderLookbackBars.SetInt(300);

        RenderLookbackBars.SetIntLimits(1, 20000);

        return;
    }

    sc.p_GDIFunction = TradeMapDrawGDIBubbles;

    std::vector<TradeMapLargeTradeEvent>* Events =
        (std::vector<TradeMapLargeTradeEvent>*)
        sc.GetPersistentPointer(PKEY_EVENTS_VECTOR);

    std::vector<TradeMapPendingAggregate>* PendingTimeAggregates =
        (std::vector<TradeMapPendingAggregate>*)
        sc.GetPersistentPointer(PKEY_PENDING_TIME_AGGS);

    if (sc.LastCallToFunction)
    {
        if (Events != NULL)
        {
            delete Events;
            sc.SetPersistentPointer(PKEY_EVENTS_VECTOR, NULL);
        }

        if (PendingTimeAggregates != NULL)
        {
            delete PendingTimeAggregates;
            sc.SetPersistentPointer(PKEY_PENDING_TIME_AGGS, NULL);
        }

        return;
    }

    if (Events == NULL)
    {
        Events = new std::vector<TradeMapLargeTradeEvent>;
        sc.SetPersistentPointer(PKEY_EVENTS_VECTOR, Events);
    }

    if (PendingTimeAggregates == NULL)
    {
        PendingTimeAggregates =
            new std::vector<TradeMapPendingAggregate>;

        sc.SetPersistentPointer(
            PKEY_PENDING_TIME_AGGS,
            PendingTimeAggregates);
    }

    int64_t& LastSequence =
        sc.GetPersistentInt64(PKEY_LAST_SEQUENCE);

    double& AggPrice =
        sc.GetPersistentDouble(PKEY_AGG_PRICE);

    int& AggBar =
        sc.GetPersistentInt(PKEY_AGG_BAR);

    int& AggVolume =
        sc.GetPersistentInt(PKEY_AGG_VOLUME);

    int& AggSide =
        sc.GetPersistentInt(PKEY_AGG_SIDE);

    int& MarkerIndex =
        sc.GetPersistentInt(PKEY_MARKER_INDEX);

    int& HistoricalBuilt =
        sc.GetPersistentInt(PKEY_HIST_BUILT);

    int& NeedsRedraw =
        sc.GetPersistentInt(PKEY_NEEDS_REDRAW);

    int& RebuildSignature =
        sc.GetPersistentInt(PKEY_REBUILD_SIGNATURE);

    SCDateTimeMS& AggDateTime =
        sc.GetPersistentSCDateTime(PKEY_AGG_DATETIME);

    int& AggMS =
        sc.GetPersistentInt(PKEY_AGG_MS);

    int& AggAskVolume =
        sc.GetPersistentInt(PKEY_AGG_ASKVOL);

    int& AggBidVolume =
        sc.GetPersistentInt(PKEY_AGG_BIDVOL);

    double& AggPriceVolume =
        sc.GetPersistentDouble(PKEY_AGG_PV);

    if (sc.IsFullRecalculation)
    {
        LastSequence = 0;
        AggPrice = 0.0f;
        AggBar = -1;
        AggVolume = 0;
        AggSide = 0;
        AggMS = 0;
        AggDateTime = 0;
        AggAskVolume = 0;
        AggBidVolume = 0;
        AggPriceVolume = 0.0f;

        if (PendingTimeAggregates != NULL)
            PendingTimeAggregates->clear();

        HistoricalBuilt = 0;
        NeedsRedraw = 1;
    }

    int CurrentSignature = 0;

    CurrentSignature += MinimumVolumeThreshold.GetInt() * 5;
    CurrentSignature += VolumeMinFilter.GetInt() * 17;
    CurrentSignature += VolumeMaxFilter.GetInt() * 7;
    CurrentSignature += CombineMode.GetIndex() * 11;
    CurrentSignature += MillisecondsWindow.GetInt() * 13;

    if (CurrentSignature != RebuildSignature)
    {
        HistoricalBuilt = 0;

        if (Events != NULL)
            Events->clear();

        if (PendingTimeAggregates != NULL)
            PendingTimeAggregates->clear();

        RebuildSignature = CurrentSignature;
    }

    static int LastVisualSignature = 0;

    int CurrentVisualSignature = 0;

    CurrentVisualSignature += MinMarkerSize.GetInt() * 3;
    CurrentVisualSignature += MaxMarkerSize.GetInt() * 5;
    CurrentVisualSignature += Transparency.GetInt() * 7;
    CurrentVisualSignature += HollowMarker.GetYesNo() * 11;
    CurrentVisualSignature += VolumeLabelFontSize.GetInt() * 13;
    CurrentVisualSignature += VolumeLabelMinVolume.GetInt() * 17;
    CurrentVisualSignature += RenderLookbackBars.GetInt() * 29;
    CurrentVisualSignature += (int)BuyColor.GetColor() * 31;
    CurrentVisualSignature += (int)SellColor.GetColor() * 37;
    CurrentVisualSignature += (int)BuyMaxColor.GetColor() * 41;
    CurrentVisualSignature += (int)SellMaxColor.GetColor() * 43;
    CurrentVisualSignature += (int)VolumeLabelColor.GetColor() * 47;

    if (CurrentVisualSignature != LastVisualSignature)
    {
        NeedsRedraw = 1;
        LastVisualSignature = CurrentVisualSignature;
    }

    if (NeedsRedraw && HistoricalBuilt)
        NeedsRedraw = 0;

    if (RebuildCurrentDate.GetYesNo() && HistoricalBuilt == 0)
    {

            if (Events != nullptr)
            Events->clear();

        TradeMapRebuildCurrentDateFromIntradayFile(
            sc,
            Events,
            MarkerIndex,
            MinimumVolumeThreshold.GetInt(),
            CombineMode.GetIndex(),
            VolumeMinFilter.GetInt(),
            VolumeMaxFilter.GetInt());

        HistoricalBuilt = 1;
    }


//historical records handling

    c_SCTimeAndSalesArray TimeSales;

    sc.GetTimeAndSales(TimeSales);

    const bool HistoricalMaintenanceRequired =
        (!HistoricalBuilt)
        || NeedsRedraw;

    const int NumRecords = TimeSales.Size();

    if (NumRecords == 0)
        return;

    uint32_t PriorSequence =
        (uint32_t)LastSequence;

    TimeSales.ValidateAndCorrectPriorSequenceNumber(
        PriorSequence);

    const uint32_t StartIndex =
        TimeSales.GetRecordIndexAtGreaterThanSequenceNumber(
            PriorSequence);

    for (uint32_t i = StartIndex;
         i < (uint32_t)NumRecords;
         ++i)
    {
        const s_TimeAndSales& Record =
            TimeSales[i];

        LastSequence = Record.Sequence;

        const int Side = Record.Type;

        if (Side != SC_TS_ASK && Side != SC_TS_BID)
            continue;

        const int Volume =
            (int)Record.Volume;

        if (Volume < VolumeMinFilter.GetInt())
            continue;

        if (VolumeMaxFilter.GetInt() > 0
            && Volume > VolumeMaxFilter.GetInt())
        {
            continue;
        }

        const float Price =
            (float)Record.GetPrice()
            * sc.RealTimePriceMultiplier;

        SCDateTimeMS DT =
            Record.DateTime + sc.TimeScaleAdjustment;

        int BarIndex =
            sc.GetContainingIndexForSCDateTime(
                sc.ChartNumber,
                DT);

        if (BarIndex < 0)
            BarIndex = sc.ArraySize - 1;

        if (BarIndex >= sc.ArraySize)
            continue;

        if (TradeMapIsTimestampKeyMode(CombineMode.GetIndex()))
        {
            TradeMapAddTimeKeyAggregate(
                sc,
                Events,
                PendingTimeAggregates,
                MarkerIndex,
                MinimumVolumeThreshold.GetInt(),
                CombineMode.GetIndex(),
                BarIndex,
                DT,
                Price,
                Volume,
                Side);

            continue;
        }

        if (CombineMode.GetIndex() == TRADEMAP_MODE_BAR_PRICE
            || CombineMode.GetIndex() == TRADEMAP_MODE_BAR_PRICE_TYPE)
        {
            TradeMapAddOrUpdateBarPriceAggregate(
                sc,
                Events,
                PendingTimeAggregates,
                MarkerIndex,
                MinimumVolumeThreshold.GetInt(),
                CombineMode.GetIndex() == TRADEMAP_MODE_BAR_PRICE_TYPE,
                BarIndex,
                DT,
                Price,
                Volume,
                Side);

            continue;
        }

        bool Merge = false;

        if (CombineMode.GetIndex() == TRADEMAP_MODE_MILLISECONDS)
        {
            Merge =
                AggVolume > 0
                && TradeMapShouldMergeFixedMSBucket(
                    DT,
                    AggMS,
                    MillisecondsWindow.GetInt());
        }
        else
        {
            Merge =
                AggVolume > 0
                && AggBar == BarIndex
                && TradeMapShouldMerge(
                    CombineMode.GetIndex(),
                    DT,
                    AggDateTime,
                    Price,
                    static_cast<float>(AggPrice),
                    Side,
                    AggSide);
        }

        if (Merge)
        {
            AggVolume += Volume;

            if (CombineMode.GetIndex() == TRADEMAP_MODE_MILLISECONDS)
            {
                if (Side == SC_TS_ASK)
                    AggAskVolume += Volume;
                else
                    AggBidVolume += Volume;

                AggPriceVolume +=
                    (double)Price * (double)Volume;

                AggPrice =
                    (AggPriceVolume / static_cast<double>(AggVolume));

                AggSide =
                    AggAskVolume >= AggBidVolume
                    ? SC_TS_ASK
                    : SC_TS_BID;
            }
        }
        else
        {
            TradeMapStoreEvent(
                sc,
                Events,
                MarkerIndex,
                AggBar,
                AggDateTime,
                static_cast<float>(AggPrice),
                AggVolume,
                AggSide,
                MinimumVolumeThreshold.GetInt());

            AggPrice = Price;
            AggBar = BarIndex;
            AggVolume = Volume;
            AggSide = Side;
            AggDateTime = DT;
            AggMS =
                MillisecondsWindow.GetInt() > 0
                ? (DT.GetTimeInMilliseconds() / MillisecondsWindow.GetInt())
                : 0;

            AggAskVolume =
                Side == SC_TS_ASK
                ? Volume
                : 0;

            AggBidVolume =
                Side == SC_TS_BID
                ? Volume
                : 0;

            AggPriceVolume =
                (double)Price * (double)Volume;
        }
    }

}
