forked from MrBaro75/Warrior_EA
cooldown-recon put the chart-wide cooldown at the end of the overlay sweep. It executed ZERO times. This store's own header already said why: the sweep 're-arms only when an era ends. A DEPLOYED ensemble runs no further eras'. Five of six charts were deployed, so there were zero 'Filtered view: swept' lines in the entire session while the saved files still held 148 same-side pairs under 30 bars on XTIUSD. Moved to the completion of the progressive vote-arrow restore, which runs on every chart including deployed ones. The restore thinning alone was never going to be enough either: MT5 persists chart objects in profiles\Charts\*\chart*.chr, so arrows drawn under an older window are ALREADY on the chart when the process starts, and a freshly-thinned restore just adds to them. Two correctly-thinned sets still union into clusters. The chart is the only authority. Same construction as before: OBJ_TREND only (the line is the canonical half of a mark, matching Snapshot()), sorted by time first because object order is not time order, and the gap>0 guard so a mis-ordered set fails visibly by keeping rather than silently by deleting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
536 lines
25 KiB
MQL5
536 lines
25 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Warrior_EA |
|
|
//| AnimateDread |
|
|
//| |
|
|
//| PERSISTENCE FOR THE VOTE ARROWS - the filtered view's own layer. |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef WARRIOR_CHART_VOTEARROWS_MQH
|
|
#define WARRIOR_CHART_VOTEARROWS_MQH
|
|
//+------------------------------------------------------------------+
|
|
//| WHY THIS EXISTS AS ITS OWN STORE. |
|
|
//| |
|
|
//| With DrawUnfilteredSignals OFF - the shipped default - the arrows |
|
|
//| on the chart are the COMBINED VOTE's (SIG_VOTE_PREFIX), drawn by |
|
|
//| the aggregate signal's overlay. They are the ones that answer |
|
|
//| "where would this EA actually have traded", because the overlay |
|
|
//| only draws a bar whose |vote| cleared Signal_ThresholdOpen. |
|
|
//| |
|
|
//| Nothing persisted them. CChartUI's .arrows sidecar is MEMBER- |
|
|
//| scoped (it scans m_view.ArrowPrefix()), so it never saw this |
|
|
//| layer at all, and the only route back was a full overlay re-sweep.|
|
|
//| That sweep replays each member's m_overlaySigSnap - an IN-MEMORY |
|
|
//| cache filled at pass-3 completion - and it re-arms only when an |
|
|
//| era ends. A DEPLOYED ensemble runs no further eras. So on a |
|
|
//| terminal restart the arrows were gone AND unrecoverable: no file |
|
|
//| to read them from, and no era ever coming to redraw them. |
|
|
//| |
|
|
//| THE THRESHOLDS ARE PART OF THE RECORD. An arrow means "the vote |
|
|
//| cleared the bar to open", so it is only true relative to the |
|
|
//| threshold it was measured against. The header stores the open and |
|
|
//| close thresholds the arrows were drawn under; on load a mismatch |
|
|
//| DISCARDS them rather than restoring a picture of a strategy the |
|
|
//| operator no longer runs. Showing stale arrows under a new |
|
|
//| threshold is worse than showing none: none is visibly empty, |
|
|
//| stale is confidently wrong. |
|
|
//+------------------------------------------------------------------+
|
|
#include "..\..\System\AtomicFile.mqh"
|
|
//--- 'WVA1'. Bump only if the record layout changes; the thresholds live IN the record, so a
|
|
//--- threshold change is a data mismatch, not a format one.
|
|
#define VOTE_ARROWS_MAGIC 0x57564131
|
|
//--- Same retention and pacing as the member arrows (see MAX_PERSISTED_ARROWS / ARROW_RESTORE_BUDGET_MS
|
|
//--- in ExpertSignalAIBase.mqh). Duplicated as its own names rather than borrowed: this layer holds one
|
|
//--- arrow per TRADE rather than one per model opinion, so it is legitimately allowed to diverge.
|
|
#define VOTE_ARROWS_MAX_KEPT 1000
|
|
#define VOTE_ARROWS_MAX_HEADER 50000
|
|
#define VOTE_ARROWS_BUDGET_MS 50
|
|
//+------------------------------------------------------------------+
|
|
//| Chart-scoped, single instance. The vote belongs to the CHART, not |
|
|
//| to any member - which is exactly why it could not live in |
|
|
//| CChartUI, whose every path is scoped to one model's prefix. |
|
|
//+------------------------------------------------------------------+
|
|
class CVoteArrowStore
|
|
{
|
|
private:
|
|
string m_key; // file basename, set once at init
|
|
double m_thresholdOpen; // what the stored arrows were measured against
|
|
double m_thresholdClose;
|
|
bool m_active; // false in tester/optimizer and in the raw view
|
|
//--- parsed-but-not-yet-drawn, consumed by AdvanceRestore()
|
|
datetime m_time[];
|
|
int m_code[];
|
|
double m_price[];
|
|
long m_tf[];
|
|
int m_index;
|
|
bool m_pending;
|
|
uint m_startMs;
|
|
int m_lastSaved;
|
|
//--- DEINIT SPLIT. Snapshot() is the chart-scan half of a save, held in memory with ZERO disk
|
|
//--- I/O, so OnDeinit can capture the arrows and purge the chart immediately; WriteSnapshot()
|
|
//--- is the disk half, run later with the heavy persistence. Measured 2026-08-25 18:23: two of
|
|
//--- six charts never reached their first cleanup step because the un-split Save() (a file
|
|
//--- write) sat ahead of it while four sibling charts flooded the same disk - "Abnormal
|
|
//--- termination" 5.9 s in, objects stranded.
|
|
long m_snapTimes[];
|
|
int m_snapCodes[];
|
|
double m_snapPrices[];
|
|
long m_snapTfs[];
|
|
int m_snapCount;
|
|
bool m_snapTaken;
|
|
|
|
string FileName(void) const { return m_key + ".votearrows"; }
|
|
|
|
public:
|
|
CVoteArrowStore(void) :
|
|
m_key(""), m_thresholdOpen(-1.0), m_thresholdClose(-1.0), m_active(false),
|
|
m_index(0), m_pending(false), m_startMs(0), m_lastSaved(0),
|
|
m_snapCount(0), m_snapTaken(false) {}
|
|
|
|
bool Active(void) const { return m_active; }
|
|
bool Pending(void) const { return m_pending; }
|
|
int LastSaved(void) const { return m_lastSaved; }
|
|
//--- TIME-ORDER THEN THIN. Called once at Load(), before the progressive restore starts drawing.
|
|
//---
|
|
//--- WHY IT IS NEEDED AT ALL: this store draws into the SAME object names as the overlay's own
|
|
//--- vote arrows (SIG_VOTE_PREFIX + bar time), so a restore that replays a file written BEFORE the
|
|
//--- signal cooldown existed puts back exactly the arrows the overlay's prune deleted. On a
|
|
//--- DEPLOYED chart that is the whole arrow set, because no era ever runs to re-sweep.
|
|
//---
|
|
//--- WHY IT SORTS FIRST, and this is not optional: Snapshot() walks ObjectsTotal(), so the record
|
|
//--- is in OBJECT order, not time order - its own comment says so. Walking an unsorted record with
|
|
//--- a spacing rule produces negative gaps, and a negative gap is always inside any window: that
|
|
//--- is precisely the bug that wiped 272 of 273 arrows off every chart in 2ca32e9.
|
|
//--- ENFORCE THE COOLDOWN OVER THE CHART ITSELF. Runs after the progressive restore finishes.
|
|
//---
|
|
//--- IT CANNOT LIVE AT THE END OF THE OVERLAY SWEEP, which is where it was first put: this store's
|
|
//--- own header explains why - the sweep "re-arms only when an era ends. A DEPLOYED ensemble runs
|
|
//--- no further eras". Five of six charts were deployed, so the sweep never ran and the
|
|
//--- reconciliation never executed once. Measured: zero "Filtered view: swept" lines in the whole
|
|
//--- session while the saved files still held 148 same-side pairs under 30 bars.
|
|
//---
|
|
//--- The restore alone is not enough either. MT5 persists chart objects in the .chr profile, so
|
|
//--- arrows drawn under an older window are ALREADY on the chart when this process starts, and a
|
|
//--- freshly-thinned restore simply adds to them. Two correctly-thinned sets still union into
|
|
//--- clusters. The chart is the only authority.
|
|
void ReconcileChartCooldown(void)
|
|
{
|
|
int window = WarriorSignalCooldownBars();
|
|
if(window <= 0)
|
|
return;
|
|
long gapMin = (long)window * PeriodSeconds();
|
|
datetime seen[];
|
|
int found = 0, totalObj = ObjectsTotal(0);
|
|
ArrayResize(seen, totalObj);
|
|
for(int oi = 0; oi < totalObj; oi++)
|
|
{
|
|
string on = ObjectName(0, oi);
|
|
if(StringFind(on, SIG_VOTE_PREFIX) != 0)
|
|
continue;
|
|
//--- OBJ_TREND only: a mark is a line AND an arrow, and the line is the canonical half.
|
|
//--- Snapshot() uses the same test for the same reason - counting both double-counts.
|
|
if(ObjectGetInteger(0, on, OBJPROP_TYPE) != OBJ_TREND)
|
|
continue;
|
|
seen[found++] = (datetime)ObjectGetInteger(0, on, OBJPROP_TIME, 0);
|
|
}
|
|
ArrayResize(seen, found);
|
|
if(found < 2)
|
|
return;
|
|
//--- Object order is NOT time order. An unsorted forward walk yields negative gaps, and a
|
|
//--- negative gap is inside any window - that is how a prune once deleted 272 of 273 arrows.
|
|
ArraySort(seen);
|
|
datetime lastKept = 0;
|
|
int killed = 0;
|
|
for(int v = 0; v < found; v++)
|
|
{
|
|
long gap = (long)(seen[v] - lastKept);
|
|
if(lastKept != 0 && gap > 0 && gap <= gapMin)
|
|
{
|
|
WarriorDeleteSignalMark(SIG_VOTE_PREFIX + TimeToString(seen[v]));
|
|
killed++;
|
|
continue;
|
|
}
|
|
lastKept = seen[v];
|
|
}
|
|
if(killed > 0)
|
|
{
|
|
ChartRedraw(0);
|
|
PrintFormat("Warrior: signal cooldown (%d bars) RECONCILED the chart - removed %d of %d vote"
|
|
" arrow(s). Whatever drew them, this runs last and enforces one window.",
|
|
window, killed, found);
|
|
}
|
|
}
|
|
void SortAndThin(void)
|
|
{
|
|
int n = ArraySize(m_time);
|
|
if(n <= 1)
|
|
return;
|
|
//--- Insertion sort on the four parallel arrays. n is capped at VOTE_ARROWS_MAX_KEPT (1000) and
|
|
//--- this runs ONCE per chart per session, on a path that has just done file I/O.
|
|
for(int i = 1; i < n; i++)
|
|
{
|
|
datetime kt = m_time[i];
|
|
int kc = m_code[i];
|
|
double kp = m_price[i];
|
|
long kf = m_tf[i];
|
|
int j = i - 1;
|
|
while(j >= 0 && m_time[j] > kt)
|
|
{
|
|
m_time[j + 1] = m_time[j];
|
|
m_code[j + 1] = m_code[j];
|
|
m_price[j + 1] = m_price[j];
|
|
m_tf[j + 1] = m_tf[j];
|
|
j--;
|
|
}
|
|
m_time[j + 1] = kt;
|
|
m_code[j + 1] = kc;
|
|
m_price[j + 1] = kp;
|
|
m_tf[j + 1] = kf;
|
|
}
|
|
int window = WarriorSignalCooldownBars();
|
|
if(window <= 0)
|
|
return;
|
|
long gapMin = (long)window * PeriodSeconds();
|
|
datetime lastKept = 0;
|
|
int w = 0;
|
|
for(int i = 0; i < n; i++)
|
|
{
|
|
long gap = (long)(m_time[i] - lastKept);
|
|
//--- `gap > 0` as well as `<= gapMin`, same guard as the overlay prune: if the record is not
|
|
//--- in the order this loop assumes it fails visibly by KEEPING, not silently by deleting.
|
|
if(lastKept != 0 && gap > 0 && gap <= gapMin)
|
|
continue;
|
|
lastKept = m_time[i];
|
|
m_time[w] = m_time[i];
|
|
m_code[w] = m_code[i];
|
|
m_price[w] = m_price[i];
|
|
m_tf[w] = m_tf[i];
|
|
w++;
|
|
}
|
|
if(w < n)
|
|
{
|
|
PrintFormat("Warrior: vote-arrow restore thinned %d of %d persisted arrow(s) to the %d-bar"
|
|
" signal cooldown - the file predates it, or was written under a shorter window.",
|
|
n - w, n, window);
|
|
ArrayResize(m_time, w);
|
|
ArrayResize(m_code, w);
|
|
ArrayResize(m_price, w);
|
|
ArrayResize(m_tf, w);
|
|
}
|
|
}
|
|
|
|
void Configure(const string key, const double thresholdOpen, const double thresholdClose,
|
|
const bool active);
|
|
void Load(void);
|
|
void AdvanceRestore(void);
|
|
void Snapshot(void);
|
|
bool WriteSnapshot(void);
|
|
bool Save(void) { Snapshot(); return WriteSnapshot(); }
|
|
void Discard(const string reason);
|
|
};
|
|
CVoteArrowStore g_voteArrows;
|
|
//+------------------------------------------------------------------+
|
|
//| Bind the store to this chart's configuration. `active` is false |
|
|
//| in the tester (throwaway charts) and in the raw view, where the |
|
|
//| per-member layer owns the chart and this one draws nothing. |
|
|
//+------------------------------------------------------------------+
|
|
void CVoteArrowStore::Configure(const string key, const double thresholdOpen, const double thresholdClose,
|
|
const bool active)
|
|
{
|
|
m_key = key;
|
|
m_thresholdOpen = thresholdOpen;
|
|
m_thresholdClose = thresholdClose;
|
|
m_active = active;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Read the sidecar and QUEUE it. Deliberately draws nothing here - |
|
|
//| same reason CChartUI::LoadChartSignals defers: OnInit must not |
|
|
//| block, and this queue can hold a thousand object pairs. |
|
|
//+------------------------------------------------------------------+
|
|
void CVoteArrowStore::Load(void)
|
|
{
|
|
if(!m_active || m_key == "")
|
|
return;
|
|
string fn = FileName();
|
|
if(!FileIsExist(fn, FILE_COMMON))
|
|
return;
|
|
//--- share flags: read-only, and must not fail because another instance holds the file.
|
|
int handle = FileOpen(fn, FILE_COMMON | FILE_BIN | FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE);
|
|
if(handle == INVALID_HANDLE)
|
|
return;
|
|
if(FileReadInteger(handle) != VOTE_ARROWS_MAGIC)
|
|
{
|
|
FileClose(handle);
|
|
return;
|
|
}
|
|
double savedOpen = FileReadDouble(handle);
|
|
double savedClose = FileReadDouble(handle);
|
|
int n = FileReadInteger(handle);
|
|
FileClose(handle);
|
|
//--- THE THRESHOLD GATE. See the header note: an arrow is a claim about a threshold, so a changed
|
|
//--- threshold makes every stored arrow a claim about a strategy that is no longer configured.
|
|
if(savedOpen != m_thresholdOpen || savedClose != m_thresholdClose)
|
|
{
|
|
Discard(StringFormat("thresholds changed (stored open %.0f / close %.0f, now open %.0f / close %.0f)"
|
|
" - those arrows describe a strategy this chart no longer runs, so they are"
|
|
" discarded rather than redrawn. They rebuild from the vote as the models run.",
|
|
savedOpen, savedClose, m_thresholdOpen, m_thresholdClose));
|
|
return;
|
|
}
|
|
if(n <= 0 || n > VOTE_ARROWS_MAX_HEADER)
|
|
{
|
|
if(n > VOTE_ARROWS_MAX_HEADER)
|
|
Print("Warrior: .votearrows header declares " + IntegerToString(n) + " arrows - refusing to restore (limit " +
|
|
IntegerToString(VOTE_ARROWS_MAX_HEADER) + "); the file looks corrupt.");
|
|
return;
|
|
}
|
|
//--- Reopen to stream the records: the header read above is cheap and lets a mismatched/corrupt
|
|
//--- file be rejected before any array is sized to it.
|
|
handle = FileOpen(fn, FILE_COMMON | FILE_BIN | FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE);
|
|
if(handle == INVALID_HANDLE)
|
|
return;
|
|
FileReadInteger(handle);
|
|
FileReadDouble(handle);
|
|
FileReadDouble(handle);
|
|
FileReadInteger(handle);
|
|
ArrayResize(m_time, n);
|
|
ArrayResize(m_code, n);
|
|
ArrayResize(m_price, n);
|
|
ArrayResize(m_tf, n);
|
|
int parsed = 0;
|
|
for(int i = 0; i < n && !FileIsEnding(handle); i++)
|
|
{
|
|
m_time[parsed] = (datetime)FileReadLong(handle);
|
|
m_code[parsed] = FileReadInteger(handle);
|
|
m_price[parsed] = FileReadDouble(handle);
|
|
m_tf[parsed] = FileReadLong(handle);
|
|
parsed++;
|
|
}
|
|
FileClose(handle);
|
|
//--- A truncated file yields fewer records than the header claimed - keep what parsed.
|
|
if(parsed != n)
|
|
{
|
|
ArrayResize(m_time, parsed);
|
|
ArrayResize(m_code, parsed);
|
|
ArrayResize(m_price, parsed);
|
|
ArrayResize(m_tf, parsed);
|
|
}
|
|
//--- BEFORE arming the restore: a replay that ignores the cooldown resurrects the very arrows the
|
|
//--- overlay prune removed, and on a deployed chart nothing would ever remove them again.
|
|
SortAndThin();
|
|
int queued = ArraySize(m_time);
|
|
m_index = 0;
|
|
m_pending = (queued > 0);
|
|
m_startMs = GetTickCount();
|
|
if(m_pending)
|
|
Print("Warrior: queued " + IntegerToString(queued) + " combined-vote arrows for progressive restore"
|
|
" (threshold to open " + DoubleToString(m_thresholdOpen, 0) + "%).");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Draw a time-boxed slice. Bounds THROUGHPUT, not latency to a |
|
|
//| stop - hence the IsStopped() check inside the loop as well. |
|
|
//+------------------------------------------------------------------+
|
|
void CVoteArrowStore::AdvanceRestore(void)
|
|
{
|
|
if(!m_pending)
|
|
return;
|
|
int total = ArraySize(m_time);
|
|
uint sliceStart = GetTickCount();
|
|
int drawn = 0;
|
|
while(m_index < total)
|
|
{
|
|
//--- Budget checked every 64 objects: GetTickCount() is not free, and at microseconds per object
|
|
//--- a per-iteration clock read would dominate the work being measured.
|
|
if(drawn > 0 && (drawn & 63) == 0 && GetTickCount() - sliceStart >= VOTE_ARROWS_BUDGET_MS)
|
|
break;
|
|
if(drawn > 0 && (drawn & 63) == 0 && IsStopped())
|
|
return;
|
|
datetime t = m_time[m_index];
|
|
int code = m_code[m_index];
|
|
double price = m_price[m_index];
|
|
long tf = m_tf[m_index];
|
|
m_index++;
|
|
drawn++;
|
|
bool isBuy = (code == WARRIOR_SIG_CODE_BUY);
|
|
string nm = SIG_VOTE_PREFIX + TimeToString(t);
|
|
//--- isTrade=true: this layer IS the trade layer, and it must keep the heavier styling that
|
|
//--- distinguishes it from a member's raw opinion (see WarriorPlotSignalLevel).
|
|
WarriorPlotSignalLevel(nm, t, (ENUM_TIMEFRAMES)Period(), price, isBuy, true,
|
|
StringFormat("would trade %s @ %s | vote cleared %.0f%% | restored from the"
|
|
" previous session",
|
|
(isBuy ? "BUY" : "SELL"),
|
|
DoubleToString(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS)),
|
|
m_thresholdOpen));
|
|
//--- Both halves, or a restore made while signals are hidden brings the lines back alone.
|
|
ObjectSetInteger(0, nm, OBJPROP_TIMEFRAMES, tf);
|
|
ObjectSetInteger(0, WarriorSignalArrowName(nm), OBJPROP_TIMEFRAMES, tf);
|
|
}
|
|
ChartRedraw(0);
|
|
if(m_index < total)
|
|
return; // more slices to come
|
|
m_pending = false;
|
|
Print("Warrior: restored " + IntegerToString(total) + " combined-vote arrows in " +
|
|
IntegerToString((int)(GetTickCount() - m_startMs)) + " ms (progressive, non-blocking).");
|
|
//--- LAST, once every arrow this store is going to draw is on the chart. Reconciles the union of
|
|
//--- what was restored and what MT5 already had in the profile - see the method's comment.
|
|
ReconcileChartCooldown();
|
|
ArrayFree(m_time);
|
|
ArrayFree(m_code);
|
|
ArrayFree(m_price);
|
|
ArrayFree(m_tf);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| SCAN HALF of a save: capture the drawn vote arrows into memory, |
|
|
//| zero disk I/O. See the m_snap* declaration comment - this is what |
|
|
//| lets OnDeinit purge the chart before any file write can block on |
|
|
//| a contended disk. |
|
|
//| |
|
|
//| Unlike the member layer a save does NOT clear the chart: the |
|
|
//| overlay redraws this layer from scratch on every sweep, so the |
|
|
//| file is a mirror of the chart rather than its only copy. |
|
|
//+------------------------------------------------------------------+
|
|
void CVoteArrowStore::Snapshot(void)
|
|
{
|
|
m_snapCount = 0;
|
|
m_snapTaken = true;
|
|
if(!m_active || m_key == "")
|
|
return;
|
|
int n = 0;
|
|
int total = ObjectsTotal(0);
|
|
//--- Room for the chart's arrows PLUS anything still sitting in the restore queue: a save that
|
|
//--- runs mid-restore would otherwise write only the part already drawn and silently truncate the
|
|
//--- history to the slice that happened to have finished.
|
|
int queueRemaining = m_pending ? (ArraySize(m_time) - m_index) : 0;
|
|
if(queueRemaining < 0)
|
|
queueRemaining = 0;
|
|
ArrayResize(m_snapTimes, total + queueRemaining);
|
|
ArrayResize(m_snapCodes, total + queueRemaining);
|
|
ArrayResize(m_snapPrices, total + queueRemaining);
|
|
ArrayResize(m_snapTfs, total + queueRemaining);
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
string nm = ObjectName(0, i);
|
|
if(StringFind(nm, SIG_VOTE_PREFIX) != 0)
|
|
continue;
|
|
//--- OBJ_TREND only: a mark is a line AND an arrow, and the line is the canonical half (it
|
|
//--- carries the trigger price). Counting both would write every arrow twice.
|
|
if(ObjectGetInteger(0, nm, OBJPROP_TYPE) != OBJ_TREND)
|
|
continue;
|
|
m_snapTimes[n] = (long)ObjectGetInteger(0, nm, OBJPROP_TIME, 0);
|
|
//--- Direction from the COLOUR - a line carries no arrow code. Same encoding as the member
|
|
//--- sidecar so both files stay readable by one convention.
|
|
m_snapCodes[n] = ((color)ObjectGetInteger(0, nm, OBJPROP_COLOR) == WARRIOR_SIG_BUY_COLOR)
|
|
? WARRIOR_SIG_CODE_BUY : WARRIOR_SIG_CODE_SELL;
|
|
m_snapPrices[n] = ObjectGetDouble(0, nm, OBJPROP_PRICE, 0);
|
|
m_snapTfs[n] = g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS;
|
|
n++;
|
|
}
|
|
for(int q = m_index; q < m_index + queueRemaining; q++)
|
|
{
|
|
m_snapTimes[n] = (long)m_time[q];
|
|
m_snapCodes[n] = m_code[q];
|
|
m_snapPrices[n] = m_price[q];
|
|
m_snapTfs[n] = g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS;
|
|
n++;
|
|
}
|
|
//--- Keep the most recent VOTE_ARROWS_MAX_KEPT by TIME (scan order is object order, not time order).
|
|
if(n > VOTE_ARROWS_MAX_KEPT)
|
|
{
|
|
long sortedTimes[];
|
|
ArrayResize(sortedTimes, n);
|
|
ArrayCopy(sortedTimes, m_snapTimes, 0, 0, n);
|
|
ArraySort(sortedTimes); // ascending
|
|
long cutoff = sortedTimes[n - VOTE_ARROWS_MAX_KEPT];
|
|
int w = 0;
|
|
for(int i = 0; i < n; i++)
|
|
{
|
|
//--- ">= cutoff" can match more than the cap when several arrows share the cut-off timestamp;
|
|
//--- the w < cap test keeps the kept set at exactly the cap in that case.
|
|
if(m_snapTimes[i] >= cutoff && w < VOTE_ARROWS_MAX_KEPT)
|
|
{
|
|
m_snapTimes[w] = m_snapTimes[i];
|
|
m_snapCodes[w] = m_snapCodes[i];
|
|
m_snapPrices[w] = m_snapPrices[i];
|
|
m_snapTfs[w] = m_snapTfs[i];
|
|
w++;
|
|
}
|
|
}
|
|
n = w;
|
|
}
|
|
m_snapCount = n;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| DISK HALF: write the captured snapshot, thresholds included. |
|
|
//| Consuming - the snapshot is freed either way, so a stale capture |
|
|
//| can never be replayed over a fresher chart. |
|
|
//+------------------------------------------------------------------+
|
|
bool CVoteArrowStore::WriteSnapshot(void)
|
|
{
|
|
if(!m_snapTaken)
|
|
return true;
|
|
m_snapTaken = false;
|
|
int n = m_snapCount;
|
|
m_snapCount = 0;
|
|
bool ok = true;
|
|
//--- NOTHING DRAWN IS NOT NOTHING TO SAY, but it is not a reason to destroy the record either: a
|
|
//--- chart that has not swept yet has no arrows and would otherwise wipe a good file on shutdown.
|
|
if(m_active && m_key != "" && n > 0)
|
|
{
|
|
string fn = FileName();
|
|
string tmpName = "";
|
|
int handle = AtomicWriteBegin(fn, FILE_COMMON, tmpName);
|
|
if(handle == INVALID_HANDLE)
|
|
{
|
|
Print("Warrior: ERROR - could not open " + tmpName + " to persist " + IntegerToString(n) +
|
|
" combined-vote arrows, error " + IntegerToString(GetLastError()));
|
|
ok = false;
|
|
}
|
|
else
|
|
{
|
|
ok = (FileWriteInteger(handle, VOTE_ARROWS_MAGIC) > 0);
|
|
if(ok && FileWriteDouble(handle, m_thresholdOpen) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteDouble(handle, m_thresholdClose) <= 0)
|
|
ok = false;
|
|
if(ok && FileWriteInteger(handle, n) <= 0)
|
|
ok = false;
|
|
for(int i = 0; ok && i < n; i++)
|
|
{
|
|
if(FileWriteLong(handle, m_snapTimes[i]) <= 0 || FileWriteInteger(handle, m_snapCodes[i]) <= 0 ||
|
|
FileWriteDouble(handle, m_snapPrices[i]) <= 0 || FileWriteLong(handle, m_snapTfs[i]) <= 0)
|
|
ok = false;
|
|
}
|
|
//--- Staged + atomically renamed, so an interrupted write keeps the previous arrow set rather
|
|
//--- than publishing a truncated one.
|
|
if(!AtomicWriteEnd(handle, fn, tmpName, FILE_COMMON, ok, __FUNCTION__))
|
|
ok = false;
|
|
else
|
|
if(ok)
|
|
m_lastSaved = n;
|
|
}
|
|
}
|
|
ArrayFree(m_snapTimes);
|
|
ArrayFree(m_snapCodes);
|
|
ArrayFree(m_snapPrices);
|
|
ArrayFree(m_snapTfs);
|
|
return ok;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Drop the stored arrows and the queue, and say why on the journal. |
|
|
//| The chart objects go too: whatever reason invalidated the file |
|
|
//| invalidates what is drawn from it. |
|
|
//+------------------------------------------------------------------+
|
|
void CVoteArrowStore::Discard(const string reason)
|
|
{
|
|
m_pending = false;
|
|
m_index = 0;
|
|
ArrayFree(m_time);
|
|
ArrayFree(m_code);
|
|
ArrayFree(m_price);
|
|
ArrayFree(m_tf);
|
|
ObjectsDeleteAll(0, SIG_VOTE_PREFIX);
|
|
if(m_key != "" && FileIsExist(FileName(), FILE_COMMON))
|
|
FileDelete(FileName(), FILE_COMMON);
|
|
ChartRedraw(0);
|
|
Print("Warrior: combined-vote arrows cleared - " + reason);
|
|
}
|
|
#endif // WARRIOR_CHART_VOTEARROWS_MQH
|
|
//+------------------------------------------------------------------+
|