Skip to content

Commit 95310a0

Browse files
mccaffersCopilot
andauthored
10 create end to end trade management logic (#16)
* Added QuestDB tick streaming * Refactoring tick flow * Looping around tick data from multiple symbols * Update source/CMakeLists.txt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactoring * fix run script, shell scopping behaviour was putting the build script in it's own child process * Update build os to target macos-latest * Update build os to target macos-latest * Updating github workflow os * Refactoring build pipeline * fixing workflow * Updating github workflow os * Updating github workflow os * Updating github workflow os * Updating github workflow os * Updating main flow, extracting the strategy to query questdB * Updating trade management operations * Refactoring operations flow * updated workflows and optimised build & test * updated workflow references * updated workflow references * updated workflow references * updated workflow references * update readme with new workflow badges * ignoring cpp:S2245 as it's just for testing * Refactoring to add in pip scalling * updated build to reference boost-decimal * refactoring to use decimal * Fixing test dependencies --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3053960 commit 95310a0

19 files changed

Lines changed: 374 additions & 155 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
-derivedDataPath "${RUNNER_TEMP}/Build/DerivedData"
4848
-parallelizeTargets
4949
-jobs "$(sysctl -n hw.logicalcpu)"
50-
HEADER_SEARCH_PATHS="./external/libpqxx/include/pqxx/internal ./external/libpqxx/include/ ./external/libpqxx/build/include/ ./external/"
50+
HEADER_SEARCH_PATHS="./external/libpqxx/include/pqxx/internal ./external/libpqxx/include/ ./external/libpqxx/build/include/ ./external/boost-decimal/include/ ./external/boost-decimal/include/decimal/ ./external/"
5151
LIBRARY_SEARCH_PATHS="./external/libpqxx/src/ ./external/libpqxx/build/src/"
5252
OTHER_LDFLAGS="-L./external/libpqxx/build/src -lpqxx -lpq -L$(brew --prefix pkgconf)/lib -L$(brew --prefix pkgconf)/lib/pkgconfig -L$(brew --prefix postgresql@18)/lib/postgresql -L$(brew --prefix postgresql@18)/lib/postgresql/pgxs -L$(brew --prefix postgresql@18)/lib/postgresql/pkgconfig"
5353
clean build test

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "external/libpqxx"]
22
path = external/libpqxx
33
url = https://github.com/jtv/libpqxx.git
4+
[submodule "external/boost-decimal"]
5+
path = external/boost-decimal
6+
url = https://github.com/boostorg/decimal

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,13 @@ if(APPLE)
5757
set(OpenMP_C_LIB_NAMES "omp")
5858
set(OpenMP_CXX_LIB_NAMES "omp")
5959
set(OpenMP_omp_LIBRARY /opt/homebrew/opt/libomp/lib/libomp.dylib)
60+
find_package(OpenMP REQUIRED)
6061
target_include_directories(BacktestingEngineLib PRIVATE /opt/homebrew/opt/libomp/include)
6162
endif()
6263

64+
add_subdirectory(external/boost-decimal)
65+
target_link_libraries(BacktestingEngineLib PUBLIC Boost::decimal)
66+
6367
find_package(OpenMP REQUIRED)
6468
target_link_libraries(BacktestingEngineLib PUBLIC pqxx OpenMP::OpenMP_CXX)
6569

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ I'm developing a high-performance C++ backtesting engine designed to analyze fin
1212

1313
I'm extracting results and creating various graphs for trend analyses using SciPy for calculations and Plotly for visualization.
1414

15-
![alt text](images/random-indices-sp500-variable.svg)
15+
![alt text](documents/images/random-indices-sp500-variable.svg)
1616

1717
*Read more results on https://mccaffers.com/randomly_trading/*
1818

backtesting-engine-cpp.xcodeproj/project.pbxproj

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
943398272D57E54000287A2D /* jsonParser.mm in Sources */ = {isa = PBXBuildFile; fileRef = 943398262D57E54000287A2D /* jsonParser.mm */; };
2323
94364CB62D416D8D00F35B55 /* db.mm in Sources */ = {isa = PBXBuildFile; fileRef = 94364CB52D416D8000F35B55 /* db.mm */; };
2424
944698852D3A545B0070E30F /* libpq.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8A972D2D34A100041BBA /* libpq.a */; };
25+
9464E5F12FA7467200D82BAD /* symbolScale.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9464E5F02FA7467200D82BAD /* symbolScale.mm */; };
2526
94674B872D533B4000973137 /* tradeManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94674B852D533B4000973137 /* tradeManager.cpp */; };
2627
94674B882D533B4000973137 /* tradeManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94674B852D533B4000973137 /* tradeManager.cpp */; };
2728
94674B8A2D533BDA00973137 /* tradeManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 94674B892D533BDA00973137 /* tradeManager.mm */; };
@@ -75,13 +76,13 @@
7576
944D0DC82C8C3704004DD0FC /* LICENSE.MD */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.MD; sourceTree = "<group>"; };
7677
944D0DC92C8C3704004DD0FC /* build.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = build.sh; sourceTree = "<group>"; };
7778
944D0DCA2C8C3704004DD0FC /* clean.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = clean.sh; sourceTree = "<group>"; };
78-
944D0DCB2C8C3704004DD0FC /* environment.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = environment.sh; sourceTree = "<group>"; };
7979
944D0DCC2C8C3704004DD0FC /* run.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = run.sh; sourceTree = "<group>"; };
8080
944D0DCD2C8C3704004DD0FC /* test.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = test.sh; sourceTree = "<group>"; };
8181
944D0DCF2C8C3704004DD0FC /* sonar-project.properties */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "sonar-project.properties"; sourceTree = "<group>"; };
8282
944D0DD02C8C3704004DD0FC /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
83-
944D0DD12C8C3704004DD0FC /* random-indices-sp500-variable.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "random-indices-sp500-variable.svg"; sourceTree = "<group>"; };
8483
944D0DD32C8C3704004DD0FC /* CMakeLists.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CMakeLists.txt; sourceTree = "<group>"; };
84+
9464E5EF2FA7466900D82BAD /* symbolScale.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = symbolScale.hpp; sourceTree = "<group>"; };
85+
9464E5F02FA7467200D82BAD /* symbolScale.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = symbolScale.mm; sourceTree = "<group>"; };
8586
94674B822D533B1D00973137 /* trade.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = trade.hpp; sourceTree = "<group>"; };
8687
94674B832D533B2F00973137 /* tradeManager.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = tradeManager.hpp; sourceTree = "<group>"; };
8788
94674B852D533B4000973137 /* tradeManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = tradeManager.cpp; sourceTree = "<group>"; };
@@ -1314,6 +1315,7 @@
13141315
942966D72D48E84100532862 /* models */ = {
13151316
isa = PBXGroup;
13161317
children = (
1318+
9464E5EF2FA7466900D82BAD /* symbolScale.hpp */,
13171319
94674B822D533B1D00973137 /* trade.hpp */,
13181320
942966D82D48E84A00532862 /* priceData.hpp */,
13191321
);
@@ -1324,7 +1326,6 @@
13241326
isa = PBXGroup;
13251327
children = (
13261328
94CD849E2D2D22C900041BBA /* external */,
1327-
944D0DD22C8C3704004DD0FC /* images */,
13281329
944D0DCE2C8C3704004DD0FC /* scripts */,
13291330
94DE4F772C8C3E7C00FE48FF /* include */,
13301331
9470B5A22C8C5AD0007D9CC6 /* source */,
@@ -1355,21 +1356,12 @@
13551356
94BBA4502D2EA2570010E04D /* arguments */,
13561357
944D0DC92C8C3704004DD0FC /* build.sh */,
13571358
944D0DCA2C8C3704004DD0FC /* clean.sh */,
1358-
944D0DCB2C8C3704004DD0FC /* environment.sh */,
13591359
944D0DCC2C8C3704004DD0FC /* run.sh */,
13601360
944D0DCD2C8C3704004DD0FC /* test.sh */,
13611361
);
13621362
path = scripts;
13631363
sourceTree = "<group>";
13641364
};
1365-
944D0DD22C8C3704004DD0FC /* images */ = {
1366-
isa = PBXGroup;
1367-
children = (
1368-
944D0DD12C8C3704004DD0FC /* random-indices-sp500-variable.svg */,
1369-
);
1370-
path = images;
1371-
sourceTree = "<group>";
1372-
};
13731365
94674B842D533B2F00973137 /* trading */ = {
13741366
isa = PBXGroup;
13751367
children = (
@@ -1422,6 +1414,7 @@
14221414
9470B5AD2C8C5B99007D9CC6 /* tests */ = {
14231415
isa = PBXGroup;
14241416
children = (
1417+
9464E5F02FA7467200D82BAD /* symbolScale.mm */,
14251418
943398262D57E54000287A2D /* jsonParser.mm */,
14261419
94674B892D533BDA00973137 /* tradeManager.mm */,
14271420
94364CB52D416D8000F35B55 /* db.mm */,
@@ -3648,6 +3641,7 @@
36483641
files = (
36493642
94CD8BA12D2E8CE500041BBA /* databaseConnection.cpp in Sources */,
36503643
941408AF2D59F93F000ED1F9 /* sqlManager.cpp in Sources */,
3644+
9464E5F12FA7467200D82BAD /* symbolScale.mm in Sources */,
36513645
943398242D57E53400287A2D /* jsonParser.cpp in Sources */,
36523646
94280BA42D2FC00200F1CF56 /* base64.cpp in Sources */,
36533647
94674B8D2D533E7800973137 /* trade.cpp in Sources */,
@@ -3789,12 +3783,14 @@
37893783
HEADER_SEARCH_PATHS = (
37903784
"\"$(SRCROOT)/external/libpqxx/include\"",
37913785
"\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"",
3792-
"\"$(SRCROOT)/external/",
3786+
"\"$(SRCROOT)/external/\"",
3787+
"\"$(SRCROOT)/external/boost-decimal/include\"",
37933788
);
37943789
INCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = "";
37953790
LIBRARY_SEARCH_PATHS = (
37963791
"\"$(SRCROOT)/external/libpqxx/build/src\"",
37973792
"/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql",
3793+
"\"$(SRCROOT)/external/boost-decimal/build\"",
37983794
);
37993795
MACOSX_DEPLOYMENT_TARGET = 26.0;
38003796
OTHER_LDFLAGS = "";
@@ -3812,12 +3808,14 @@
38123808
HEADER_SEARCH_PATHS = (
38133809
"\"$(SRCROOT)/external/libpqxx/include\"",
38143810
"\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"",
3815-
"\"$(SRCROOT)/external/",
3811+
"\"$(SRCROOT)/external/\"",
3812+
"\"$(SRCROOT)/external/boost-decimal/include\"",
38163813
);
38173814
INCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = "";
38183815
LIBRARY_SEARCH_PATHS = (
38193816
"\"$(SRCROOT)/external/libpqxx/build/src\"",
38203817
"/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql",
3818+
"\"$(SRCROOT)/external/boost-decimal/build\"",
38213819
);
38223820
MACOSX_DEPLOYMENT_TARGET = 26.0;
38233821
OTHER_LDFLAGS = "";
@@ -3836,11 +3834,13 @@
38363834
HEADER_SEARCH_PATHS = (
38373835
"\"$(SRCROOT)/external/libpqxx/include\"",
38383836
"\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"",
3839-
"\"$(SRCROOT)/external/",
3837+
"\"$(SRCROOT)/external/\"",
3838+
"\"$(SRCROOT)/external/boost-decimal/include\"",
38403839
);
38413840
LIBRARY_SEARCH_PATHS = (
38423841
"\"$(SRCROOT)/external/libpqxx/build/src\"",
38433842
"/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql",
3843+
"\"$(SRCROOT)/external/boost-decimal/build\"",
38443844
);
38453845
MACOSX_DEPLOYMENT_TARGET = 26.0;
38463846
MARKETING_VERSION = 1.0;
@@ -3859,11 +3859,13 @@
38593859
HEADER_SEARCH_PATHS = (
38603860
"\"$(SRCROOT)/external/libpqxx/include\"",
38613861
"\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"",
3862-
"\"$(SRCROOT)/external/",
3862+
"\"$(SRCROOT)/external/\"",
3863+
"\"$(SRCROOT)/external/boost-decimal/include\"",
38633864
);
38643865
LIBRARY_SEARCH_PATHS = (
38653866
"\"$(SRCROOT)/external/libpqxx/build/src\"",
38663867
"/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql",
3868+
"\"$(SRCROOT)/external/boost-decimal/build\"",
38673869
);
38683870
MACOSX_DEPLOYMENT_TARGET = 26.0;
38693871
MARKETING_VERSION = 1.0;
File renamed without changes.

external/boost-decimal

Submodule boost-decimal added at e995162

include/models/priceData.hpp

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@
66
#pragma once
77
#include <chrono>
88
#include <string>
9+
#include <boost/decimal.hpp>
910

11+
// decimal64_t: 16 significant base-10 digits, no binary floating-point drift on
12+
// values like 1.23456. Closest C# analogue is System.Decimal (though decimal64_t
13+
// is 64-bit IEEE 754-2008 vs C#'s 128-bit type).
1014
struct PriceData {
11-
double ask;
12-
double bid;
15+
boost::decimal::decimal64_t ask;
16+
boost::decimal::decimal64_t bid;
1317
std::chrono::system_clock::time_point timestamp;
1418
std::string symbol;
1519

16-
// Constructor for easy creation
17-
PriceData(double ask, double bid, const std::chrono::system_clock::time_point& ts, const std::string& symbol)
20+
PriceData(boost::decimal::decimal64_t ask, boost::decimal::decimal64_t bid,
21+
const std::chrono::system_clock::time_point& ts, const std::string& symbol)
1822
: ask(ask), bid(bid), timestamp(ts), symbol(symbol) {}
1923

20-
PriceData() : ask(0.0), bid(0.0), timestamp{}, symbol("") {}
24+
PriceData() : ask(0), bid(0), timestamp{}, symbol("") {}
2125
};

include/models/symbolScale.hpp

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Backtesting Engine in C++
2+
//
3+
// (c) 2026 Ryan McCaffery | https://mccaffers.com
4+
// This code is licensed under MIT license (see LICENSE.txt for details)
5+
// ---------------------------------------
6+
//
7+
// symbolScale.hpp — fast symbol -> pip-scale lookup.
8+
//
9+
// Given a symbol like "EURUSD" or "USDJPY", returns the integer scale
10+
// used to convert price differences into pips (e.g. 10000 for most FX
11+
// pairs, 100 for JPY pairs, 1 for indices/metals).
12+
//
13+
// Design goals (this is on the hot path of the backtester):
14+
// - No heap allocation, no hashing, no string parsing at runtime.
15+
// - The whole table is `constexpr`, so when the caller passes a string
16+
// literal the compiler can fold the lookup down to a single `mov`.
17+
// - Header-only on purpose: every translation unit sees the body of
18+
// `get()` and can inline it.
19+
//
20+
// C# analogy: think of this as a `static readonly Dictionary<string,int>`,
21+
// except the lookup is resolved at compile time when possible.
22+
#pragma once
23+
24+
// <array> — std::array<T, N> is a fixed-size, stack-allocated array
25+
// with an STL-style interface. Like a C# fixed-size buffer
26+
// but type-safe and bounds-checkable.
27+
// <string_view> — std::string_view is a non-owning view over an existing
28+
// string's characters: a (const char* + length) pair. It
29+
// does NOT allocate or copy. Closest C# equivalent is
30+
// ReadOnlySpan<char>. Use it for parameters that read but
31+
// don't store the string.
32+
#include <array>
33+
#include <string_view>
34+
35+
// `namespace` is C++'s scoping construct. `symbol_scale::get(...)` is
36+
// the fully qualified name from outside this namespace.
37+
namespace symbol_scale {
38+
39+
// A plain "POD" (plain old data) record. No constructors needed — we use
40+
// brace-initialization below. `string_view` is safe to store here because
41+
// the strings it points to are static string literals with program-long
42+
// lifetime.
43+
struct Entry {
44+
std::string_view symbol;
45+
int scale;
46+
};
47+
48+
// Three keywords doing three different jobs on this one declaration:
49+
// - `inline` : tells the linker "if you see this defined in multiple
50+
// .cpp files (because it's in a header), that's fine —
51+
// they're all the same thing." Required for variables
52+
// defined in headers since C++17.
53+
// - `constexpr` : the value is computable at compile time. The whole
54+
// table lives in read-only program memory and the
55+
// compiler can use it during constant evaluation.
56+
// - `std::array<Entry, 29>` : 29 Entry objects laid out contiguously
57+
// in memory — great for CPU cache locality during the
58+
// binary search below.
59+
//
60+
// IMPORTANT: this table MUST stay sorted ascending by symbol. The
61+
// `static_assert` below enforces that at compile time.
62+
inline constexpr std::array<Entry, 29> kTable{{
63+
{"AUDNZD", 10000},
64+
{"AUDUSD", 10000},
65+
{"AUSIDXAUD", 1},
66+
{"BRENTCMDUSD", 1},
67+
{"COPPERCMDUSD", 1},
68+
{"DEUIDXEUR", 1},
69+
{"EURAUD", 10000},
70+
{"EURCHF", 10000},
71+
{"EURGBP", 10000},
72+
{"EURJPY", 100},
73+
{"EURNOK", 10000},
74+
{"EURUSD", 10000},
75+
{"FRAIDXEUR", 1},
76+
{"GBPJPY", 100},
77+
{"GBPUSD", 10000},
78+
{"GBRIDXGBP", 1},
79+
{"HKGIDXHKD", 1},
80+
{"JPNIDXJPY", 1},
81+
{"LIGHTCMDUSD", 1},
82+
{"NZDUSD", 10000},
83+
{"USA30IDXUSD", 1},
84+
{"USA500IDXUSD", 1},
85+
{"USATECHIDXUSD", 1},
86+
{"USDCAD", 10000},
87+
{"USDCHF", 10000},
88+
{"USDJPY", 100},
89+
{"USDSEK", 10000},
90+
{"XAGUSD", 1},
91+
{"XAUUSD", 1},
92+
}};
93+
94+
// Compile-time invariant check. The pattern is an IIFE — Immediately
95+
// Invoked Function Expression. We declare a lambda `[]{ ... }` and
96+
// then invoke it with `()`, all in one expression. This lets us run
97+
// real logic (a loop) inside `static_assert`, which only accepts a
98+
// boolean expression.
99+
//
100+
// If a future maintainer adds an entry in the wrong place, compilation
101+
// fails with the message below — far better than a silently broken
102+
// binary search.
103+
static_assert([] {
104+
for (std::size_t i = 1; i < kTable.size(); ++i) {
105+
if (!(kTable[i - 1].symbol < kTable[i].symbol)) return false;
106+
}
107+
return true;
108+
}(), "symbol_scale::kTable must be sorted ascending by symbol — binary search depends on it");
109+
110+
// Sentinel returned for unknown symbols. Multiplying a real price by 0
111+
// will produce an obviously-wrong result, which fails loudly rather
112+
// than corrupting silently.
113+
inline constexpr int kUnknown = 0;
114+
115+
// `[[nodiscard]]` : compiler warning if the caller ignores the returned
116+
// value (this function has no other purpose, so
117+
// ignoring the result is almost always a bug).
118+
// `constexpr` : callable at compile time. When the symbol is a
119+
// literal known to the compiler, the entire binary
120+
// search is folded away and the result becomes a
121+
// constant in the generated assembly.
122+
// `noexcept` : promises this function will not throw. Lets the
123+
// compiler skip exception-handling bookkeeping at
124+
// call sites.
125+
// Parameter is `std::string_view` (by value — it's just a pointer +
126+
// length, cheap to copy) so callers can pass `std::string`, string
127+
// literals, or `const char*` without converting or allocating.
128+
[[nodiscard]] constexpr int get(std::string_view symbol) noexcept {
129+
// Standard binary search over the sorted table.
130+
// `lo` and `hi` are the half-open range [lo, hi) of indices still
131+
// in play. Each iteration halves the range, so for 29 entries we
132+
// do at most 5 iterations.
133+
std::size_t lo = 0;
134+
std::size_t hi = kTable.size();
135+
while (lo < hi) {
136+
// `lo + ((hi - lo) >> 1)` is the overflow-safe way to compute
137+
// the midpoint. `(lo + hi) / 2` would be wrong if the indices
138+
// were near std::size_t's max; not a real risk here, but it's
139+
// the canonical idiom worth learning. `>> 1` is just `/ 2`.
140+
const std::size_t mid = lo + ((hi - lo) >> 1);
141+
const auto& entry = kTable[mid];
142+
143+
// Three-way compare on the symbol. `string_view::operator<`
144+
// does a lexicographic comparison (essentially memcmp).
145+
if (entry.symbol < symbol) {
146+
lo = mid + 1; // target is in the upper half
147+
} else if (symbol < entry.symbol) {
148+
hi = mid; // target is in the lower half
149+
} else {
150+
return entry.scale; // exact match
151+
}
152+
}
153+
return kUnknown;
154+
}
155+
156+
} // namespace symbol_scale

0 commit comments

Comments
 (0)