// FirmTape Levels for Sierra Chart (ACSIL). // // Draws FirmTape's dealer-gamma levels on a Sierra Chart price chart: the zero-gamma flip, // call resistance (call wall), put support (put wall), optionally the open-interest book's // three levels, and a gamma-by-strike profile at the right edge. The numbers come from the // FirmTape levels API v1 (https://firmtape.com/api/v1), with the customer's own API token. // It reads and draws only: it places no orders and touches no trading function. // // Build: copy this one file into Sierra Chart's ACS_Source folder, then // Analysis > Build Custom Studies DLL. Visual C++ (C++17); needs only sierrachart.h and the // standard library. // // The API as this file uses it: // GET /api/v1/levels?sym=ES&key=K -> { book, levels: [ Level ] } // GET /api/v1/gamma?sym=ES&book=vol&key=K -> { sym, book, session, as_of, unit, strikes[], gamma[], basis? } // errors -> { error: { code, message, pricing?, retry_after_s? } } // sc.MakeHTTPRequest cannot set headers, so the key travels as ?key=. It exposes neither the // HTTP status nor the headers either, so an error is read from the JSON body only. // // Layout of this file: // 1. the pure part (namespace firmtape): JSON reader, answer parsing, symbol mapping, // timing rules, text formatting. No Sierra types. sierra/test/ compiles it alone with // FIRMTAPE_PURE_ONLY defined and runs it against saved answers. // 2. the study (scsf_FirmTapeLevels): inputs, the request state machine, the drawings. // // Code is ASCII only on purpose: Sierra's build may not read the file as UTF-8. #ifndef FIRMTAPE_PURE_ONLY #include "sierrachart.h" #endif #include #include #include #include #include #include #include #include #include // Distribution of a compiled DLL (see sierra/distribution.md). Off for the source build that // customers compile themselves; the build we ship as a DLL sets it to 1. #ifndef FIRMTAPE_REQUIRE_SC_ALLOW_LIST #define FIRMTAPE_REQUIRE_SC_ALLOW_LIST 0 #endif namespace firmtape { // ========================================================================================= // 1. THE PURE PART // ========================================================================================= static const char* const kBaseUrl = "https://firmtape.com"; static const int kMinPollSeconds = 3; static const int kMaxBackoffSeconds = 120; static const int kFatalRetrySeconds = 60; static const int kRequestTimeoutSeconds = 30; static const char* const kSierraHttpError = "HTTP_REQUEST_ERROR"; // ---------------------------------------------------------------------------- JSON // A JSON value. Objects keep their keys and values in two parallel vectors, which keeps the // type simple (a vector of an incomplete type is fine in C++17; a pair of one is less sure). struct Json { enum Kind { Null, Bool, Number, String, Array, Object }; Kind kind = Null; bool boolean = false; double number = 0.0; std::string text; std::vector items; // array elements, or object values std::vector keys; // object keys, parallel to items // The member named `key`, or nullptr when this is not an object or has no such member. const Json* Get(const char* key) const { if (kind != Object) return nullptr; for (size_t i = 0; i < keys.size(); ++i) if (keys[i] == key) return &items[i]; return nullptr; } bool IsNumber() const { return kind == Number; } bool IsObject() const { return kind == Object; } bool IsArray() const { return kind == Array; } }; // A strict, small JSON reader. Numbers are parsed by hand rather than with strtod, whose // decimal separator follows the process locale. class JsonReader { public: static bool Parse(const char* s, size_t n, Json& out) { JsonReader r(s, s + n); out = Json(); if (!r.Value(out)) return false; r.Ws(); return r.p == r.end; } static bool Parse(const std::string& s, Json& out) { return Parse(s.data(), s.size(), out); } private: const char* p; const char* end; int depth; JsonReader(const char* b, const char* e) : p(b), end(e), depth(0) {} void Ws() { while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) ++p; } bool Literal(const char* w) { size_t n = std::strlen(w); if ((size_t)(end - p) < n || std::memcmp(p, w, n) != 0) return false; p += n; return true; } bool Value(Json& v) { if (++depth > 64) return false; Ws(); bool ok = false; if (p >= end) ok = false; else if (*p == '{') ok = ObjectValue(v); else if (*p == '[') ok = ArrayValue(v); else if (*p == '"') { v.kind = Json::String; ok = StringValue(v.text); } else if (*p == 't') { v.kind = Json::Bool; v.boolean = true; ok = Literal("true"); } else if (*p == 'f') { v.kind = Json::Bool; v.boolean = false; ok = Literal("false"); } else if (*p == 'n') { v.kind = Json::Null; ok = Literal("null"); } else if (*p == '-' || (*p >= '0' && *p <= '9')) { v.kind = Json::Number; ok = NumberValue(v.number); } --depth; return ok; } bool ObjectValue(Json& v) { v.kind = Json::Object; ++p; // '{' Ws(); if (p < end && *p == '}') { ++p; return true; } for (;;) { Ws(); if (p >= end || *p != '"') return false; std::string key; if (!StringValue(key)) return false; Ws(); if (p >= end || *p != ':') return false; ++p; v.keys.push_back(key); v.items.push_back(Json()); if (!Value(v.items.back())) return false; Ws(); if (p >= end) return false; if (*p == ',') { ++p; continue; } if (*p == '}') { ++p; return true; } return false; } } bool ArrayValue(Json& v) { v.kind = Json::Array; ++p; // '[' Ws(); if (p < end && *p == ']') { ++p; return true; } for (;;) { v.items.push_back(Json()); if (!Value(v.items.back())) return false; Ws(); if (p >= end) return false; if (*p == ',') { ++p; continue; } if (*p == ']') { ++p; return true; } return false; } } static int Hex(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } bool Hex4(unsigned& cp) { if (end - p < 4) return false; cp = 0; for (int i = 0; i < 4; ++i) { int h = Hex(p[i]); if (h < 0) return false; cp = cp * 16 + (unsigned)h; } p += 4; return true; } static void Utf8(std::string& o, unsigned cp) { if (cp < 0x80) o += (char)cp; else if (cp < 0x800) { o += (char)(0xC0 | (cp >> 6)); o += (char)(0x80 | (cp & 0x3F)); } else if (cp < 0x10000) { o += (char)(0xE0 | (cp >> 12)); o += (char)(0x80 | ((cp >> 6) & 0x3F)); o += (char)(0x80 | (cp & 0x3F)); } else { o += (char)(0xF0 | (cp >> 18)); o += (char)(0x80 | ((cp >> 12) & 0x3F)); o += (char)(0x80 | ((cp >> 6) & 0x3F)); o += (char)(0x80 | (cp & 0x3F)); } } bool StringValue(std::string& o) { o.clear(); ++p; // opening quote while (p < end) { char c = *p++; if (c == '"') return true; if ((unsigned char)c < 0x20) return false; if (c != '\\') { o += c; continue; } if (p >= end) return false; char e = *p++; switch (e) { case '"': o += '"'; break; case '\\': o += '\\'; break; case '/': o += '/'; break; case 'b': o += '\b'; break; case 'f': o += '\f'; break; case 'n': o += '\n'; break; case 'r': o += '\r'; break; case 't': o += '\t'; break; case 'u': { unsigned cp = 0; if (!Hex4(cp)) return false; if (cp >= 0xD800 && cp <= 0xDBFF) { unsigned lo = 0; if (end - p < 6 || p[0] != '\\' || p[1] != 'u') return false; p += 2; if (!Hex4(lo) || lo < 0xDC00 || lo > 0xDFFF) return false; cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); } Utf8(o, cp); break; } default: return false; } } return false; } bool NumberValue(double& out) { bool neg = false; if (*p == '-') { neg = true; ++p; } if (p >= end || *p < '0' || *p > '9') return false; std::uint64_t mant = 0; int digits = 0, exp10 = 0; if (*p == '0') ++p; else while (p < end && *p >= '0' && *p <= '9') { if (digits < 18) { mant = mant * 10 + (std::uint64_t)(*p - '0'); if (mant) ++digits; } else ++exp10; ++p; } if (p < end && *p == '.') { ++p; if (p >= end || *p < '0' || *p > '9') return false; while (p < end && *p >= '0' && *p <= '9') { if (digits < 18) { mant = mant * 10 + (std::uint64_t)(*p - '0'); if (mant) ++digits; --exp10; } ++p; } } if (p < end && (*p == 'e' || *p == 'E')) { ++p; bool eneg = false; if (p < end && (*p == '+' || *p == '-')) { eneg = *p == '-'; ++p; } if (p >= end || *p < '0' || *p > '9') return false; int e = 0; while (p < end && *p >= '0' && *p <= '9') { if (e < 10000) e = e * 10 + (*p - '0'); ++p; } exp10 += eneg ? -e : e; } double v = (double)mant; // Dividing by an exact power of ten (10^k is exact up to k = 22) rounds correctly, // so "6752.25" comes back as the double nearest 6752.25. if (exp10 < 0) v = (-exp10 <= 22) ? v / std::pow(10.0, -exp10) : v * std::pow(10.0, exp10); else if (exp10 > 0) v *= std::pow(10.0, exp10); out = neg ? -v : v; return std::isfinite(out); } }; // ---------------------------------------------------------------------------- readings // A number that may be JSON null. struct Opt { bool has = false; double v = 0.0; }; static Opt NumAt(const Json* o, const char* key) { Opt r; const Json* x = o ? o->Get(key) : nullptr; if (x && x->IsNumber()) { r.has = true; r.v = x->number; } return r; } static std::string StrAt(const Json* o, const char* key) { const Json* x = o ? o->Get(key) : nullptr; return (x && x->kind == Json::String) ? x->text : std::string(); } // One symbol's reading from /api/v1/levels: the volume book, plus the OI book under `oi`. struct Level { std::string sym, contract, session, cadenceBook, unit, gammaSign; Opt price, flip, callWall, putWall, netGamma, basis; bool hasOi = false; Opt oiFlip, oiCallWall, oiPutWall; double ageS = 0.0; // server's age of the book at answer time long long asOfUnix = 0; std::string errorCode; // set when this one symbol failed (unknown_symbol, no_data, no_basis) }; static Level LevelFromJson(const Json& L) { Level x; x.sym = StrAt(&L, "sym"); const Json* e = L.Get("error"); if (e && e->IsObject()) { x.errorCode = StrAt(e, "code"); if (x.errorCode.empty()) x.errorCode = "no_data"; return x; } x.contract = StrAt(&L, "contract"); x.session = StrAt(&L, "session"); x.price = NumAt(&L, "price"); x.flip = NumAt(&L, "flip"); x.callWall = NumAt(&L, "call_wall"); x.putWall = NumAt(&L, "put_wall"); const Json* ng = L.Get("net_gamma"); x.netGamma = NumAt(ng, "value"); x.gammaSign = StrAt(ng, "sign"); x.unit = StrAt(ng, "unit"); Opt age = NumAt(&L, "age_s"); x.ageS = age.has ? age.v : 0.0; Opt asof = NumAt(&L, "as_of_unix"); x.asOfUnix = asof.has ? (long long)asof.v : 0; x.cadenceBook = StrAt(L.Get("cadence"), "book"); x.basis = NumAt(L.Get("basis"), "value"); const Json* oi = L.Get("oi"); if (oi && oi->IsObject()) { x.hasOi = true; x.oiFlip = NumAt(oi, "flip"); x.oiCallWall = NumAt(oi, "call_wall"); x.oiPutWall = NumAt(oi, "put_wall"); } return x; } // Gamma by strike from /api/v1/gamma. For ES and NQ the strikes are already moved by the // basis, so they sit on the futures price axis. struct Gamma { std::string sym, book, session, unit, asOf; std::vector strikes, gamma; }; static Gamma GammaFromJson(const Json& g) { Gamma x; x.sym = StrAt(&g, "sym"); x.book = StrAt(&g, "book"); x.session = StrAt(&g, "session"); x.unit = StrAt(&g, "unit"); x.asOf = StrAt(&g, "as_of"); const Json* k = g.Get("strikes"); const Json* v = g.Get("gamma"); if (k && v && k->IsArray() && v->IsArray()) { size_t n = k->items.size() < v->items.size() ? k->items.size() : v->items.size(); for (size_t i = 0; i < n; ++i) if (k->items[i].IsNumber() && v->items[i].IsNumber()) { x.strikes.push_back(k->items[i].number); x.gamma.push_back(v->items[i].number); } } return x; } // ---------------------------------------------------------------------------- answers // What one HTTP answer means for the study. struct Answer { enum Kind { Ok, // a reading (level or gamma) to keep Failed, // an error to show; keep or clear the drawing depending on `fatal` Foreign // valid JSON that is not an answer to this request (another study's); ignore }; Kind kind = Failed; std::string code; // API error code, or "network", "bad_json" std::string text; // the words the status line shows bool fatal = false; // a retry will not cure it: 401, 403, unknown_symbol int retryAfterS = 0; // from error.retry_after_s (429) Level level; Gamma gamma; }; // The short words the status line shows for an error. static std::string DescribeError(const std::string& code) { if (code == "no_key") return "key missing"; if (code == "bad_key") return "key rejected"; if (code == "live_required") return "LIVE plan required"; if (code == "rate_limited") return "rate limited"; if (code == "unknown_symbol") return "symbol not covered"; if (code == "no_basis") return "no basis yet"; if (code == "contract_not_live") return "contract not measured today"; if (code == "no_data") return "no data"; if (code == "unavailable") return "server busy, retrying"; if (code == "network") return "request failed, retrying"; if (code == "timeout") return "no answer, retrying"; if (code == "bad_json") return "bad answer from server"; return "error " + code; } static bool IsFatalCode(const std::string& code) { return code == "no_key" || code == "bad_key" || code == "live_required" || code == "unknown_symbol"; } static Answer Fail(const std::string& code, int retryAfterS) { Answer a; a.kind = Answer::Failed; a.code = code; a.text = DescribeError(code); a.fatal = IsFatalCode(code); a.retryAfterS = retryAfterS; if (code == "rate_limited" && a.retryAfterS <= 0) a.retryAfterS = 60; return a; } // {"error": {...}} -> a failure; anything else -> false. static bool ReadErrorBody(const Json& root, Answer& out) { const Json* e = root.Get("error"); if (!e || !e->IsObject()) return false; std::string code = StrAt(e, "code"); if (code.empty()) code = "unavailable"; Opt r = NumAt(e, "retry_after_s"); out = Fail(code, r.has ? (int)std::ceil(r.v) : 0); return true; } // Sierra's text for a request that failed (network down, or a non-2xx status whose body // Sierra did not pass on). Which of the two it is cannot be told apart from here. static bool IsSierraHttpError(const std::string& body) { return body.compare(0, std::strlen(kSierraHttpError), kSierraHttpError) == 0; } // Interpret an answer of /api/v1/levels (one symbol asked). static Answer InterpretLevels(const std::string& body) { if (IsSierraHttpError(body)) return Fail("network", 0); Json root; if (!JsonReader::Parse(body, root) || !root.IsObject()) return Fail("bad_json", 0); Answer a; if (ReadErrorBody(root, a)) return a; const Json* levels = root.Get("levels"); if (!levels) { a.kind = Answer::Foreign; return a; } if (!levels->IsArray() || levels->items.empty()) return Fail("no_data", 0); Level L = LevelFromJson(levels->items[0]); if (!L.errorCode.empty()) return Fail(L.errorCode, 0); a.kind = Answer::Ok; a.level = L; return a; } // Interpret an answer of /api/v1/gamma. static Answer InterpretGamma(const std::string& body) { if (IsSierraHttpError(body)) return Fail("network", 0); Json root; if (!JsonReader::Parse(body, root) || !root.IsObject()) return Fail("bad_json", 0); Answer a; if (ReadErrorBody(root, a)) return a; if (!root.Get("strikes")) { a.kind = Answer::Foreign; return a; } a.kind = Answer::Ok; a.gamma = GammaFromJson(root); return a; } // ---------------------------------------------------------------------------- symbols static std::string Upper(std::string s) { for (char& c : s) if (c >= 'a' && c <= 'z') c = (char)(c - 'a' + 'A'); return s; } static std::string Trim(const std::string& s) { size_t b = 0, e = s.size(); while (b < e && (s[b] == ' ' || s[b] == '\t' || s[b] == '\r' || s[b] == '\n')) ++b; while (e > b && (s[e - 1] == ' ' || s[e - 1] == '\t' || s[e - 1] == '\r' || s[e - 1] == '\n')) --e; return s.substr(b, e - b); } static bool IsMonthCode(char c) { return std::strchr("FGHJKMNQUVXZ", c) != nullptr && c != '\0'; } // "ESZ25" -> "ES", "MNQH6" -> "MNQ"; anything not shaped like a futures contract unchanged. static std::string StripContractCode(const std::string& s) { size_t n = s.size(), d = 0; while (d < n && d < 4 && s[n - 1 - d] >= '0' && s[n - 1 - d] <= '9') ++d; if (d == 0 || d > 4 || n < d + 2) return s; char month = s[n - 1 - d]; if (!IsMonthCode(month)) return s; return s.substr(0, n - 1 - d); } // The API symbol for a chart. A non-blank override wins. Otherwise the chart symbol loses // its exchange suffix and contract code: "ESZ25-CME" -> ES, "MESZ5.CME" -> ES, // "NQH26_FUT_CME" -> NQ, "ES#" -> ES, "F.US.EPZ25" (CQG) -> ES, "$SPX" -> SPX, "AAPL-NQTV" -> AAPL. static std::string ResolveSymbol(const std::string& overrideSymbol, const std::string& chartSymbol) { std::string o = Upper(Trim(overrideSymbol)); while (!o.empty() && (o[0] == '$' || o[0] == '^')) o.erase(0, 1); if (o == "MES") return "ES"; // the API answers MES and MNQ as ES and NQ if (o == "MNQ") return "NQ"; if (!o.empty()) return o; std::string s = Upper(Trim(chartSymbol)); if (s.compare(0, 5, "F.US.") == 0) s = s.substr(5); // CQG futures names size_t cut = s.find_first_of("-_. #"); if (cut != std::string::npos) s = s.substr(0, cut); while (!s.empty() && (s[0] == '$' || s[0] == '^' || s[0] == '/')) s.erase(0, 1); s = StripContractCode(s); if (s == "ES" || s == "MES" || s == "EP") return "ES"; if (s == "NQ" || s == "MNQ" || s == "ENQ") return "NQ"; return s; } // ---------------------------------------------------------------------------- dated contracts // ES and NQ answer for the front quarterly. A dated symbol (ESH27) asks for that contract's own // basis, which the API measures in the two weeks before a roll; outside that window it answers // "contract_not_live" and the study falls back to the front view for kNotLiveCacheSeconds. static const int kNotLiveCacheSeconds = 600; static const char* const kMonthCodes = "FGHJKMNQUVXZ"; static const char* const kMonthNames[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; // ("MES", 3, 2027) -> "ESH27"; micro roots and CQG names read their full-size view (MES and // EP -> ES, MNQ and ENQ -> NQ). Empty for any other root or a month outside 1..12. static std::string DatedSymbol(const std::string& root, int month, int year) { std::string r = Upper(Trim(root)); if (r == "MES" || r == "EP") r = "ES"; if (r == "MNQ" || r == "ENQ") r = "NQ"; if (r != "ES" && r != "NQ") return ""; if (month < 1 || month > 12 || year < 0) return ""; int yy = year % 100; std::string out = r; out += kMonthCodes[month - 1]; out += (char)('0' + yy / 10); out += (char)('0' + yy % 10); return out; } // The dated contract of a Sierra chart symbol, always with a two-digit year: // "ESH27-CME" -> ESH27, "MESZ5.CME" -> ESZ25, "NQZ26_FUT_CME" -> NQZ26, "F.US.EPH27" -> ESH27. // Empty for continuous charts ("ES_FUT_CME", "ES#"), SPX, stocks. A one-digit year is read as // 202x, the decade Sierra's one-digit symbols name today. static std::string ChartContractCode(const std::string& chartSymbol) { std::string s = Upper(Trim(chartSymbol)); if (s.compare(0, 5, "F.US.") == 0) s = s.substr(5); size_t cut = s.find_first_of("-_. #"); if (cut != std::string::npos) s = s.substr(0, cut); while (!s.empty() && (s[0] == '$' || s[0] == '^' || s[0] == '/')) s.erase(0, 1); size_t n = s.size(), d = 0; while (d < n && s[n - 1 - d] >= '0' && s[n - 1 - d] <= '9') ++d; if (d < 1 || d > 2 || n < d + 3) return ""; char month = s[n - 1 - d]; if (!IsMonthCode(month)) return ""; int yd = std::atoi(s.c_str() + (n - d)); int year = d == 1 ? 2020 + yd : 2000 + yd; return DatedSymbol(s.substr(0, n - 1 - d), (int)(std::strchr(kMonthCodes, month) - kMonthCodes) + 1, year); } // True for a dated ES / NQ symbol as the API reads it: ESH27, MNQZ26, ESH7. static bool IsDated(const std::string& sym) { std::string s = Upper(Trim(sym)); if (!s.empty() && s[0] == 'M') s.erase(0, 1); if (s.size() < 4 || s.size() > 5) return false; if (s.compare(0, 2, "ES") != 0 && s.compare(0, 2, "NQ") != 0) return false; if (!IsMonthCode(s[2])) return false; for (size_t i = 3; i < s.size(); ++i) if (s[i] < '0' || s[i] > '9') return false; return true; } // The front view a dated symbol falls back to: "ESH27" -> ES, "MNQZ26" -> NQ; others unchanged. static std::string FrontSymbol(const std::string& sym) { if (!IsDated(sym)) return sym; std::string s = Upper(Trim(sym)); if (s[0] == 'M') s.erase(0, 1); return s.substr(0, 2); } // The symbol to ask the API for: an override when set, else the chart's dated contract // (ESH27) when it has one, else the plain view (ES, NQ, SPX, QQQ...). static std::string RequestSymbol(const std::string& overrideSymbol, const std::string& chartSymbol) { std::string plain = ResolveSymbol(overrideSymbol, chartSymbol); if (!Trim(overrideSymbol).empty()) return plain; std::string dated = ChartContractCode(chartSymbol); if (!dated.empty() && dated.compare(0, 2, plain) == 0) return dated; return plain; } // The symbol a poll asks for: the dated one, unless the API said "contract_not_live" for it // less than kNotLiveCacheSeconds ago (nowS < notLiveUntilS), then the front view. static std::string EffectiveSymbol(const std::string& sym, double nowS, double notLiveUntilS) { return IsDated(sym) && nowS < notLiveUntilS ? FrontSymbol(sym) : sym; } // True when an answer to a levels request for a dated symbol says to fall back to the front. static bool ShouldFallBack(const std::string& askedSym, const Answer& a) { return a.kind == Answer::Failed && (a.code == "contract_not_live" || a.code == "unknown_symbol") && IsDated(askedSym); } // Days since 1970-01-01 of a civil date, and back (Howard Hinnant's algorithms). static long long DaysFromCivil(int y, int m, int d) { y -= m <= 2; const long long era = (y >= 0 ? y : y - 399) / 400; const unsigned yoe = (unsigned)(y - era * 400); const unsigned doy = (153 * (unsigned)(m + (m > 2 ? -3 : 9)) + 2) / 5 + (unsigned)d - 1; const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; return era * 146097 + (long long)doe - 719468; } static void CivilFromDays(long long z, int& y, int& m, int& d) { z += 719468; const long long era = (z >= 0 ? z : z - 146096) / 146097; const unsigned doe = (unsigned)(z - era * 146097); const unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); const unsigned mp = (5 * doy + 2) / 153; d = (int)(doy - (153 * mp + 2) / 5 + 1); m = (int)(mp < 10 ? mp + 3 : mp - 9); y = (int)(yoe + era * 400) + (m <= 2); } // First day the API measures a contract: 22 days before its expiry, the third Friday of the // contract month. "ESZ26" -> the day number of 2026-11-26. -1 when not a dated symbol. static long long RollWindowStart(const std::string& contract) { if (!IsDated(contract)) return -1; std::string s = Upper(Trim(contract)); if (s[0] == 'M') s.erase(0, 1); int month = (int)(std::strchr(kMonthCodes, s[2]) - kMonthCodes) + 1; int yd = std::atoi(s.c_str() + 3); int year = s.size() == 4 ? 2020 + yd : 2000 + yd; long long first = DaysFromCivil(year, month, 1); int dow = (int)(((first % 7) + 7 + 4) % 7); // 1970-01-01 was a Thursday; 0 = Sunday int toFriday = (5 - dow + 7) % 7; return first + toFriday + 14 - 22; } static std::string Unmicro(const std::string& c) { std::string s = Upper(Trim(c)); if (IsDated(s) && s[0] == 'M') s.erase(0, 1); return s; } // The warning under the status line when the drawn levels are not for the chart's contract, // which only happens after a "contract_not_live" fallback: "levels are for ESZ26; ESH27 is // measured from Feb 25" before its roll window opens, else "levels are for ESZ26; ESH27 is // not measured today". Empty when they match or the chart is not on a dated contract. // `todayDays` is today's UTC date as days since 1970-01-01. static std::string ContractWarning(const std::string& chartContract, const std::string& levelContract, long long todayDays) { if (chartContract.empty() || levelContract.empty()) return ""; std::string c = Unmicro(chartContract), l = Unmicro(levelContract); if (c == l) return ""; long long start = RollWindowStart(c); if (start >= 0 && todayDays < start) { int y, m, d; CivilFromDays(start, y, m, d); return "levels are for " + l + "; " + c + " is measured from " + kMonthNames[m - 1] + " " + std::to_string(d); } return "levels are for " + l + "; " + c + " is not measured today"; } static std::string UrlEncode(const std::string& s) { static const char* hex = "0123456789ABCDEF"; std::string o; for (unsigned char c : s) { if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') o += (char)c; else { o += '%'; o += hex[c >> 4]; o += hex[c & 15]; } } return o; } static std::string LevelsUrl(const std::string& sym, const std::string& key) { return std::string(kBaseUrl) + "/api/v1/levels?sym=" + UrlEncode(sym) + "&key=" + UrlEncode(key); } static std::string GammaUrl(const std::string& sym, bool oiBook, const std::string& key) { return std::string(kBaseUrl) + "/api/v1/gamma?sym=" + UrlEncode(sym) + "&book=" + (oiBook ? "oi" : "vol") + "&key=" + UrlEncode(key); } // ---------------------------------------------------------------------------- timing static int ClampPoll(int pollSeconds) { return pollSeconds < kMinPollSeconds ? kMinPollSeconds : pollSeconds; } // The rate limit (120 a minute per key) counts every endpoint, so the profile rides slower // than the levels: a second-cadence book at three polls but never under 15 s, a five-minute // book once a minute. static int GammaIntervalSeconds(const std::string& cadenceBook, int pollSeconds) { int poll = ClampPoll(pollSeconds); if (cadenceBook == "1s") return 3 * poll > 15 ? 3 * poll : 15; return poll > 60 ? poll : 60; } // Delay before the next request: the server's retry_after_s wins; fatal errors wait a // minute; other failures double from the poll interval up to two minutes. static int NextDelaySeconds(int pollSeconds, int consecutiveFailures, int retryAfterS, bool fatal) { int poll = ClampPoll(pollSeconds); if (retryAfterS > 0) return retryAfterS > poll ? retryAfterS : poll; if (fatal) return kFatalRetrySeconds > poll ? kFatalRetrySeconds : poll; if (consecutiveFailures <= 0) return poll; int f = consecutiveFailures > 10 ? 10 : consecutiveFailures; long long d = (long long)poll << f; if (d > kMaxBackoffSeconds) d = kMaxBackoffSeconds; return d < poll ? poll : (int)d; } // STALE: the book is older than it should be for a running session (past three polls for a // second-cadence book, past 15 minutes for a five-minute one), or the server says "stale". // A closed session is not stale; it is the last close. static bool IsStale(const Level& L, double ageS, int pollSeconds) { if (L.session == "stale") return true; if (L.session != "open") return false; if (L.cadenceBook == "1s") return ageS > 3.0 * ClampPoll(pollSeconds); return ageS > 900.0; } // ---------------------------------------------------------------------------- formatting // Hand-rolled so the decimal point never follows the Windows locale. // "7,762.00", "-3.25". static std::string FormatPrice(double v) { if (!std::isfinite(v)) return "?"; bool neg = v < 0; long long cents = (long long)std::llround(std::fabs(v) * 100.0); long long whole = cents / 100; int frac = (int)(cents % 100); std::string w = std::to_string(whole); std::string g; int count = 0; for (size_t i = w.size(); i > 0; --i) { g.insert(g.begin(), w[i - 1]); if (++count % 3 == 0 && i > 1) g.insert(g.begin(), ','); } std::string out = (neg && cents != 0) ? "-" : ""; out += g; out += '.'; out += (char)('0' + frac / 10); out += (char)('0' + frac % 10); return out; } // "+58.75", "-3.25", "0.00". static std::string FormatSigned(double v) { std::string s = FormatPrice(std::fabs(v)); if (std::llround(v * 100.0) > 0) return "+" + s; if (std::llround(v * 100.0) < 0) return "-" + s; return s; } // "12 s old", "4 min old", "3 h old", "2 d old". static std::string FormatAge(double ageS) { if (!std::isfinite(ageS) || ageS < 0) return ""; if (ageS < 90) return std::to_string(std::llround(ageS)) + " s old"; if (ageS < 90 * 60) return std::to_string(std::llround(ageS / 60)) + " min old"; if (ageS < 36 * 3600) return std::to_string(std::llround(ageS / 3600)) + " h old"; return std::to_string(std::llround(ageS / 86400)) + " d old"; } // The status line. With a reading: // "FirmTape ES (ESZ26) volume book, basis +58.75, 12 s old" // plus ", closed, last session", ", STALE" or a passing error. Without one, or after a // fatal error: "FirmTape ES: LIVE plan required", "FirmTape ES: connecting". static std::string StatusLine(const std::string& sym, const Level* L, double ageS, bool stale, const std::string& error, bool errorFatal) { std::string s = "FirmTape " + (sym.empty() ? std::string("?") : sym); if (L && !L->contract.empty()) s += " (" + L->contract + ")"; if (!error.empty() && (errorFatal || !L)) return s + ": " + error; if (!L) return s + ": connecting"; s += " volume book"; if (L->basis.has) s += ", basis " + FormatSigned(L->basis.v); if (L->session == "closed") s += ", closed, last session"; std::string age = FormatAge(ageS); if (!age.empty()) s += ", " + age; if (stale) s += ", STALE"; if (!error.empty()) s += ", " + error; return s; } static std::string LevelLabel(const char* what, double value, bool oi) { return std::string("FT ") + (oi ? "OI " : "") + what + " " + FormatPrice(value); } // ---------------------------------------------------------------------------- the study's state typedef std::chrono::steady_clock Clock; static double Seconds(Clock::duration d) { return std::chrono::duration_cast>(d).count(); } // Everything the study keeps between calls. Lives on the heap behind // sc.GetPersistentPointer(1); freed on sc.LastCallToFunction. struct State { enum Phase { Idle, LevelsPending, GammaPending }; Phase phase = Idle; Clock::time_point sentAt; Clock::time_point nextLevelsAt; Clock::time_point nextGammaAt; // What the chart draws. bool haveLevel = false; Level level; Clock::time_point levelReceivedAt; bool haveGamma = false; Gamma gamma; std::string error; // status line error, empty when the last request went through bool errorFatal = false; int failures = 0; // What the reading was asked for; a change of any of these starts over. std::string sym, key; // The symbol the last levels request asked for: sym, or its front view (ES) while a // "contract_not_live" answer for the dated sym (ESH27) is cached until notLiveUntil. std::string askedSym; Clock::time_point notLiveUntil; int profileOi = -1; std::string pendingTag; // Tag() at the time the pending request was sent std::string Tag() const { return sym + "|" + key + "|" + std::to_string(profileOi); } unsigned version = 1; // bumped whenever the drawn levels change unsigned drawnVersion = 0; std::string drawnStatus; std::string loggedError; // Book age now: the server's age_s plus the time since that answer, so a skewed local // clock does not matter. double AgeSeconds(Clock::time_point now) const { if (!haveLevel) return NAN; double since = Seconds(now - levelReceivedAt); return level.ageS + (since > 0 ? since : 0); } void Reset(Clock::time_point now) { haveLevel = false; level = Level(); haveGamma = false; gamma = Gamma(); error.clear(); errorFatal = false; failures = 0; nextLevelsAt = now; nextGammaAt = now; askedSym.clear(); notLiveUntil = Clock::time_point(); ++version; } }; // Apply one answer to the state. `forGamma` says which request it answers. // Returns false when the answer was not ours (Foreign) and the request is still pending. static bool ApplyAnswer(State& st, const Answer& a, bool forGamma, int pollSeconds, Clock::time_point now) { if (a.kind == Answer::Foreign) return false; st.phase = State::Idle; if (a.kind == Answer::Ok) { st.failures = 0; st.error.clear(); st.errorFatal = false; if (forGamma) { st.gamma = a.gamma; st.haveGamma = true; st.nextGammaAt = now + std::chrono::seconds(GammaIntervalSeconds(st.level.cadenceBook, pollSeconds)); } else { st.level = a.level; st.haveLevel = true; st.levelReceivedAt = now; st.nextLevelsAt = now + std::chrono::seconds(ClampPoll(pollSeconds)); } ++st.version; return true; } // Failed. ++st.failures; st.error = a.text; st.errorFatal = a.fatal; if (a.fatal) { // A key or plan error clears the drawing: those levels are no longer this account's. st.haveLevel = false; st.haveGamma = false; st.level = Level(); st.gamma = Gamma(); ++st.version; } int delay = NextDelaySeconds(pollSeconds, st.failures, a.retryAfterS, a.fatal); st.nextLevelsAt = now + std::chrono::seconds(delay); if (forGamma || a.fatal || a.retryAfterS > 0) st.nextGammaAt = now + std::chrono::seconds(delay); return true; } } // namespace firmtape // ========================================================================================= // 2. THE STUDY // ========================================================================================= #ifndef FIRMTAPE_PURE_ONLY SCDLLName("FirmTape Levels") namespace { using namespace firmtape; enum InputIndex { IN_API_KEY = 0, IN_SYMBOL, IN_POLL_SECONDS, IN_SHOW_OI, IN_SHOW_PROFILE, IN_PROFILE_BOOK, IN_PROFILE_WIDTH, IN_FLIP_COLOR, IN_CALL_COLOR, IN_PUT_COLOR, IN_STATUS_COLOR, IN_LINE_WIDTH, IN_LABEL_FONT_SIZE, }; // Drawing slots, one LineNumber each: base + slot. enum Slot { SLOT_FLIP = 0, SLOT_CALL, SLOT_PUT, SLOT_OI_FLIP, SLOT_OI_CALL, SLOT_OI_PUT, SLOT_STATUS, SLOT_WARNING, SLOT_COUNT }; // LineNumbers unique to this study instance on this chart, clear of hand-drawn tools. int LineNumberFor(SCStudyInterfaceRef sc, int slot) { return 740000000 + (sc.StudyGraphInstanceID % 100000) * 16 + slot; } State* GetState(SCStudyInterfaceRef sc) { return static_cast(sc.GetPersistentPointer(1)); } void DeleteSlot(SCStudyInterfaceRef sc, int slot) { sc.DeleteACSChartDrawing(sc.ChartNumber, TOOL_DELETE_CHARTDRAWING, LineNumberFor(sc, slot)); } void DeleteAllSlots(SCStudyInterfaceRef sc) { for (int s = 0; s < SLOT_COUNT; ++s) DeleteSlot(sc, s); } // A full-width horizontal line with its label at the right edge. void DrawLevelLine(SCStudyInterfaceRef sc, int slot, const Opt& value, const char* what, bool oi, COLORREF color, int width) { if (!value.has) { DeleteSlot(sc, slot); return; } s_UseTool Tool; Tool.Clear(); Tool.ChartNumber = sc.ChartNumber; Tool.DrawingType = DRAWING_HORIZONTALLINE; Tool.LineNumber = LineNumberFor(sc, slot); Tool.AddMethod = UTAM_ADD_OR_ADJUST; Tool.AddAsUserDrawnDrawing = 0; Tool.Region = sc.GraphRegion; Tool.BeginValue = (float)value.v; Tool.Color = color; Tool.LineWidth = oi ? (width > 1 ? width - 1 : 1) : width; Tool.LineStyle = oi ? LINESTYLE_DASH : LINESTYLE_SOLID; Tool.DisplayHorizontalLineValue = 0; Tool.Text = LevelLabel(what, value.v, oi).c_str(); Tool.TextAlignment = DT_RIGHT | DT_BOTTOM; int fs = sc.Input[IN_LABEL_FONT_SIZE].GetInt(); if (fs > 0) Tool.FontSize = fs; sc.UseTool(Tool); } // The status line, pinned to the top-left of the price region. void DrawStatus(SCStudyInterfaceRef sc, const std::string& text, COLORREF color, int slot = SLOT_STATUS, float fromBottom = 98) { s_UseTool Tool; Tool.Clear(); Tool.ChartNumber = sc.ChartNumber; Tool.DrawingType = DRAWING_STATIONARY_TEXT; Tool.LineNumber = LineNumberFor(sc, slot); Tool.AddMethod = UTAM_ADD_OR_ADJUST; Tool.AddAsUserDrawnDrawing = 0; Tool.Region = sc.GraphRegion; Tool.UseRelativeVerticalValues = 1; Tool.BeginDateTime = 1; // percent of the region width from the left Tool.BeginValue = fromBottom; // percent of the region height from the bottom Tool.Color = color; Tool.FontBold = 1; int fs = sc.Input[IN_LABEL_FONT_SIZE].GetInt(); if (fs > 0) Tool.FontSize = fs; Tool.Text = text.c_str(); sc.UseTool(Tool); } // Gamma by strike as horizontal bars growing leftward from the right edge of the price // region. Positive gamma in the call colour, negative in the put colour; bar length is // |gamma| over the largest |gamma| among the strikes in view. void FirmTapeDrawProfile(HWND WindowHandle, HDC DeviceContext, SCStudyInterfaceRef sc) { (void)WindowHandle; State* st = GetState(sc); if (!st || !st->haveGamma || !sc.Input[IN_SHOW_PROFILE].GetYesNo()) return; const std::vector& K = st->gamma.strikes; const std::vector& G = st->gamma.gamma; if (K.empty() || K.size() != G.size()) return; const int top = sc.StudyRegionTopCoordinate; const int bottom = sc.StudyRegionBottomCoordinate; const int left = sc.StudyRegionLeftCoordinate; const int right = sc.StudyRegionRightCoordinate; if (bottom - top < 10 || right - left < 20) return; int pct = sc.Input[IN_PROFILE_WIDTH].GetInt(); if (pct < 1) pct = 1; if (pct > 60) pct = 60; const int maxLen = (right - left) * pct / 100; if (maxLen < 2) return; // Pixel rows of the strikes, and the largest |gamma| among those in view. std::vector y(K.size()); double maxAbs = 0.0; for (size_t i = 0; i < K.size(); ++i) { y[i] = sc.RegionValueToYPixelCoordinate((float)K[i], sc.GraphRegion); if (y[i] >= top && y[i] <= bottom && std::fabs(G[i]) > maxAbs) maxAbs = std::fabs(G[i]); } if (maxAbs <= 0.0) return; // Bar thickness: most of the smallest gap between neighbouring strikes on screen. int gap = 1 << 30; for (size_t i = 0; i < K.size(); ++i) for (size_t j = i + 1; j < K.size(); ++j) { int d = std::abs(y[i] - y[j]); if (d > 0 && d < gap) gap = d; } int half = gap == (1 << 30) ? 2 : (gap * 4 / 10); if (half < 1) half = 1; if (half > 12) half = 12; HBRUSH pos = CreateSolidBrush(sc.Input[IN_CALL_COLOR].GetColor()); HBRUSH neg = CreateSolidBrush(sc.Input[IN_PUT_COLOR].GetColor()); for (size_t i = 0; i < K.size(); ++i) { if (y[i] < top || y[i] > bottom || G[i] == 0.0) continue; int len = (int)std::lround(std::fabs(G[i]) / maxAbs * maxLen); if (len < 1) len = 1; RECT r; r.left = right - len; r.right = right; r.top = y[i] - half; r.bottom = y[i] + half + 1; if (r.top < top) r.top = top; if (r.bottom > bottom) r.bottom = bottom; FillRect(DeviceContext, &r, G[i] > 0 ? pos : neg); } DeleteObject(pos); DeleteObject(neg); } void SetDefaults(SCStudyInterfaceRef sc) { sc.GraphName = "FirmTape Levels"; sc.StudyDescription = "FirmTape dealer-gamma levels from the FirmTape levels API (firmtape.com/api/v1): " "zero-gamma flip, call resistance, put support, optional open-interest levels and a gamma-by-strike profile. " "Needs an API token from firmtape.com/account on the LIVE plan. Draws only; no trading."; sc.AutoLoop = 0; sc.GraphRegion = 0; sc.UpdateAlways = 1; // called on every chart update, so the poll timer runs without new bars sc.FreeDLL = 0; sc.Input[IN_API_KEY].Name = "API key (firmtape.com/account, API tokens)"; sc.Input[IN_API_KEY].SetString(""); sc.Input[IN_SYMBOL].Name = "Symbol (blank = from the chart: ES, NQ, SPX, QQQ...)"; sc.Input[IN_SYMBOL].SetString(""); sc.Input[IN_POLL_SECONDS].Name = "Poll every (seconds, min 3)"; sc.Input[IN_POLL_SECONDS].SetInt(5); sc.Input[IN_POLL_SECONDS].SetIntLimits(kMinPollSeconds, 300); sc.Input[IN_SHOW_OI].Name = "Show open-interest levels"; sc.Input[IN_SHOW_OI].SetYesNo(0); sc.Input[IN_SHOW_PROFILE].Name = "Show gamma profile"; sc.Input[IN_SHOW_PROFILE].SetYesNo(1); sc.Input[IN_PROFILE_BOOK].Name = "Gamma profile book"; sc.Input[IN_PROFILE_BOOK].SetCustomInputStrings("Volume;Open interest"); sc.Input[IN_PROFILE_BOOK].SetCustomInputIndex(0); sc.Input[IN_PROFILE_WIDTH].Name = "Gamma profile width (% of chart)"; sc.Input[IN_PROFILE_WIDTH].SetInt(15); sc.Input[IN_PROFILE_WIDTH].SetIntLimits(1, 60); sc.Input[IN_FLIP_COLOR].Name = "Flip colour"; sc.Input[IN_FLIP_COLOR].SetColor(RGB(255, 176, 0)); sc.Input[IN_CALL_COLOR].Name = "Call resistance colour (and positive gamma)"; sc.Input[IN_CALL_COLOR].SetColor(RGB(0, 190, 90)); sc.Input[IN_PUT_COLOR].Name = "Put support colour (and negative gamma)"; sc.Input[IN_PUT_COLOR].SetColor(RGB(230, 60, 60)); sc.Input[IN_STATUS_COLOR].Name = "Status text colour"; sc.Input[IN_STATUS_COLOR].SetColor(RGB(150, 150, 150)); sc.Input[IN_LINE_WIDTH].Name = "Line width"; sc.Input[IN_LINE_WIDTH].SetInt(2); sc.Input[IN_LINE_WIDTH].SetIntLimits(1, 6); sc.Input[IN_LABEL_FONT_SIZE].Name = "Label font size (0 = chart default)"; sc.Input[IN_LABEL_FONT_SIZE].SetInt(0); sc.Input[IN_LABEL_FONT_SIZE].SetIntLimits(0, 30); } } // namespace SCSFExport scsf_FirmTapeLevels(SCStudyInterfaceRef sc) { if (sc.SetDefaults) { SetDefaults(sc); return; } State* st = GetState(sc); if (sc.LastCallToFunction) { sc.p_GDIFunction = nullptr; DeleteAllSlots(sc); if (st) { delete st; sc.SetPersistentPointer(1, nullptr); } return; } if (!st) { st = new State(); sc.SetPersistentPointer(1, st); } // Set on every call: a rebuilt DLL loads at a new address. sc.p_GDIFunction = FirmTapeDrawProfile; const Clock::time_point now = Clock::now(); const int poll = ClampPoll(sc.Input[IN_POLL_SECONDS].GetInt()); const std::string key = Trim(sc.Input[IN_API_KEY].GetString()); // A dated chart (ESH27-CME) asks for its own contract (ESH27); continuous charts ask ES. const std::string sym = RequestSymbol(sc.Input[IN_SYMBOL].GetString(), sc.Symbol.GetChars()); const std::string chartContract = ChartContractCode(sc.Symbol.GetChars()); const bool showOi = sc.Input[IN_SHOW_OI].GetYesNo() != 0; const bool showProfile = sc.Input[IN_SHOW_PROFILE].GetYesNo() != 0; const int profileOi = sc.Input[IN_PROFILE_BOOK].GetIndex() == 1 ? 1 : 0; #if FIRMTAPE_REQUIRE_SC_ALLOW_LIST // TODO(distribution): the compiled DLL we ship is gated by Sierra Chart's per-account // allow list for the DLL name above (SCDLLName). Sierra sets sc.IsUserAllowedForSCDLLName // when this Sierra Chart account is on the list we maintain on sierrachart.com. The API // key still gates the data; this only gates the compiled code. See sierra/distribution.md. if (!sc.IsUserAllowedForSCDLLName) { DeleteAllSlots(sc); DrawStatus(sc, "FirmTape: this Sierra Chart account is not authorized for the FirmTape study (see firmtape.com/account)", sc.Input[IN_PUT_COLOR].GetColor()); return; } #endif // A new key, symbol or profile book starts over. // A request already in flight still answers; its answer is read and dropped below. if (key != st->key || sym != st->sym || profileOi != st->profileOi) { st->key = key; st->sym = sym; st->profileOi = profileOi; st->Reset(now); } // ------------------------------------------------ an answer to the pending request if (st->phase != State::Idle) { const char* raw = sc.HTTPResponse.GetChars(); if (raw && raw[0]) { const bool forGamma = st->phase == State::GammaPending; const std::string body(raw); Answer a = forGamma ? InterpretGamma(body) : InterpretLevels(body); // An answer to a request for a symbol, key or book we no longer show is dropped. if (st->pendingTag != st->Tag()) { if (a.kind != Answer::Foreign) st->phase = State::Idle; } else if (!forGamma && ShouldFallBack(st->askedSym, a)) { // The chart's contract is not measured today: ask the front view at once, and // keep asking it for the next ten minutes instead of spending two requests a poll. st->phase = State::Idle; st->notLiveUntil = now + std::chrono::seconds(kNotLiveCacheSeconds); st->nextLevelsAt = now; } else ApplyAnswer(*st, a, forGamma, poll, now); } else if (Seconds(now - st->sentAt) > kRequestTimeoutSeconds) { Answer a = Fail("timeout", 0); ApplyAnswer(*st, a, st->phase == State::GammaPending, poll, now); } } // ------------------------------------------------ the next request if (st->phase == State::Idle) { if (key.empty()) { if (st->error != DescribeError("no_key")) { st->Reset(now); st->error = DescribeError("no_key"); st->errorFatal = true; } } else if (sym.empty()) { st->error = DescribeError("unknown_symbol"); st->errorFatal = true; } else if (now >= st->nextLevelsAt) { const std::string ask = EffectiveSymbol(sym, Seconds(now.time_since_epoch()), Seconds(st->notLiveUntil.time_since_epoch())); if (ask != st->askedSym) st->nextGammaAt = now; // the profile follows the contract asked st->askedSym = ask; if (sc.MakeHTTPRequest(LevelsUrl(ask, key).c_str())) { st->phase = State::LevelsPending; st->sentAt = now; st->pendingTag = st->Tag(); } else st->nextLevelsAt = now + std::chrono::seconds(1); // the chart's one request slot is busy } else if (showProfile && st->haveLevel && !st->errorFatal && now >= st->nextGammaAt) { if (sc.MakeHTTPRequest(GammaUrl(st->askedSym.empty() ? sym : st->askedSym, profileOi == 1, key).c_str())) { st->phase = State::GammaPending; st->sentAt = now; st->pendingTag = st->Tag(); } else st->nextGammaAt = now + std::chrono::seconds(1); } } // ------------------------------------------------ log a new error once if (st->error != st->loggedError) { if (!st->error.empty()) { SCString msg; msg.Format("FirmTape Levels (%s): %s", sym.c_str(), st->error.c_str()); sc.AddMessageToLog(msg, 0); } st->loggedError = st->error; } // ------------------------------------------------ draw const COLORREF flipC = sc.Input[IN_FLIP_COLOR].GetColor(); const COLORREF callC = sc.Input[IN_CALL_COLOR].GetColor(); const COLORREF putC = sc.Input[IN_PUT_COLOR].GetColor(); const int width = sc.Input[IN_LINE_WIDTH].GetInt(); // Lines: redrawn when the reading changes, on a full recalculation (which may have // cleared the chart's study drawings), and when an input changed the look. const unsigned look = (unsigned)(showOi ? 1 : 0) * 7919u + (unsigned)width * 104729u + (unsigned)flipC * 3u + (unsigned)callC * 5u + (unsigned)putC * 11u + (unsigned)sc.Input[IN_LABEL_FONT_SIZE].GetInt() * 13u; const unsigned want = st->version * 2654435761u ^ look; if (want != st->drawnVersion || sc.IsFullRecalculation) { if (st->haveLevel) { const Level& L = st->level; DrawLevelLine(sc, SLOT_FLIP, L.flip, "flip", false, flipC, width); DrawLevelLine(sc, SLOT_CALL, L.callWall, "call res", false, callC, width); DrawLevelLine(sc, SLOT_PUT, L.putWall, "put sup", false, putC, width); const Opt none; DrawLevelLine(sc, SLOT_OI_FLIP, showOi && L.hasOi ? L.oiFlip : none, "flip", true, flipC, width); DrawLevelLine(sc, SLOT_OI_CALL, showOi && L.hasOi ? L.oiCallWall : none, "call res", true, callC, width); DrawLevelLine(sc, SLOT_OI_PUT, showOi && L.hasOi ? L.oiPutWall : none, "put sup", true, putC, width); } else { for (int s = SLOT_FLIP; s <= SLOT_OI_PUT; ++s) DeleteSlot(sc, s); } st->drawnVersion = want; st->drawnStatus.clear(); // force the status line too } // Status: redrawn whenever its text changes (the age moves every second). const double age = st->AgeSeconds(now); const bool stale = st->haveLevel && IsStale(st->level, age, poll); const std::string status = StatusLine(sym, st->haveLevel ? &st->level : nullptr, age, stale, st->error, st->errorFatal); if (status != st->drawnStatus) { const bool alarm = stale || (!st->error.empty() && (st->errorFatal || !st->haveLevel)); DrawStatus(sc, status, alarm ? putC : sc.Input[IN_STATUS_COLOR].GetColor()); st->drawnStatus = status; // Second line, only when the chart's contract is not measured today and the levels // fell back to the front one: "levels are for ESZ26; ESH27 is measured from Feb 25". const std::string warning = st->haveLevel ? ContractWarning(chartContract, st->level.contract, (long long)(std::time(nullptr) / 86400)) : std::string(); if (warning.empty()) DeleteSlot(sc, SLOT_WARNING); else DrawStatus(sc, warning, putC, SLOT_WARNING, 94); } } #endif // FIRMTAPE_PURE_ONLY