#include #include #include "../data_framework/NormalVolumeBreakBars.h" #include "../data_framework/SimpleMarketData.h" #include "../alert_framework/AlertBase.h" #include "../data_framework/GenericL1Data.h" #include "../data_framework/GenericTosData.h" #include "../../shared/MarketHours.h" #include "ConsolidationPatterns.h" /* These are alerts releated to consolidations are confirmed running. Consolidation means that the price is moving less than expected, based on historical volatility. Consolidation defines a channel, as the highest and lowest price seen in the consolidation pattern. Running (Confirmed) means just the opposite. The price has picked a direction, and has moved in that direction faster than can be explained by random movements, based on historical volatility. Breakout / Breakdown (Confirmed) is a Consolidation transitioning directly into a Running (Confirmed). These are seperated into seperate alerts from the running alerts just so the user can more easily focus on these. These share the exact same logic, with an if statement deciding which alerts are running vs which are breakout / breakdown. (Fast) Breakout / Breakdown looks for the bid or ask to move beyond the channel. Note that these all work with our equal volume candles. On the user interface these do not name a time frame. Many of these have counterparts which use traditional candles. Those appear to the user with a time frame, such as a "5 minute consolidation breakdown". These will have similar icons, except that the alerts based on the traditional candles will have numbers in them, representing the time frame. The alerts defined in this file do not contain a number in the icon. */ ///////////////////////////////////////////////////////////////////// // Global ///////////////////////////////////////////////////////////////////// static double computeVariance(double high, double low, double volatility, int count) { assert(count > 0); // Otherwise this is meaningless. if (volatility == 0.0) // We don't expect to get here. Someone else should be checking to avoid // this. But this would be the right answer. return std::numeric_limits< double >::infinity(); return (high - low) / sqrt(count) / volatility; } static double runningQuality(double variance) { // This was created just to make the manual for Running Up Confirmed // and Running Down Confirmed look better. In retrospect, we should have // made this a ratio. That works well in a lot of other alerts. return variance * 8.0 - 9.0; } ///////////////////////////////////////////////////////////////////// // Channel ///////////////////////////////////////////////////////////////////// class Channel : public DataNode { public: struct Info { bool consolidated; bool interesting; // If consolidated is false, the rest of this is meaningless // and may not be set at all. double quality; // 2 is min for consolidation, 5 is min for strong consolidation, 10 is max. double highPrice; double lowPrice; Integer shares; int barCount; time_t startTime; time_t reportTime; }; private: NormalVolumeBreakBars *_barData; Info _current, _previous, _beforeThat; double _volatility; void updateCurrentState(); void onWakeup(int msgId); Channel(DataNodeArgument const &args); friend class DataNode; public: Info const &getCurrent() { return _current; } Info const &getPrevious() { return _previous; } Info const &getBeforeThat() { return _beforeThat; } static DataNodeLink *find(DataNodeListener *listener, int msgId, Channel *&node, std::string const &symbol) { return findHelper(listener, msgId, node, symbol); } }; void Channel::updateCurrentState() { static const VolumeBlocks::size_type CONSOLIDATION_MIN_LENGTH = 7; _current.consolidated = false; _current.interesting = false; VolumeBlocks const &bars = _barData->getBlocks(); double highestHigh = std::numeric_limits< double >::min(); double lowestLow = std::numeric_limits< double >::max(); double lowestVariance = std::numeric_limits< double >::max(); VolumeBlocks::size_type bestLength = 0; for (VolumeBlocks::size_type currentLength = 1; currentLength <= bars.size(); currentLength++) { const int currentBar = bars.size() - currentLength; highestHigh = std::max(highestHigh, bars[currentBar].high); lowestLow = std::min(lowestLow, bars[currentBar].low); if (currentLength >= CONSOLIDATION_MIN_LENGTH) { const double variance = (highestHigh - lowestLow) / sqrt(currentLength) / _volatility; if (variance < lowestVariance) { lowestVariance = variance; bestLength = currentLength; } } } if (bestLength > 0) { static const double MIN_QUALITY = 2.0; const double quality = 10 - 20 * lowestVariance; if (quality >= MIN_QUALITY) { _current.quality = quality; highestHigh = std::numeric_limits< double >::min(); lowestLow = std::numeric_limits< double >::max(); for (VolumeBlocks::size_type currentLength = 1; currentLength <= bestLength; currentLength++) { const int currentBar = bars.size() - currentLength; highestHigh = std::max(highestHigh, bars[currentBar].high); lowestLow = std::min(lowestLow, bars[currentBar].low); } _current.highPrice = highestHigh; _current.lowPrice = lowestLow; _current.startTime = bars[bars.size() - bestLength].startTime; _current.reportTime = getSubmitTime(); _current.shares = bestLength * _barData->getGroupBy(); _current.barCount = bestLength; _current.consolidated = true; _current.interesting = _current.highPrice > _current.lowPrice; } } } void Channel::onWakeup(int msgId) { _beforeThat = _previous; _previous = _current; updateCurrentState(); notifyListeners(); } Channel::Channel(DataNodeArgument const &args) { std::string const &symbol = args.getStringValue(); _volatility = getTickVolatility(symbol); if (_volatility >= MIN_VOLATILITY) { addAutoLink(NormalVolumeBreakBars::find(this, 0, _barData, symbol)); // It would be better to check for history here. We load all // historical bar data, which could have been collecting since // before this data node was created. Ideally we would initialize // ourselves by checking for consolidations immediately, and // checking what they would have been one bar ago. } _current.consolidated = false; _current.interesting = false; _previous.consolidated = false; _previous.interesting = false; _beforeThat.consolidated = false; _beforeThat.interesting = false; } ///////////////////////////////////////////////////////////////////// // Consolidation ///////////////////////////////////////////////////////////////////// class Consolidation : public Alert { private: Channel *_channelData; int _shouldSkip; Channel::Info _lastDisplayed; void onWakeup(int msgId); Consolidation(DataNodeArgument const &args); friend class GenericDataNodeFactory; }; void Consolidation::onWakeup(int msgId) { if (_channelData->getCurrent().consolidated) { if (_shouldSkip <= 0) { static const double MIN_TIGHT_QUALITY = 5.0; std::string msg; std::string altMsg; if (_channelData->getCurrent().quality >= MIN_TIGHT_QUALITY) { msg = "Strong consolidation"; altMsg = "sc=1&"; } else msg = "Consolidation"; if (_lastDisplayed.consolidated) { if (_channelData->getCurrent().quality < _lastDisplayed.quality) { msg += " (Decaying)"; altMsg += "chg=1&"; } else { msg += " (Improving)"; altMsg += "chg=-1&"; } } msg += ". $"; const std::string lowPrice = formatPrice(_channelData->getCurrent().lowPrice); msg += lowPrice; altMsg += "lp="; altMsg += lowPrice; msg += '-'; const std::string highPrice = formatPrice(_channelData->getCurrent().highPrice); msg += highPrice; altMsg += "&hp="; altMsg += highPrice; msg += ", Shares: "; msg += addCommas(_channelData->getCurrent().shares); altMsg += "&sh="; altMsg += ntoa(_channelData->getCurrent().shares); msg += ". Time: "; msg += durationString(_channelData->getCurrent().startTime, _channelData->getCurrent().reportTime); altMsg += "&d="; altMsg += ntoa(_channelData->getCurrent().reportTime - _channelData->getCurrent().startTime); report(msg, altMsg, _channelData->getCurrent().quality); _lastDisplayed = _channelData->getCurrent(); _shouldSkip = std::max(0, (int)ceil(log2(_lastDisplayed.barCount)) - 3); } else _shouldSkip--; } else { _shouldSkip = 0; _lastDisplayed.consolidated = false; } } Consolidation::Consolidation(DataNodeArgument const &args) : _shouldSkip(0) { _lastDisplayed.consolidated = false; _lastDisplayed.interesting = false; addAutoLink(Channel::find(this, 0, _channelData, args.getStringValue())); } ///////////////////////////////////////////////////////////////////// // RunningConfirmed ///////////////////////////////////////////////////////////////////// class RunningConfirmed : public Alert { private: bool _up; bool _breakoutRequired; bool _previouslyReported; double _tickVolatility; NormalVolumeBreakBars *_barData; Channel *_channelData; struct PastReport { int barNumber; double variance; }; typedef std::vector< PastReport > PastReports; PastReports _pastReports; void reportChannelBreak(Channel::Info channel, double variance); void addReport(int position, double variance); bool beatsPreviousReports(double variance, int position); void onWakeup(int msgId); // NewBarData double getVariance(double high, double low, int count = 1); RunningConfirmed(DataNodeArgument const &args); friend class GenericDataNodeFactory; }; double RunningConfirmed::getVariance(double high, double low, int count) { try { return computeVariance(high, low, _tickVolatility, count); } catch (...) { // We hope to catch this somewhere sooner, but the result is // the same. Invalid volatility (or count) means no alert. return 0.0; } } RunningConfirmed::RunningConfirmed(DataNodeArgument const &args) : _previouslyReported(false), _barData(NULL), _channelData(NULL) { DataNodeArgumentVector const &argList = args.getListValue(); assert(argList.size() == 3); // (Symbol, Up, BreakoutRequired) std::string const &symbol = argList[0].getStringValue(); _up = argList[1].getBooleanValue(); _breakoutRequired = argList[2].getBooleanValue(); _tickVolatility = getTickVolatility(symbol); if (_tickVolatility >= MIN_VOLATILITY) { addAutoLink(Channel::find(this, 0, _channelData, symbol)); addAutoLink(NormalVolumeBreakBars::find(NULL, 0, _barData, symbol)); } } void RunningConfirmed::reportChannelBreak(Channel::Info channel, double variance) { std::string msg = "Channel break"; std::string altMsg; if (_up) msg += "out"; else msg += "down"; msg += " from $"; const std::string lowPrice = formatPrice(channel.lowPrice); msg += lowPrice; altMsg += "lp="; altMsg += lowPrice; msg += '-'; const std::string highPrice = formatPrice(channel.highPrice); msg += highPrice; altMsg += "&hp="; altMsg += highPrice; msg += ", "; msg += durationString(channel.startTime, getSubmitTime()); altMsg += "&d="; altMsg += ntoa(getSubmitTime() - channel.startTime); msg += '.'; report(msg, altMsg, runningQuality(variance)); } void RunningConfirmed::addReport(int position, double variance) { int newIndex = _pastReports.size(); while (true) { if (newIndex == 0) break; if (variance < _pastReports[newIndex - 1].variance) break; newIndex--; } _pastReports.resize(newIndex + 1); _pastReports[newIndex].barNumber = position; _pastReports[newIndex].variance = variance; } bool RunningConfirmed::beatsPreviousReports(double variance, int position) { for (PastReports::const_reverse_iterator it = _pastReports.rbegin(); it != _pastReports.rend(); it++) { if (it->barNumber < position) return true; if (it->variance >= variance) return false; } return true; } // NewBarData void RunningConfirmed::onWakeup(int msgId) { static const double MIN_VARIANCE = 1.25; static const double MIN_STRONG_VARIANCE = 1.75; const bool reportedLastTime = _previouslyReported; _previouslyReported = false; const int lastBar = _barData->getBlockCount() - 1; if (lastBar <= 0) return; VolumeBlocks const &bars = _barData->getBlocks(); if (!reportedLastTime) { if (_channelData->getCurrent().consolidated) return; if (_channelData->getPrevious().consolidated) { double variance; if (_up) variance = std::min(getVariance(bars[lastBar].high, bars[lastBar-1].high), getVariance(bars[lastBar].low, bars[lastBar-1].low)); else variance = std::min(getVariance(bars[lastBar-1].high, bars[lastBar].high), getVariance(bars[lastBar-1].low, bars[lastBar].low)); if (variance > MIN_VARIANCE) { if (_breakoutRequired) reportChannelBreak(_channelData->getPrevious(), variance); _previouslyReported = true; return; } } else if (_channelData->getBeforeThat().consolidated) { double variance; if (_up) variance = std::min(getVariance(bars[lastBar].high, bars[lastBar-2].high), getVariance(bars[lastBar].low, bars[lastBar-2].low)); else variance = std::min(getVariance(bars[lastBar-2].high, bars[lastBar].high), getVariance(bars[lastBar-2].low, bars[lastBar].low)); if (variance > MIN_VARIANCE) { if (_breakoutRequired) reportChannelBreak(_channelData->getPrevious(), variance); _previouslyReported = true; return; } } } if (!_breakoutRequired) { int count = 0; double highestVariance = 0.0; int highestVariancePosition = -1; for (int i = lastBar - 1; i >= 0; i--) { count++; double highsVariance, lowsVariance; if (_up) { if ((bars[lastBar].high <= bars[i].high) || (bars[lastBar].low <= bars[i].low)) break; highsVariance = getVariance(bars[lastBar].high, bars[i].high, count); lowsVariance = getVariance(bars[lastBar].low, bars[i].low, count); } else // down { if ((bars[lastBar].high >= bars[i].high) || (bars[lastBar].low >= bars[i].low)) break; highsVariance = getVariance(bars[i].high, bars[lastBar].high, count); lowsVariance = getVariance(bars[i].low, bars[lastBar].low, count); } const double variance = std::min(highsVariance, lowsVariance); if (variance > highestVariance) if (beatsPreviousReports(variance, i)) { highestVariance = variance; highestVariancePosition = i; } } if (highestVariance > MIN_VARIANCE) { std::string msg; std::string altMsg; if (_up) msg = "Running up"; else msg = "Running down"; if (highestVariance > MIN_STRONG_VARIANCE) { msg += " briskly"; altMsg = "br=1&"; } msg += ": "; const double amount = _up ?(bars[lastBar].high - bars[highestVariancePosition].low) :(bars[lastBar].low - bars[highestVariancePosition].high); msg += formatPrice(amount, true); altMsg += "a="; altMsg += formatPrice(amount); msg += " in "; msg += durationString(bars[highestVariancePosition].startTime, bars[lastBar].endTime); altMsg += "&d="; altMsg += ntoa(bars[lastBar].endTime - bars[highestVariancePosition].startTime); msg += ". Confirmed by volume."; report(msg, altMsg, runningQuality(highestVariance)); addReport(lastBar - 1, highestVariance); } } } //////////////////////////////////////////////////////////////////////// // ChannelBreakout //////////////////////////////////////////////////////////////////////// class ChannelBreakout : public Alert { private: GenericL1DataNode *_l1Data; GenericTosDataNode *_tosData; Channel *_channelData; double _resistance; time_t _lastReport; bool _primed; bool _channelValid; void newL1Data(); void newTosData(); void newChannelData(); enum { wL1Data, wTosData, wChannelData }; void onWakeup(int msgId); void reportNow(); ChannelBreakout(DataNodeArgument const &args); friend class GenericDataNodeFactory; }; void ChannelBreakout::onWakeup(int msgId) { switch (msgId) { case wL1Data: newL1Data(); break; case wTosData: newTosData(); break; case wChannelData: newChannelData(); break; } } ChannelBreakout::ChannelBreakout(DataNodeArgument const &args) : _lastReport(0), _primed(false), _channelValid(false) { std::string const &symbol = args.getStringValue(); addAutoLink(GenericL1DataNode::find(this, wL1Data, _l1Data, symbol)); addAutoLink(GenericTosDataNode::find(this, wTosData, _tosData, symbol)); addAutoLink(Channel::find(this, wChannelData, _channelData, symbol)); } void ChannelBreakout::newL1Data() { if ((!_primed) || (!_channelValid) || (!_l1Data->getValid())) return; L1Data const ¤t = _l1Data->getCurrent(); if ((current.bidPrice == 0) || (current.askPrice == 0) || (current.bidPrice > current.askPrice)) return; if (current.bidPrice > _resistance) reportNow(); } void ChannelBreakout::newTosData() { if (_primed || (!_channelValid) || (!_l1Data->getValid()) || (!_tosData->getValid())) return; L1Data const ¤t = _l1Data->getCurrent(); if ((current.bidPrice == 0) || (current.askPrice == 0) || (current.bidPrice > current.askPrice)) return; TosData const &last = _tosData->getLast(); double channelTop = _channelData->getCurrent().highPrice; if ((current.bidPrice == 0) || (current.askPrice == 0) || (current.bidPrice > current.askPrice)) return; if ((current.askPrice <= channelTop) && (last.price < channelTop)) _primed = true; } void ChannelBreakout::newChannelData() { _lastReport = 0; Channel::Info const ¤t = _channelData->getCurrent(); if (!current.interesting) _channelValid = false; else { _channelValid = true; Channel::Info const &previous = _channelData->getPrevious(); if (previous.interesting) _resistance = std::max(current.highPrice, previous.highPrice); else _primed = true; newL1Data(); } } void ChannelBreakout::reportNow() { if ((_lastReport == 0) || ((getSubmitTime() - _lastReport) > MARKET_HOURS_MINUTE)) { Channel::Info channel = _channelData->getCurrent(); std::string msg = "Channel breakout from $"; const std::string lowPrice = formatPrice(channel.lowPrice); msg += lowPrice; std::string altMsg = "lp="; altMsg += lowPrice; msg += '-'; const std::string highPrice = formatPrice(channel.highPrice); msg += highPrice; altMsg += "&hp="; altMsg += highPrice; msg += ", "; msg += durationString(channel.startTime, getSubmitTime()); altMsg += "&d="; altMsg += ntoa(getSubmitTime() - channel.startTime); msg += '.'; report(msg, altMsg, channel.quality); _lastReport = getSubmitTime(); _primed = false; } } //////////////////////////////////////////////////////////////////////// // class ChannelBreakdown //////////////////////////////////////////////////////////////////////// class ChannelBreakdown : public Alert { private: GenericL1DataNode *_l1Data; GenericTosDataNode *_tosData; Channel *_channelData; time_t _lastReport; bool _primed; bool _channelValid; double _support; void newL1Data(); void newTosData(); void newChannelData(); enum { wL1Data, wTosData, wChannelData }; void onWakeup(int msgId); void reportNow(); ChannelBreakdown(DataNodeArgument const &args); friend class GenericDataNodeFactory; }; void ChannelBreakdown::newL1Data() { if ((!_primed) || (!_channelValid) || (!_l1Data->getValid())) return; L1Data const ¤t = _l1Data->getCurrent(); if ((current.bidPrice == 0) || (current.askPrice == 0) || (current.bidPrice > current.askPrice)) return; if (current.askPrice < _support) reportNow(); } void ChannelBreakdown::newTosData() { if (_primed || (!_channelValid) || (!_l1Data->getValid())|| (!_tosData->getValid())) return; L1Data const ¤t = _l1Data->getCurrent(); if ((current.bidPrice == 0) || (current.askPrice == 0) || (current.bidPrice > current.askPrice)) return; TosData const &last = _tosData->getLast(); double channelBottom = _channelData->getCurrent().lowPrice; if ((current.bidPrice >= channelBottom) && (last.price > channelBottom)) _primed = true; } void ChannelBreakdown::newChannelData() { _lastReport = 0; Channel::Info current = _channelData->getCurrent(); if (!current.interesting) _channelValid = false; else { _channelValid = true; Channel::Info previous = _channelData->getPrevious(); if (previous.interesting) _support= std::min(current.lowPrice, previous.lowPrice); else _support = current.lowPrice; _primed = true; newL1Data(); } } ChannelBreakdown::ChannelBreakdown(DataNodeArgument const &args) : _lastReport(0), _primed(false), _channelValid(false) { std::string const &symbol = args.getStringValue(); addAutoLink(GenericL1DataNode::find(this, wL1Data, _l1Data, symbol)); addAutoLink(GenericTosDataNode::find(this, wTosData, _tosData, symbol)); addAutoLink(Channel::find(this, wChannelData, _channelData, symbol)); } void ChannelBreakdown::onWakeup(int msgId) { switch (msgId) { case wL1Data: newL1Data(); break; case wTosData: newTosData(); break; case wChannelData: newChannelData(); break; } } void ChannelBreakdown::reportNow() { if ((_lastReport == 0) || (getSubmitTime() - _lastReport > MARKET_HOURS_MINUTE)) { Channel::Info channel = _channelData->getCurrent(); std::string msg = "Channel breakdown from $"; const std::string lowPrice = formatPrice(channel.lowPrice); msg += lowPrice; std::string altMsg = "lp="; altMsg += lowPrice; msg += '-'; const std::string highPrice = formatPrice(channel.highPrice); msg += highPrice; altMsg += "&hp="; altMsg += highPrice; msg += ", "; msg += durationString(channel.startTime, getSubmitTime()); altMsg += "&d="; altMsg += ntoa(getSubmitTime() - channel.startTime); report (msg, altMsg, channel.quality); } _lastReport = getSubmitTime(); _primed = false; } //////////////////////////////////////////////////////////////////////// // TTrendStrength // // This was available in the Delphi code. This was an attempt to offer // the running up and down alerts as a filter. It never really worked out. // Part of the problem was that you could see the same thing as moving up // or moving down at the same time. (The alert version avoids that problem // by only showing an alert when the value gets more extreme.) Also, the // volume confirmed stuff in general wasn't very popular. We have filters // which say how much you've moved on a traditional candle chart. // // There are no plans to implement this in the C++ code. //////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////// // Global //////////////////////////////////////////////////////////////////////// void initializeConsolidationPatterns() { GenericDataNodeFactory::storeStandardFactory< Consolidation > ("Consolidation"); GenericDataNodeFactory::sf< RunningConfirmed > ("RunningUpConfirmed", symbolPlaceholderObject, true, false); GenericDataNodeFactory::sf< RunningConfirmed > ("RunningDownConfirmed", symbolPlaceholderObject, false, false); GenericDataNodeFactory::sf< RunningConfirmed > ("ChannelBreakoutConfirmed", symbolPlaceholderObject, true, true); GenericDataNodeFactory::sf< RunningConfirmed > ("ChannelBreakdownConfirmed", symbolPlaceholderObject, false, true); GenericDataNodeFactory::storeStandardFactory< ChannelBreakout > ("ChannelBreakout"); GenericDataNodeFactory::storeStandardFactory< ChannelBreakdown > ("ChannelBreakdown"); }