algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
example-analytics-offline.cpp
1// The analytics layer end to end, with no network and no credentials.
2//
3// Bars are built in code rather than fetched, so this runs anywhere and its output is
4// the same every time -- which is what makes it useful as a worked example of the
5// conventions rather than as a demonstration that a broker replied.
6//
7// It walks the whole path a strategy takes:
8//
9// BarSeries -> SeriesFrame -> indicators -> a metric -> back to a Decimal64 price
10//
11// and prints the warm-up region explicitly, because "the first N rows are blank and that
12// is deliberate" is the single most important thing to understand about these functions.
13//
14// Run it as:
15//
16// example-analytics-offline
17
18#include <algotradelib/analytics>
19#include <algotradelib/core>
20
21#include <chrono>
22#include <cstddef>
23#include <iomanip>
24#include <iostream>
25#include <sstream>
26#include <string>
27#include <vector>
28
29namespace {
30
41
42namespace analytics = algotradelib::analytics;
43namespace math = algotradelib::math;
44
45using analytics::Scalar;
46using analytics::observed;
47
49std::string cell(Scalar value) {
50 if (!observed(value)) {
51 return " --";
52 }
53 std::ostringstream text;
54 text << std::fixed << std::setprecision(2) << std::setw(7) << value;
55 return text.str();
56}
57
65BarSeries fixture() {
66 // Written as the strings a broker actually sends, so the conversion boundary is
67 // exercised on real decimal text rather than on doubles dressed up as decimals.
68 static constexpr const char* closes[] = {
69 "70000", "70800", "70200", "71500", "72300", "71900", "73100", "74000", "73400",
70 "72600", "71800", "72500", "73900", "75200", "74800", "76100", "77000", "76200",
71 "75400", "74100",
72 };
73 static constexpr const char* highs[] = {
74 "70500", "71300", "70700", "72000", "72800", "72400", "73600", "74500", "73900",
75 "73100", "72300", "73000", "74400", "75700", "75300", "76600", "77500", "76700",
76 "75900", "74600",
77 };
78 static constexpr const char* lows[] = {
79 "69400", "70200", "69600", "70900", "71700", "71300", "72500", "73400", "72800",
80 "72000", "71200", "71900", "73300", "74600", "74200", "75500", "76400", "75600",
81 "74800", "73500",
82 };
83
84 BarSeries series;
85 series.instrument_ = InstrumentId{Country::KR, Venue::COMBINED, "005930"};
86 series.interval_ = BarInterval::ONE_DAY;
87 series.currency_ = Currency{"KRW"};
88 series.received_at_ = std::chrono::system_clock::now();
89
90 const auto start = std::chrono::system_clock::time_point{} + std::chrono::hours(24 * 20000);
91 for (std::size_t day = 0; day < std::size(closes); ++day) {
92 Bar bar;
93 bar.timestamp_ = start + std::chrono::hours(24 * static_cast<int>(day));
94 // The fourth day has no open, which is what a session with no trade at the auction
95 // looks like. It stays unobserved through the frame and every indicator masks it.
96 if (day != 3) {
97 bar.open_ = Decimal64::parse(closes[day]);
98 }
99 bar.high_ = Decimal64::parse(highs[day]);
100 bar.low_ = Decimal64::parse(lows[day]);
101 bar.close_ = Decimal64::parse(closes[day]);
102 bar.volume_ = Decimal64::parse("1200000");
103 series.bars_.push_back(bar);
104 }
105 return series;
106}
107
108} // namespace
109
110int main() {
111 const BarSeries bars = fixture();
112
113 // The one place exact decimals become scalars. Everything below computes in `Scalar`
114 // and nothing below it sees a `Decimal64` again until the very last step.
115 const analytics::SeriesFrame frame = analytics::from_bars(bars);
116
117 // Views into the frame's own storage, not copies: a column of a column-major frame is
118 // contiguous, which is what `SeriesView` is for.
119 const math::SeriesView close = frame.close();
120 const math::SeriesView open = frame.open();
121 const math::Vector average = analytics::sma(close, 5);
122 const math::Vector smoothed = analytics::ema(close, analytics::alpha_from_span(5));
123 const math::Vector strength = analytics::rsi(close, 5);
124 const math::Vector score = analytics::zscore(close, 5);
125 const auto bands = analytics::bollinger(close, 5, 2.0);
126 const math::Vector range = analytics::atr(frame.high(), frame.low(), close, 5);
127 const math::Vector period_returns = analytics::returns(close);
128 const math::Vector growth = analytics::cumulative_returns(period_returns);
129
130 std::cout << bars.instrument_.symbol_ << ", " << bars.bars_.size()
131 << " daily bars, no network\n\n";
132 std::cout << "row close open sma5 ema5 rsi5 zscore"
133 << " upper lower atr5 cumret\n";
134 for (math::Index row = 0; row < close.size(); ++row) {
135 std::cout << std::setw(3) << row << " " << cell(close[row]) << " "
136 << cell(open[row]) << " " << cell(average[row]) << " "
137 << cell(smoothed[row]) << " " << cell(strength[row]) << " "
138 << cell(score[row]) << " " << cell(bands.upper_[row]) << " "
139 << cell(bands.lower_[row]) << " " << cell(range[row]) << " "
140 << cell(growth[row] * 100.0) << "\n";
141 }
142
143 // Row 3 has no open. Notice it is a dash above rather than a zero, and that the
144 // indicators over the close column are unaffected by it.
145 std::cout << "\nThe dashes are the point. A window reports nothing until it holds "
146 << "enough\nobservations, and row 3 has no open at all.\n";
147
148 // A window as long as the series is the whole series. Row 0 has no return, so one fewer
149 // observation is demanded than there are rows.
150 const auto rows = static_cast<std::size_t>(close.size());
151 const auto last = close.size() - 1;
152 std::cout << "\nOver the whole series:\n"
153 << " mean daily return " << std::fixed << std::setprecision(4)
154 << analytics::rolling_mean(period_returns, rows, rows - 1)[last] * 100.0 << " %\n"
155 << " daily volatility "
156 << analytics::rolling_stdev(period_returns, rows, rows - 1)[last] * 100.0 << " %\n"
157 << " per-period Sharpe " << analytics::sharpe(period_returns) << "\n"
158 << " annualized Sharpe "
159 << analytics::annualize_ratio(analytics::sharpe(period_returns), 245.0) << "\n"
160 << " worst drawdown "
161 << analytics::max_drawdown(close).magnitude_ * 100.0 << " %\n";
162
163 // What a live loop keeps for an exponential average: one number, updated once per bar. It
164 // agrees with the vectorized `ema` exactly, which is the property that keeps a backtest and a
165 // live loop computing the same thing.
166 const math::Scalar alpha = analytics::alpha_from_span(5);
167 math::Scalar level = math::missing();
168 for (math::Index row = 0; row < close.size(); ++row) {
169 level = analytics::ema_step(level, close[row], alpha);
170 }
171 std::cout << " ema_step == ema " << (level == smoothed[last] ? "yes" : "no") << "\n";
172
173 // The one sanctioned route back to something a broker will accept. The upper band is a
174 // computed scalar; an order needs an exact decimal on the instrument's own tick grid.
175 const auto rule =
176 algotradelib::price_rule_for(bars.instrument_.country_, SecurityType::STOCK);
177 const auto limit = analytics::round_scalar_to_tick(
178 bands.upper_[last], rule, OrderSide::SELL
179 );
180 std::cout << "\nA sell limit at the upper band, rounded onto the KRX tick grid: "
181 << (limit ? limit->str() : std::string("refused")) << "\n";
182
183 return 0;
184}
A three-letter currency code, such as KRW or USD (ISO 4217 style).
Definition types.hpp:234
An exact signed decimal: an int64 coefficient times 10^-scale.
Definition types.hpp:297
Returns, rolling statistics, indicators and performance metrics over numeric series.
The numeric vocabulary: Scalar, missing(), and the matrix and vector types.
Definition eigen.hpp:10
bool observed(Scalar value)
Tests whether a value is an observation.
Definition scalar.hpp:36
void start(S &strategy, Ctx &context)
Calls strategy.on_start(context) if the strategy declares on_start; otherwise does nothing.
Definition concept.hpp:149
void bar(S &strategy, Ctx &context, const BarEvent &event)
Calls strategy.on_bar(context, event) if the strategy declares on_bar; otherwise does nothing.
Definition concept.hpp:191
void bars(S &strategy, Ctx &context, const BarSlice &slice)
Calls strategy.on_bars(context, slice) if the strategy declares on_bars; otherwise does nothing.
Definition concept.hpp:218
PriceRule price_rule_for(Country country, SecurityType type)
Returns the library's price grid for a kind of security in a country.
SecurityType
What kind of security an instrument is.
Country
A country (market) the library supports.
Definition types.hpp:30
Venue
A trading venue: an exchange, or the combined view across exchanges.
Definition types.hpp:43
BarInterval
The period one bar covers.
OrderSide
The side of an order: its economic direction, buy or sell.
Definition types.hpp:1683
The bars of one instrument and interval, oldest first.
One bar: the prices and volume of one period, as exact decimals.
Identifies an instrument: its country, venue and symbol.
Definition types.hpp:78