// ============================================================================
//  LargeVolumeAggregateTrades - Sierra Chart ACSIL
//  -based on Deep Trades or Volumetrica aggregate volume-
// 
//  Uses sc.GetTimeAndSales() to access Time & Sales data.
//  Aggregates prints at the same price/side within a time window.
//  Author: Cid da Silva
//  Date: April- May 2026
//  -Fixed some bugs, namely realted to the use of AggBar
//  -Added the ability to merge concentric/overlapped markers to reduce marker pollution, if trades occur within a certain range/proximity, in ticks, merger occurs. 
//   This only occurs if trades occur in the same bar and side.
//  -Changed marker persistence to use a std::list<LargeTradeData> instead of a vector. 
//   This is much more efficient when erasing items since it is replacing nodes in a link list.
// ============================================================================
#include "sierrachart.h"

SCDLLName("LargeVolumeAggregateTradesDLL")

#define USE_DRAWINGS_INSTEAD_OF_SUBGRAPH

const int MAX_MARKERS_PER_BAR = 60; //same as SC_SUBGRAPHS_AVAILABLE

// Persistent variable keys
const int KEY_SEQ = 0;

const int KEY_AGG_PRICE = 0;

enum AggPersistIntVarKeyes
{
	KEY_AGG_BAR,
	KEY_AGG_VOL,
	KEY_AGG_MS,
	KEY_AGG_SIDE,
	MARKER_IDX,
	RECYCLE_IDX
};

const int BASE_DRAWING_ID = 4373400;

struct LargeTradeData
{
	int barIndex;
	int markerIdx;
	float price;
	uint32_t vol;
	int side;
};

inline float GetMarkerSize(int aggVol, int thresh, int minSize, int maxSize)
{
	if (thresh == 0.0f)
		return 0.0f;

	float ratio = static_cast<float>(aggVol) / static_cast<float>(thresh);

	float   sz = static_cast<float>(pow(ratio, 2)) * static_cast<float>(minSize);
	if (sz > static_cast<float>(maxSize))
		sz = static_cast<float>(maxSize);

	return sz;
}

inline int GetDrawingID(int barIndex, int markerIndex)
{
	return BASE_DRAWING_ID + (barIndex * MAX_MARKERS_PER_BAR * 2) + (markerIndex * 2);
}

int RecycleMarker(SCStudyInterfaceRef sc, int markerIndex, int barIndex, int volume, float price, int side, std::list<LargeTradeData>* p_LargeTradeDataArray)
{
	bool recFound = false;
	int deletedDrawings = 0;

	for (std::list<LargeTradeData>::iterator it = p_LargeTradeDataArray->end(); it != p_LargeTradeDataArray->begin(); --it)
	{
		if (it->barIndex == barIndex && it->markerIdx == markerIndex)
		{
			it->price = price;
			it->vol = volume;
			it->side = side;
			recFound = true;
			break;
		}
	}

	if (recFound)
	{
		//Remove Marker & marker text
		int lineNumber = GetDrawingID(barIndex, markerIndex);
		deletedDrawings = sc.DeleteACSChartDrawing(sc.ChartNumber, TOOL_DELETE_CHARTDRAWING, lineNumber);
		deletedDrawings += sc.DeleteACSChartDrawing(sc.ChartNumber, TOOL_DELETE_CHARTDRAWING, lineNumber + 1);
	}

	return deletedDrawings;

}

int MergeExistingMarker(SCStudyInterfaceRef sc, int markerIndex, int barIndex, float price, int side, int mergeThreshold, std::list<LargeTradeData>* p_LargeTradeDataArray)
{

	int deleteMarkerIndex = -1;
	int vol = 0;
	bool priceInRange = false;

	for (std::list<LargeTradeData>::iterator it = p_LargeTradeDataArray->end(); it != p_LargeTradeDataArray->begin(); --it)
	{
		if (it->barIndex == barIndex && it->side == side)
		{
			priceInRange = it->price == price || (it->price < price && it->price >= (price - (sc.TickSize * mergeThreshold))) || (it->price > price && it->price <= (price + (sc.TickSize * mergeThreshold)));

			if (priceInRange)
			{
				deleteMarkerIndex = it->markerIdx;
				vol = it->vol;
				p_LargeTradeDataArray->erase(it);
				break;
			}
			else
				continue;
		}
	}

#ifdef USE_DRAWINGS_INSTEAD_OF_SUBGRAPH

	if (vol > 0)
	{
		//Remove Marker & marker text
		int lineNumber = GetDrawingID(barIndex, deleteMarkerIndex);
		sc.DeleteACSChartDrawing(sc.ChartNumber, TOOL_DELETE_CHARTDRAWING, lineNumber);
		sc.DeleteACSChartDrawing(sc.ChartNumber, TOOL_DELETE_CHARTDRAWING, lineNumber + 1);
	}
#endif

	return vol;

}

void DrawVolumeMarker(SCStudyInterfaceRef sc, int markerIndex, int barIndex, float price, int volume, unsigned int color, bool hollowMarker, bool showVolLabel, float sz, int transparency, int labelTxtSize, unsigned int labelClr)
{

#ifdef USE_DRAWINGS_INSTEAD_OF_SUBGRAPH

	int drawingID = GetDrawingID(barIndex, markerIndex);

	s_UseTool Tool;
	Tool.Clear();

	Tool.ChartNumber = sc.ChartNumber;
	Tool.Region = sc.GraphRegion;
	Tool.AddMethod = UTAM_ADD_OR_ADJUST;
	Tool.LineNumber = drawingID;

	Tool.BeginDateTime = sc.BaseDateTimeIn[barIndex];
	Tool.BeginValue = price;

	Tool.Color = color;
	Tool.TransparencyLevel = transparency;
	Tool.DrawUnderneathMainGraph = 1;

	Tool.DrawingType = DRAWING_MARKER;
	Tool.MarkerType = MARKER_POINT;
	Tool.MarkerSize = static_cast<int>(sz);
	Tool.LineWidth = 1;

	if (hollowMarker)
		Tool.DrawOutlineOnly = 1;

	sc.UseTool(Tool);

	if (showVolLabel)
	{
		s_UseTool Tool;
		Tool.Clear();

		Tool.ChartNumber = sc.ChartNumber;
		Tool.Region = sc.GraphRegion;
		Tool.AddMethod = UTAM_ADD_OR_ADJUST;
		Tool.LineNumber = drawingID + 1;
		Tool.DrawingType = DRAWING_TEXT;

		Tool.BeginDateTime = sc.BaseDateTimeIn[barIndex];
		Tool.BeginValue = price;

		Tool.Color = labelClr;
		Tool.FontSize = 10;
		Tool.FontBold = 1;
		SCString label;
		label.Format("%u", (uint32_t)volume);
		Tool.Text = label;
		Tool.TextAlignment = DT_CENTER | DT_VCENTER;
		Tool.TransparentLabelBackground = 1;
		Tool.DrawUnderneathMainGraph = 0;

		sc.UseTool(Tool);
	}
	else //draw center of marker
	{
		s_UseTool Tool;
		Tool.Clear();

		Tool.ChartNumber = sc.ChartNumber;
		Tool.Region = sc.GraphRegion;
		Tool.AddMethod = UTAM_ADD_OR_ADJUST;
		Tool.LineNumber = drawingID + 1;
		Tool.DrawingType = DRAWING_MARKER;
		Tool.MarkerType = MARKER_POINT;
		Tool.MarkerSize = 2;
		Tool.LineWidth = 2;

		Tool.BeginDateTime = sc.BaseDateTimeIn[barIndex];
		Tool.BeginValue = price;

		Tool.Color = color;
		Tool.TransparentLabelBackground = 1;
		Tool.DrawUnderneathMainGraph = 0;

		sc.UseTool(Tool);
	}

#else

	sc.Subgraph[markerIndex].Arrays[0][barIndex] = sz;
	sc.Subgraph[markerIndex].Data[barIndex] = price;
	sc.Subgraph[markerIndex].DataColor[barIndex] = color;

#endif

}

SCSFExport scsf_LargeVolumeAggregateTrades(SCStudyInterfaceRef sc)
{
	// -- Subgraphs -------------------------------------------------------------

	// -- Inputs ----------------------------------------------------------------
	SCInputRef Threshold = sc.Input[0];
	SCInputRef AggWindowMS = sc.Input[1];
	SCInputRef AggType = sc.Input[2];
	SCInputRef Transparent = sc.Input[3];
	SCInputRef ShowBG = sc.Input[4];
	SCInputRef BuyCol = sc.Input[5];
	SCInputRef SellCol = sc.Input[6];
	SCInputRef MaxSize = sc.Input[7];
	SCInputRef MinSize = sc.Input[8];
	SCInputRef CrossBarAgg = sc.Input[9];
	SCInputRef ShowVolumeLabel = sc.Input[10];
	SCInputRef VolumeLabelColor = sc.Input[11];
	SCInputRef VolumeLabelFontSize = sc.Input[12];
	SCInputRef MergeThreshold = sc.Input[13];

	// -- Defaults --------------------------------------------------------------
	if (sc.SetDefaults)
	{
		sc.GraphName = "Large Volume Aggregate Trades";
		sc.StudyDescription =
			"Large Volume Aggregate Trades, DeepTrades equivalent."
			"Green bubble = buy aggression. Red bubble = sell aggression.";

		sc.AutoLoop = 0;
		sc.GraphRegion = 0;
		sc.MaintainAdditionalChartDataArrays = 1;

		Threshold.Name = "Threshold";
		Threshold.SetInt(30);

		AggWindowMS.Name = "Aggregation Window (ms)";
		AggWindowMS.SetInt(500);

		AggType.Name = "Aggregation Mode";
		AggType.SetCustomInputStrings("Time, Type & Price;Time & Type;Large Trades");
		AggType.SetCustomInputIndex(1);

		Transparent.Name = "Transparency";
		Transparent.SetInt(85);

		ShowBG.Name = "Hollow Marker?";
		ShowBG.SetYesNo(1);

		BuyCol.Name = "Buy Color";
		BuyCol.SetColor(RGB(0, 215, 140));

		SellCol.Name = "Sell Color";
		SellCol.SetColor(RGB(255, 75, 75));

		MaxSize.Name = "Max Circle Size (10-100)";
		MaxSize.SetInt(50);
		MaxSize.SetIntLimits(10, 100);

		MinSize.Name = "Min Circle Size (5-20)";
		MinSize.SetInt(10);
		MinSize.SetIntLimits(5, 20);

		CrossBarAgg.Name = "Cross Bar Aggregation?";
		CrossBarAgg.SetYesNo(0);

		ShowVolumeLabel.Name = "Show Volume Label?";
		ShowVolumeLabel.SetYesNo(0);

		VolumeLabelColor.Name = "Label Color";
		VolumeLabelColor.SetColor(RGB(255, 255, 255));

		VolumeLabelFontSize.Name = "Label Text Size (points)";
		VolumeLabelFontSize.SetInt(10);
		VolumeLabelFontSize.SetIntLimits(0, 10);

		MergeThreshold.Name = "Marker Merge Threshold in Ticks (-1=Disabled)";
		MergeThreshold.SetInt(4);
		MergeThreshold.SetIntLimits(-1, 100);

#ifndef USE_DRAWINGS_INSTEAD_OF_SUBGRAPH

		for (int SubgraphIndex = 0; SubgraphIndex < SC_SUBGRAPHS_AVAILABLE; ++SubgraphIndex)
		{
			SCString SubgraphName;
			SubgraphName.Format("M%d", SubgraphIndex);
			sc.Subgraph[SubgraphIndex].Name = SubgraphName;
			sc.Subgraph[SubgraphIndex].DrawStyle = ShowBG.GetYesNo() ? DRAWSTYLE_CIRCLE_HOLLOW_VARIABLE_SIZE : DRAWSTYLE_TRANSPARENT_CIRCLE_VARIABLE_SIZE;
			sc.Subgraph[SubgraphIndex].PrimaryColor = COLOR_WHITE;
			sc.Subgraph[SubgraphIndex].DrawZeros = FALSE;
		}
#endif
		return;
	}


	std::list<LargeTradeData>* p_LargeTradeDataArray = (std::list<LargeTradeData>*)sc.GetPersistentPointer(1);

	if (sc.LastCallToFunction)
	{
		if (p_LargeTradeDataArray != NULL)
		{
			p_LargeTradeDataArray->clear();
			delete p_LargeTradeDataArray;
			sc.SetPersistentPointer(1, NULL);
		}

		return;
	}


	if (p_LargeTradeDataArray == NULL)
	{
		p_LargeTradeDataArray = new std::list<LargeTradeData>;

		if (p_LargeTradeDataArray != NULL)
		{
			sc.SetPersistentPointer(1, p_LargeTradeDataArray);

		}
		else
		{
			sc.AddMessageToLog("Large Trades Data std::list memory allocation error.", 1);
			return;
		}
	}


	// -- Load frequently used settings --------------------------------------------------------
	int      thresh = Threshold.GetInt();
	int      aggMS = AggWindowMS.GetInt();
	int     aggType = AggType.GetInputDataIndex();

	// -- Persistent state --------------------------------------------------------------
	int64_t& LastSeq = sc.GetPersistentInt64(KEY_SEQ);

	float& AggPrice = sc.GetPersistentFloat(KEY_AGG_PRICE);

	int& AggBar = sc.GetPersistentInt(KEY_AGG_BAR);
	int& AggVol = sc.GetPersistentInt(KEY_AGG_VOL);
	int& AggMS = sc.GetPersistentInt(KEY_AGG_MS);
	int& AggSide = sc.GetPersistentInt(KEY_AGG_SIDE);

	int& MarkerIdx = sc.GetPersistentInt(MARKER_IDX);
	int& RecycleIdx = sc.GetPersistentInt(RECYCLE_IDX);

	if (sc.IsFullRecalculation)
	{
		if (MinSize.GetInt() > MaxSize.GetInt())
			MinSize.SetInt(5);
		sc.SetChartStudyTransparencyLevel(sc.ChartNumber, sc.StudyGraphInstanceID, Transparent.GetInt());

#ifndef USE_DRAWINGS_INSTEAD_OF_SUBGRAPH

		for (int SubgraphIndex = 0; SubgraphIndex < SC_SUBGRAPHS_AVAILABLE; ++SubgraphIndex)
		{
			sc.Subgraph[SubgraphIndex].DrawStyle = ShowBG.GetYesNo() ? DRAWSTYLE_CIRCLE_HOLLOW_VARIABLE_SIZE : DRAWSTYLE_TRANSPARENT_CIRCLE_VARIABLE_SIZE;
		}
#endif
		//
		//-------------repopulate large volume---------
		//
		for (std::list<LargeTradeData>::iterator it = p_LargeTradeDataArray->begin(); it != p_LargeTradeDataArray->end(); ++it)
		{
			float markerSz = GetMarkerSize((int)it->vol, thresh, MinSize.GetInt(), MaxSize.GetInt());
			unsigned int markerClr = it->side == SC_TS_ASK ? BuyCol.GetColor() : SellCol.GetColor();
			DrawVolumeMarker(sc, it->markerIdx, it->barIndex, it->price, (int)it->vol, markerClr, ShowBG.GetYesNo(), ShowVolumeLabel.GetYesNo(), markerSz, Transparent.GetInt(), VolumeLabelFontSize.GetInt(), VolumeLabelColor.GetColor());
		}


		LastSeq = 0;
	}

	// -- Get Time & Sales via sc.GetTimeAndSales() --------------------------------------
	c_SCTimeAndSalesArray TAS;
	sc.GetTimeAndSales(TAS);

	int numRec = TAS.Size();
	if (numRec == 0)
		return;

	uint32_t u_lastSeq = static_cast<uint32_t>(LastSeq);

	//If sequence wraps around this call resets it
	TAS.ValidateAndCorrectPriorSequenceNumber(u_lastSeq);

	uint32_t seqIndex = TAS.GetRecordIndexAtGreaterThanSequenceNumber(u_lastSeq);


	for (uint32_t i = seqIndex; i < static_cast<uint32_t>(numRec); ++i)
	{
		const s_TimeAndSales& rec = TAS[i];

		LastSeq = TAS[i].Sequence;

		float      price = static_cast<float>(rec.GetPrice()) * sc.RealTimePriceMultiplier;
		int        vol = (int)rec.Volume;
		int        side = rec.Type;
		SCDateTimeMS ts = rec.DateTime + sc.TimeScaleAdjustment;

		if (vol <= 0 || price <= 0.0f)
			continue;

		// SC_TS_ASK (1) = buyer aggressor (hit ask)
		// SC_TS_BID (2) = seller aggressor (hit bid)
		if (side != SC_TS_ASK && side != SC_TS_BID)
			continue;

		// Bar this tick belongs to
		int bar = sc.GetContainingIndexForSCDateTime(sc.ChartNumber, ts);
		if (bar < 0)
			bar = sc.ArraySize - 1;
		if (bar >= sc.ArraySize)
			continue;

		// ms-of-day for aggregation window
		int ms = ts.GetTimeInMilliseconds();

		// -- Aggregation: merge if same price, side, bar, within window --------
		bool merge = false;

		if (aggType == 0)
		{
			bool crossBarAgg = CrossBarAgg.GetYesNo() ? true : AggBar == bar;

			merge = (
				side == AggSide &&
				AggVol > 0 &&
				AggPrice == price &&
				crossBarAgg &&
				abs(ms - AggMS) <= aggMS
				);

		}
		else if (aggType == 1)
		{
			bool crossBarAgg = CrossBarAgg.GetYesNo() ? true : AggBar == bar;

			merge = (
				side == AggSide &&
				AggVol > 0 &&
				crossBarAgg &&
				abs(ms - AggMS) <= aggMS
				);
		}
		else
		{
			AggVol = vol;
		}


		if (merge)
		{
			AggVol += vol;
		}
		else
		{
			// -- Flush: draw bubble if aggregated volume >= threshold -----------
			if (AggVol >= thresh && AggBar >= 0 && AggBar < sc.ArraySize)
			{
				++MarkerIdx;

				//recycle subgraphs if reached max, unlikely but possible in large time frames
				if (MarkerIdx == MAX_MARKERS_PER_BAR)
				{
					RecycleIdx = AggBar;
					MarkerIdx = 0;
				}

#ifdef USE_DRAWINGS_INSTEAD_OF_SUBGRAPH
				//no drawings to delete in subgraph mode
				if (RecycleIdx > 0)
					RecycleMarker(sc, MarkerIdx, AggBar, AggVol, AggPrice, AggSide, p_LargeTradeDataArray);

#endif

				if (MergeThreshold.GetInt() > -1)
				{
					int matchVolume = MergeExistingMarker(sc, MarkerIdx, AggBar, AggPrice, AggSide, MergeThreshold.GetInt(), p_LargeTradeDataArray);

					AggVol += matchVolume;
				}

				// Circle size scales with volume
				float sz = GetMarkerSize(AggVol, thresh, MinSize.GetInt(), MaxSize.GetInt());

				unsigned int markerClr = AggSide == SC_TS_ASK ? BuyCol.GetColor() : SellCol.GetColor();

				DrawVolumeMarker(sc, MarkerIdx, AggBar, AggPrice, AggVol, markerClr, ShowBG.GetYesNo(), ShowVolumeLabel.GetYesNo(), sz, Transparent.GetInt(), VolumeLabelFontSize.GetInt(), VolumeLabelColor.GetColor());

				//persistence
				LargeTradeData s_LargeTradeData;
				s_LargeTradeData.barIndex = AggBar;
				s_LargeTradeData.markerIdx = MarkerIdx;
				s_LargeTradeData.price = AggPrice;
				s_LargeTradeData.vol = AggVol;
				s_LargeTradeData.side = AggSide;

				p_LargeTradeDataArray->push_back(s_LargeTradeData);

			}

			// Start fresh aggregation from this tick		
			AggPrice = price;
			AggVol = vol;
			AggMS = ms;
			AggBar = bar;
			AggSide = side;
		}
	}

	if (sc.GetBarHasClosedStatus(sc.UpdateStartIndex) == BHCS_BAR_HAS_CLOSED)
	{
		MarkerIdx = -1;
		RecycleIdx = -1;
	}
}




