// ============================================================================ // AtlasLargeTradesProV2_4_SubgraphBubbles.cpp - Sierra Chart ACSIL Custom Study // // Purpose: // Large trade bubbles for Sierra Chart with same-day historical rebuilding. // - Rebuilds the current chart date from Sierra's intraday tick records. // - Continues processing live Time & Sales after the rebuild. // - Draws buy/sell aggression bubbles historically so you can review trades. // V2.4 uses Sierra subgraph variable-size circles so Transparency and Hollow Bubble settings work correctly. // // Recommended ES RTH settings: // Minimum Volume Threshold: 75 // Aggregation Window: 500 ms // Aggregation Mode: Time & Side // Rebuild Current Date: Yes // // Notes: // SC_TS_ASK = aggressive buyer lifted the offer. // SC_TS_BID = aggressive seller hit the bid. // ============================================================================ #include "sierrachart.h" #include #include SCDLLName("Atlas Large Trades Pro V2.4 Subgraph Bubbles") const int ATLAS_MAX_MARKERS_PER_BAR = 60; // Sierra exposes 60 subgraphs; one bubble slot per subgraph per bar. const int ATLAS_BASE_DRAWING_ID = 87445000; // Used for volume text labels only in V2.4. 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_MS = 5; const int PKEY_AGG_SIDE = 6; const int PKEY_MARKER_INDEX = 7; const int PKEY_EVENTS_VECTOR = 1; const int PKEY_HIST_BUILT = 8; struct AtlasLargeTradeEventV2 { int BarIndex; int MarkerIndex; SCDateTimeMS DateTime; float Price; int Volume; int Side; }; inline int AtlasClampIntV2(int Value, int MinValue, int MaxValue) { if (Value < MinValue) return MinValue; if (Value > MaxValue) return MaxValue; return Value; } inline float AtlasGetMarkerSizeV2(int Volume, int Threshold, int MinSize, int MaxSize) { if (Threshold <= 0) return static_cast(MinSize); const float Ratio = static_cast(Volume) / static_cast(Threshold); float Size = static_cast(MinSize) * std::pow(Ratio, 1.55f); if (Size < static_cast(MinSize)) Size = static_cast(MinSize); if (Size > static_cast(MaxSize)) Size = static_cast(MaxSize); return Size; } inline int AtlasGetDrawingIDV2(int BarIndex, int MarkerIndex) { return ATLAS_BASE_DRAWING_ID + (BarIndex * ATLAS_MAX_MARKERS_PER_BAR * 2) + (MarkerIndex * 2); } void AtlasApplyBubbleSubgraphSettingsV2( SCStudyInterfaceRef sc, bool HollowMarker, int Transparency) { sc.SetChartStudyTransparencyLevel( sc.ChartNumber, sc.StudyGraphInstanceID, AtlasClampIntV2(Transparency, 0, 100)); for (int SubgraphIndex = 0; SubgraphIndex < ATLAS_MAX_MARKERS_PER_BAR; ++SubgraphIndex) { SCString SubgraphName; SubgraphName.Format("BubbleSlot%d", SubgraphIndex); sc.Subgraph[SubgraphIndex].Name = SubgraphName; sc.Subgraph[SubgraphIndex].DrawStyle = HollowMarker ? DRAWSTYLE_CIRCLE_HOLLOW_VARIABLE_SIZE : DRAWSTYLE_TRANSPARENT_CIRCLE_VARIABLE_SIZE; sc.Subgraph[SubgraphIndex].PrimaryColor = COLOR_WHITE; sc.Subgraph[SubgraphIndex].DrawZeros = false; } } void AtlasClearBarBubbleSlotsV2(SCStudyInterfaceRef sc, int BarIndex) { if (BarIndex < 0 || BarIndex >= sc.ArraySize) return; for (int i = 0; i < ATLAS_MAX_MARKERS_PER_BAR; ++i) { sc.Subgraph[i].Data[BarIndex] = 0.0f; sc.Subgraph[i].Arrays[0][BarIndex] = 0.0f; sc.Subgraph[i].DataColor[BarIndex] = 0; const int LabelDrawingID = AtlasGetDrawingIDV2(BarIndex, i) + 1; sc.DeleteACSChartDrawing(sc.ChartNumber, TOOL_DELETE_CHARTDRAWING, LabelDrawingID); } } void AtlasDrawLargeTradeMarkerV2( SCStudyInterfaceRef sc, int MarkerIndex, int BarIndex, SCDateTimeMS EventDateTime, float Price, int Volume, COLORREF MarkerColor, bool HollowMarker, bool ShowVolumeLabel, float MarkerSize, int Transparency, int LabelFontSize, COLORREF LabelColor) { if (BarIndex < 0 || BarIndex >= sc.ArraySize) return; if (MarkerIndex < 0) return; if (MarkerIndex >= ATLAS_MAX_MARKERS_PER_BAR) MarkerIndex = MarkerIndex % ATLAS_MAX_MARKERS_PER_BAR; // V2.4: Use Sierra Subgraph variable-size circles for the actual bubble. // Reason: DRAWING_MARKER / MARKER_POINT is visible, but Sierra does not reliably // apply the hollow-circle and transparency behavior to that marker type. Subgraph // draw styles are the reliable native Sierra way to get transparent/hollow bubbles. sc.Subgraph[MarkerIndex].Data[BarIndex] = Price; sc.Subgraph[MarkerIndex].Arrays[0][BarIndex] = MarkerSize; sc.Subgraph[MarkerIndex].DataColor[BarIndex] = MarkerColor; // Keep the draw style/transparency synced with the input settings. sc.Subgraph[MarkerIndex].DrawStyle = HollowMarker ? DRAWSTYLE_CIRCLE_HOLLOW_VARIABLE_SIZE : DRAWSTYLE_TRANSPARENT_CIRCLE_VARIABLE_SIZE; sc.SetChartStudyTransparencyLevel( sc.ChartNumber, sc.StudyGraphInstanceID, AtlasClampIntV2(Transparency, 0, 100)); const int DrawingID = AtlasGetDrawingIDV2(BarIndex, MarkerIndex); if (ShowVolumeLabel) { s_UseTool TextTool; TextTool.Clear(); TextTool.ChartNumber = sc.ChartNumber; TextTool.Region = sc.GraphRegion; TextTool.AddMethod = UTAM_ADD_OR_ADJUST; TextTool.LineNumber = DrawingID + 1; TextTool.DrawingType = DRAWING_TEXT; TextTool.BeginDateTime = sc.BaseDateTimeIn[BarIndex]; TextTool.BeginIndex = BarIndex; TextTool.BeginValue = Price; TextTool.Color = LabelColor; TextTool.FontSize = LabelFontSize; TextTool.FontBold = 1; TextTool.TextAlignment = DT_CENTER | DT_VCENTER; TextTool.TransparentLabelBackground = 1; TextTool.DrawUnderneathMainGraph = 0; SCString Label; Label.Format("%d", Volume); TextTool.Text = Label; sc.UseTool(TextTool); } else { sc.DeleteACSChartDrawing(sc.ChartNumber, TOOL_DELETE_CHARTDRAWING, DrawingID + 1); } } inline int AtlasDetermineIntradaySideV2(const s_IntradayRecord& Record) { if (Record.AskVolume > Record.BidVolume) return SC_TS_ASK; if (Record.BidVolume > Record.AskVolume) return SC_TS_BID; return 0; } void AtlasStoreAndDrawEventV2( SCStudyInterfaceRef sc, std::vector* Events, int& MarkerIndex, int BarIndex, SCDateTimeMS EventDateTime, float Price, int Volume, int Side, int Threshold, int MinSize, int MaxSize, COLORREF BuyColor, COLORREF SellColor, bool HollowMarker, bool ShowVolumeLabel, int Transparency, int LabelFontSize, COLORREF LabelColor) { if (Volume < Threshold || BarIndex < 0 || BarIndex >= sc.ArraySize) return; ++MarkerIndex; if (MarkerIndex >= ATLAS_MAX_MARKERS_PER_BAR) MarkerIndex = 0; const COLORREF Color = Side == SC_TS_ASK ? BuyColor : SellColor; const float Size = AtlasGetMarkerSizeV2(Volume, Threshold, MinSize, MaxSize); AtlasDrawLargeTradeMarkerV2( sc, MarkerIndex, BarIndex, EventDateTime, Price, Volume, Color, HollowMarker, ShowVolumeLabel, Size, Transparency, LabelFontSize, LabelColor); if (Events != NULL) { AtlasLargeTradeEventV2 Event; Event.BarIndex = BarIndex; Event.MarkerIndex = MarkerIndex; Event.DateTime = EventDateTime; Event.Price = Price; Event.Volume = Volume; Event.Side = Side; Events->push_back(Event); } } int AtlasRebuildCurrentDateFromIntradayFileV2( SCStudyInterfaceRef sc, std::vector* Events, int& MarkerIndex, int Threshold, int AggWindowMS, int Mode, int MinSize, int MaxSize, COLORREF BuyColor, COLORREF SellColor, bool HollowMarker, bool ShowVolumeLabel, int Transparency, int LabelFontSize, COLORREF LabelColor) { if (sc.ArraySize <= 0) return 0; if (Events != NULL) Events->clear(); MarkerIndex = -1; const int CurrentDate = sc.BaseDateTimeIn[sc.ArraySize - 1].GetDate(); int StartBar = 0; for (int i = sc.ArraySize - 1; i >= 0; --i) { if (sc.BaseDateTimeIn[i].GetDate() != CurrentDate) { StartBar = i + 1; break; } } for (int BarIndex = StartBar; BarIndex < sc.ArraySize; ++BarIndex) AtlasClearBarBubbleSlotsV2(sc, BarIndex); // Aggregation state for historical rebuild. float AggPrice = 0.0f; int AggBar = -1; int AggVolume = 0; int AggMS = 0; SCDateTimeMS AggDateTime = 0; int AggSide = 0; 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 = AtlasDetermineIntradaySideV2(Record); if (Side != SC_TS_ASK && Side != SC_TS_BID) continue; const int Volume = static_cast(Record.TotalVolume); if (Volume <= 0) continue; const float Price = static_cast(Record.Close) * sc.RealTimePriceMultiplier; if (Price <= 0.0f) continue; SCDateTimeMS RecordDateTime = Record.DateTime + sc.TimeScaleAdjustment; const int CurrentMS = RecordDateTime.GetTimeInMilliseconds(); bool Merge = false; if (Mode == 0) // Time, Side & Price { Merge = AggVolume > 0 && Side == AggSide && Price == AggPrice && AggBar == BarIndex && std::abs(CurrentMS - AggMS) <= AggWindowMS; } else if (Mode == 1) // Time & Side { Merge = AggVolume > 0 && Side == AggSide && AggBar == BarIndex && std::abs(CurrentMS - AggMS) <= AggWindowMS; } else // Large Trades Only { Merge = false; } if (Merge) { AggVolume += Volume; AggMS = CurrentMS; if (Mode == 1) { AggPrice = Price; AggDateTime = RecordDateTime; AggBar = BarIndex; } } else { AtlasStoreAndDrawEventV2( sc, Events, MarkerIndex, AggBar, AggDateTime, AggPrice, AggVolume, AggSide, Threshold, MinSize, MaxSize, BuyColor, SellColor, HollowMarker, ShowVolumeLabel, Transparency, LabelFontSize, LabelColor); AggPrice = Price; AggVolume = Volume; AggMS = CurrentMS; AggDateTime = RecordDateTime; AggBar = BarIndex; AggSide = Side; if (Mode == 2) // Large Trades Only { AtlasStoreAndDrawEventV2( sc, Events, MarkerIndex, AggBar, AggDateTime, AggPrice, AggVolume, AggSide, Threshold, MinSize, MaxSize, BuyColor, SellColor, HollowMarker, ShowVolumeLabel, Transparency, LabelFontSize, LabelColor); AggVolume = 0; } } } sc.ReadIntradayFileRecordForBarIndexAndSubIndex(-1, -1, Record, IFLA_RELEASE_AFTER_READ); } AtlasStoreAndDrawEventV2( sc, Events, MarkerIndex, AggBar, AggDateTime, AggPrice, AggVolume, AggSide, Threshold, MinSize, MaxSize, BuyColor, SellColor, HollowMarker, ShowVolumeLabel, Transparency, LabelFontSize, LabelColor); return Events != NULL ? static_cast(Events->size()) : 0; } SCSFExport scsf_AtlasLargeTradesProV2Historical(SCStudyInterfaceRef sc) { SCInputRef MinimumVolumeThreshold = sc.Input[0]; SCInputRef AggregationWindowMS = sc.Input[1]; SCInputRef AggregationMode = sc.Input[2]; SCInputRef BuyColor = sc.Input[3]; SCInputRef SellColor = sc.Input[4]; SCInputRef HollowMarker = sc.Input[5]; SCInputRef Transparency = sc.Input[6]; SCInputRef MinMarkerSize = sc.Input[7]; SCInputRef MaxMarkerSize = sc.Input[8]; SCInputRef ShowVolumeLabel = sc.Input[9]; SCInputRef VolumeLabelColor = sc.Input[10]; SCInputRef VolumeLabelFontSize = sc.Input[11]; SCInputRef EnableAlerts = sc.Input[12]; SCInputRef RebuildCurrentDate = sc.Input[13]; if (sc.SetDefaults) { sc.GraphName = "Atlas Large Trades Pro V2.4 Subgraph Bubbles"; sc.StudyDescription = "Large trade bubbles with current-day historical rebuild. V2.4 uses subgraph circles so transparency and hollow bubble settings work."; sc.AutoLoop = 0; sc.GraphRegion = 0; sc.UpdateAlways = 1; sc.MaintainAdditionalChartDataArrays = 1; MinimumVolumeThreshold.Name = "Minimum Volume Threshold"; MinimumVolumeThreshold.SetInt(75); MinimumVolumeThreshold.SetIntLimits(1, 100000); AggregationWindowMS.Name = "Aggregation Window in Milliseconds"; AggregationWindowMS.SetInt(500); AggregationWindowMS.SetIntLimits(0, 10000); AggregationMode.Name = "Aggregation Mode"; AggregationMode.SetCustomInputStrings("Time, Side & Price;Time & Side;Large Trades Only"); AggregationMode.SetCustomInputIndex(1); BuyColor.Name = "Buy Aggression Bubble Color"; BuyColor.SetColor(RGB(0, 215, 140)); SellColor.Name = "Sell Aggression Bubble Color"; SellColor.SetColor(RGB(255, 75, 75)); HollowMarker.Name = "Use Hollow Bubbles"; HollowMarker.SetYesNo(0); Transparency.Name = "Bubble Transparency 0=Solid 100=Invisible"; Transparency.SetInt(35); Transparency.SetIntLimits(0, 100); MinMarkerSize.Name = "Minimum Bubble Size"; MinMarkerSize.SetInt(9); MinMarkerSize.SetIntLimits(1, 100); MaxMarkerSize.Name = "Maximum Bubble Size"; MaxMarkerSize.SetInt(48); MaxMarkerSize.SetIntLimits(1, 200); ShowVolumeLabel.Name = "Show Volume Label"; ShowVolumeLabel.SetYesNo(1); VolumeLabelColor.Name = "Volume Label Color"; VolumeLabelColor.SetColor(RGB(255, 255, 255)); VolumeLabelFontSize.Name = "Volume Label Font Size"; VolumeLabelFontSize.SetInt(9); VolumeLabelFontSize.SetIntLimits(6, 48); EnableAlerts.Name = "Enable Alert on Large Trade"; EnableAlerts.SetYesNo(0); RebuildCurrentDate.Name = "Rebuild Current Chart Date From Intraday File"; RebuildCurrentDate.SetYesNo(1); AtlasApplyBubbleSubgraphSettingsV2(sc, HollowMarker.GetYesNo() != 0, Transparency.GetInt()); return; } std::vector* Events = static_cast*>(sc.GetPersistentPointer(PKEY_EVENTS_VECTOR)); if (sc.LastCallToFunction) { if (Events != NULL) { Events->clear(); delete Events; sc.SetPersistentPointer(PKEY_EVENTS_VECTOR, NULL); } return; } if (Events == NULL) { Events = new std::vector; if (Events == NULL) { sc.AddMessageToLog("Atlas Large Trades Pro V2: Failed to allocate event vector.", 1); return; } sc.SetPersistentPointer(PKEY_EVENTS_VECTOR, Events); } const int Threshold = MinimumVolumeThreshold.GetInt(); const int AggMSInput = AggregationWindowMS.GetInt(); const int Mode = AggregationMode.GetInputDataIndex(); int MinSize = MinMarkerSize.GetInt(); int MaxSize = MaxMarkerSize.GetInt(); if (MinSize > MaxSize) { MinSize = 9; MaxSize = 48; } // Keep transparency/hollow settings synced even when the user changes inputs. AtlasApplyBubbleSubgraphSettingsV2(sc, HollowMarker.GetYesNo() != 0, Transparency.GetInt()); int64_t& LastSequence = sc.GetPersistentInt64(PKEY_LAST_SEQUENCE); float& AggPrice = sc.GetPersistentFloat(PKEY_AGG_PRICE); int& AggBar = sc.GetPersistentInt(PKEY_AGG_BAR); int& AggVolume = sc.GetPersistentInt(PKEY_AGG_VOLUME); int& AggMS = sc.GetPersistentInt(PKEY_AGG_MS); int& AggSide = sc.GetPersistentInt(PKEY_AGG_SIDE); SCDateTimeMS AggDateTime = 0; int& MarkerIndex = sc.GetPersistentInt(PKEY_MARKER_INDEX); int& HistoricalBuilt = sc.GetPersistentInt(PKEY_HIST_BUILT); if (sc.IsFullRecalculation) { LastSequence = 0; AggPrice = 0.0f; AggBar = -1; AggVolume = 0; AggMS = 0; AggSide = 0; MarkerIndex = -1; HistoricalBuilt = 0; } if (RebuildCurrentDate.GetYesNo() && HistoricalBuilt == 0) { const int RebuiltBubbleCount = AtlasRebuildCurrentDateFromIntradayFileV2( sc, Events, MarkerIndex, Threshold, AggMSInput, Mode, MinSize, MaxSize, BuyColor.GetColor(), SellColor.GetColor(), HollowMarker.GetYesNo() != 0, ShowVolumeLabel.GetYesNo() != 0, Transparency.GetInt(), VolumeLabelFontSize.GetInt(), VolumeLabelColor.GetColor()); SCString RebuildMessage; RebuildMessage.Format("Atlas Large Trades Pro V2.4: Historical rebuild completed. Bubbles rebuilt: %d", RebuiltBubbleCount); sc.AddMessageToLog(RebuildMessage, 0); if (RebuiltBubbleCount > 0 && Events != NULL && !Events->empty()) { const AtlasLargeTradeEventV2& FirstEvent = Events->front(); const AtlasLargeTradeEventV2& LastEvent = Events->back(); SCString DiagnosticMessage; DiagnosticMessage.Format("Atlas V2.4 diagnostic: first bubble price %.2f vol %d bar %d | last bubble price %.2f vol %d bar %d", FirstEvent.Price, FirstEvent.Volume, FirstEvent.BarIndex, LastEvent.Price, LastEvent.Volume, LastEvent.BarIndex); sc.AddMessageToLog(DiagnosticMessage, 0); } HistoricalBuilt = 1; } c_SCTimeAndSalesArray TimeSales; sc.GetTimeAndSales(TimeSales); const int NumRecords = TimeSales.Size(); if (NumRecords == 0) return; uint32_t PriorSequence = static_cast(LastSequence); TimeSales.ValidateAndCorrectPriorSequenceNumber(PriorSequence); const uint32_t StartIndex = TimeSales.GetRecordIndexAtGreaterThanSequenceNumber(PriorSequence); for (uint32_t Index = StartIndex; Index < static_cast(NumRecords); ++Index) { const s_TimeAndSales& Record = TimeSales[Index]; LastSequence = Record.Sequence; const int Side = Record.Type; if (Side != SC_TS_ASK && Side != SC_TS_BID) continue; const int Volume = static_cast(Record.Volume); if (Volume <= 0) continue; const float Price = static_cast(Record.GetPrice()) * sc.RealTimePriceMultiplier; if (Price <= 0.0f) continue; const SCDateTimeMS AdjustedDateTime = Record.DateTime + sc.TimeScaleAdjustment; int BarIndex = sc.GetContainingIndexForSCDateTime(sc.ChartNumber, AdjustedDateTime); if (BarIndex < 0) BarIndex = sc.ArraySize - 1; if (BarIndex >= sc.ArraySize) continue; const int CurrentMS = AdjustedDateTime.GetTimeInMilliseconds(); bool Merge = false; if (Mode == 0) { Merge = AggVolume > 0 && Side == AggSide && Price == AggPrice && AggBar == BarIndex && std::abs(CurrentMS - AggMS) <= AggMSInput; } else if (Mode == 1) { Merge = AggVolume > 0 && Side == AggSide && AggBar == BarIndex && std::abs(CurrentMS - AggMS) <= AggMSInput; } else { Merge = false; } if (Merge) { AggVolume += Volume; AggMS = CurrentMS; if (Mode == 1) { AggPrice = Price; AggDateTime = AdjustedDateTime; AggBar = BarIndex; } } else { AtlasStoreAndDrawEventV2( sc, Events, MarkerIndex, AggBar, AggDateTime, AggPrice, AggVolume, AggSide, Threshold, MinSize, MaxSize, BuyColor.GetColor(), SellColor.GetColor(), HollowMarker.GetYesNo() != 0, ShowVolumeLabel.GetYesNo() != 0, Transparency.GetInt(), VolumeLabelFontSize.GetInt(), VolumeLabelColor.GetColor()); if (EnableAlerts.GetYesNo() && AggVolume >= Threshold) { SCString AlertMessage; AlertMessage.Format("Atlas Large Trade: %d contracts at %.2f", AggVolume, AggPrice); sc.SetAlert(1, AlertMessage); } AggPrice = Price; AggVolume = Volume; AggMS = CurrentMS; AggDateTime = AdjustedDateTime; AggBar = BarIndex; AggSide = Side; if (Mode == 2 && AggVolume >= Threshold) { AtlasStoreAndDrawEventV2( sc, Events, MarkerIndex, AggBar, AggDateTime, AggPrice, AggVolume, AggSide, Threshold, MinSize, MaxSize, BuyColor.GetColor(), SellColor.GetColor(), HollowMarker.GetYesNo() != 0, ShowVolumeLabel.GetYesNo() != 0, Transparency.GetInt(), VolumeLabelFontSize.GetInt(), VolumeLabelColor.GetColor()); if (EnableAlerts.GetYesNo()) { SCString AlertMessage; AlertMessage.Format("Atlas Large Trade: %d contracts at %.2f", AggVolume, AggPrice); sc.SetAlert(1, AlertMessage); } AggVolume = 0; } } } }