algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
example-strategy-dryrun.cpp
1// A strategy run end to end with no broker, no credentials, and no network.
2//
3// Bars are built in code, so the output is identical every time and the binary contains no code
4// path that could place an order -- `<algotradelib/runner>` links no TLS, which a test asserts.
5// That is what makes this the right place to see what a strategy decides before letting it near
6// a market.
7//
8// Run it as:
9//
10// example-strategy-dryrun
11
12#include <algotradelib/runner>
13#include <algotradelib/vanilla>
14
15#include <chrono>
16#include <cstddef>
17#include <iomanip>
18#include <iostream>
19#include <map>
20#include <utility>
21#include <string>
22#include <vector>
23
24namespace {
25
28namespace vanilla = algotradelib::strategy::vanilla;
29
43
44InstrumentId samsung() { return InstrumentId{Country::KR, Venue::COMBINED, "005930"}; }
45
47Instrument samsung_reference() {
48 Instrument reference;
49 reference.id_ = samsung();
50 reference.name_ = "Samsung Electronics";
51 reference.type_ = SecurityType::STOCK;
52 reference.currency_ = Currency{"KRW"};
53 reference.lot_size_ = Decimal64::parse("1");
54 reference.price_rule_ = PriceRule::krx_equity();
55 return reference;
56}
57
64const std::vector<const char*>& closes() {
65 static const std::vector<const char*> series = {
66 "60000", "60500", "61200", "61000", "62000", "63000", "64500", "65000", "66200", "67000",
67 "68500", "69000", "70200", "71000", "72500", "73000", "72800", "72000", "71000", "69500",
68 "68000", "66500", "65000", "63500", "62000", "61000", "60000", "59000", "58500", "58000",
69 };
70 return series;
71}
72
74strategy::BarEvent bar(std::size_t index) {
75 Bar value;
76 value.timestamp_ = std::chrono::system_clock::time_point{} +
77 std::chrono::hours(24 * (20000 + static_cast<int>(index)));
78 value.open_ = Decimal64::parse(closes()[index]);
79 value.high_ = Decimal64::parse(closes()[index]);
80 value.low_ = Decimal64::parse(closes()[index]);
81 value.close_ = Decimal64::parse(closes()[index]);
82 value.volume_ = Decimal64::parse("1200000");
83 return strategy::make_bar_event(samsung(), BarInterval::ONE_DAY, BarStamp::CLOSE, value);
84}
85
87template <typename Report>
88void print(std::size_t day, const Report& report) {
89 for (const auto& order : report.would_place_) {
90 std::cout << " day " << std::setw(2) << day << " "
91 << (order.side_ == OrderSide::BUY ? "BUY " : "SELL") << ' ' << std::setw(4)
92 << order.quantity_.str() << " shares";
93 if (order.price_) {
94 std::cout << " at " << order.price_->str();
95 } else {
96 std::cout << " at market";
97 }
98 std::cout << " [" << order.tag_ << "]\n";
99 }
100 for (const auto& rejection : report.rejected_) {
101 std::cout << " day " << std::setw(2) << day << " rejected: "
102 << strategy::reject_reason_name(rejection.reason_) << '\n';
103 }
104}
105
113using RefusalTally = std::map<std::string, std::pair<std::size_t, std::size_t>>;
114
115template <typename Report>
116void tally(std::size_t day, const Report& report, RefusalTally& counts) {
117 for (const auto& refusal : report.refused_) {
118 const std::string reason(strategy::guard_reason_name(refusal.reason_));
119 auto found = counts.find(reason);
120 if (found == counts.end()) {
121 counts.emplace(reason, std::pair<std::size_t, std::size_t>{day, 1});
122 } else {
123 ++found->second.second;
124 }
125 }
126}
127
135template <typename Strategy>
136void run(const char* title, Strategy instance, const strategy::RiskLimits& limits = {}) {
137 std::cout << title << '\n';
138
139 runner::DryRunner<Strategy> dry(std::move(instance), limits);
140 dry.context().set_instrument(samsung_reference());
141 dry.context().set_phase(strategy::MarketPhase::OPEN);
142 dry.set_portfolio_value(Decimal64::parse("100000000"));
143
144 Decimal64 held = Decimal64::parse("0");
145 RefusalTally refusals;
146 for (std::size_t day = 0; day < closes().size(); ++day) {
147 dry.context().set_last_price(samsung(), Decimal64::parse(closes()[day]));
148
150 HoldingPosition position;
151 position.instrument_ = samsung();
152 position.quantity_ = held;
153 // What the account may sell, which a broker's holdings state and the guard checks a sell against by
154 // default: here the whole holding. Without it a dry run refuses every sell (`quantity_not_sellable`).
155 position.sellable_quantity_ = held;
156 portfolio.positions_.push_back(position);
160 false,
161 };
162 dry.context().set_portfolio(portfolio);
163
164 const auto report = dry.feed(bar(day));
165 print(day, report);
166 tally(day, report, refusals);
167
168 for (const auto& order : report.would_place_) {
169 const auto signed_quantity = order.side_ == OrderSide::BUY ?
170 algotradelib::decimal_add(held, order.quantity_) :
171 algotradelib::decimal_subtract(held, order.quantity_);
172 if (signed_quantity) {
173 held = *signed_quantity;
174 }
175 }
176 }
177 for (const auto& entry : refusals) {
178 std::cout << " day " << std::setw(2) << entry.second.first << " refused: " << entry.first
179 << " (and on " << entry.second.second - 1 << " later bars)\n";
180 }
181 std::cout << " ending position: " << held.str() << " shares\n\n";
182}
183
184} // namespace
185
186int main() {
187 std::cout << "005930, " << closes().size()
188 << " daily bars built in code -- no credentials, no network\n\n";
189
190 vanilla::BuyAndHold holding;
191 holding.params_.instrument_ = samsung();
192 holding.params_.quantity_ = Decimal64::parse("100");
193 run("BuyAndHold: one target, then nothing, and it remembers nothing to do it", holding);
194
195#if defined(ATL_HAS_EIGEN)
196 vanilla::MovingAverageCrossover::Params crossing;
197 crossing.instrument_ = samsung();
198 crossing.fast_ = 3;
199 crossing.slow_ = 8;
200 crossing.quantity_ = Decimal64::parse("100");
201 run(
202 "SmaCrossover(3, 8): long while the fast average leads, flat while it lags",
203 vanilla::SmaCrossover{crossing}
204 );
205
206 vanilla::DonchianBreakout breaking;
207 breaking.params_.instrument_ = samsung();
208 breaking.params_.lookback_ = 5;
209 breaking.params_.quantity_ = Decimal64::parse("100");
210 run("DonchianBreakout(5): the channel excludes today, so a break is measurable", breaking);
211
212 vanilla::RsiThreshold indexed;
213 indexed.params_.instrument_ = samsung();
214 indexed.params_.period_ = 5;
215 indexed.params_.oversold_ = 35.0;
216 indexed.params_.overbought_ = 65.0;
217 indexed.params_.quantity_ = Decimal64::parse("100");
218 run("RsiThreshold(5, 35/65): Wilder's smoothing and Wilder's seed", indexed);
219#else
220 std::cout
221 << "The indicator strategies need a linear algebra backend and this build has none.\n"
222 << "Configure with -DATL_USE_EIGEN=ON to see SmaCrossover, DonchianBreakout and RsiThreshold.\n"
223 << "BuyAndHold and TimedLiquidation read no series and run either way.\n\n";
224#endif
225
226 // A guard refusing an order is part of the pipeline, not an error, so it belongs in the output.
228 tight.max_order_notional_ = Decimal64::parse("1000000");
229 vanilla::BuyAndHold oversized;
230 oversized.params_.instrument_ = samsung();
231 oversized.params_.quantity_ = Decimal64::parse("100");
232 run(
233 "BuyAndHold with a 1,000,000 KRW per-order ceiling: refused, never resized",
234 oversized,
235 tight
236 );
237
238 return 0;
239}
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
BrokerError refusal()
Returns the error every account-side member of DataOnlyTraits answers with.
Runs a strategy: the runner, the polling loop, the order journal, and stopping.
Definition clock.hpp:7
Runner< Strategy, CapsType, DryTag, RecordingExecutor< DryTag > > DryRunner
A runner that sends nothing: handlers fire and instructions are recorded by a RecordingExecutor.
Definition runner.hpp:4524
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
The strategies this library ships (buy and hold, crossovers, mean reversion, sizing,...
What a strategy sees and emits: events, intents, the portfolio, reconciliation and risk limits.
Definition concept.hpp:16
BarEvent make_bar_event(InstrumentId instrument, BarInterval interval, BarStamp stamp, Bar bar)
Makes a bar event, computing its period_end_ with bar_period_end.
Definition event.hpp:273
@ OPEN
Inside the session.
Definition event.hpp:38
constexpr std::string_view reject_reason_name(RejectReason reason) noexcept
Returns a lowercase name for a reason, such as "no_reference_data", for logs.
constexpr std::string_view guard_reason_name(GuardReason reason) noexcept
Returns a lowercase name for a refusal reason, such as "order_rate_exceeded", for logs.
Definition risk.hpp:216
The library: broker-neutral types (core), and the modules below.
BarStamp
Which edge of its period a bar's timestamp marks.
SecurityType
What kind of security an instrument is.
std::optional< Decimal64 > decimal_add(const Decimal64 &lhs, const Decimal64 &rhs)
Adds two decimals exactly.
Definition types.hpp:509
Country
A country (market) the library supports.
Definition types.hpp:30
std::optional< Decimal64 > decimal_subtract(const Decimal64 &lhs, const Decimal64 &rhs)
Subtracts two decimals exactly.
Definition types.hpp:530
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
One bar: the prices and volume of one period, as exact decimals.
One position row in a holdings snapshot.
Definition types.hpp:1017
Identifies an instrument: its country, venue and symbol.
Definition types.hpp:78
Reference data for one instrument.
The price grid (tick sizes) an instrument's order prices must sit on.
What a sellable quantity covers, and how RiskGuard must treat resting sells against it.
Definition types.hpp:918
@ WHOLE_HOLDING
Every share held of the instrument, whatever venue or lot.
Definition types.hpp:924
@ DEDUCT
Subtract every resting sell from the figure.
Definition types.hpp:942
One bar, delivered to on_bar.
Definition event.hpp:202
What the account holds and which orders are still working, as a strategy sees it.
Definition portfolio.hpp:50
SellableBasis sellable_basis_
What the sellable_quantity_ figures on positions_ cover.
std::vector< HoldingPosition > positions_
Every position row the broker reported.
Definition portfolio.hpp:56
The limits the operator sets for RiskGuard.
Definition risk.hpp:268
std::optional< Decimal64 > max_order_notional_
The largest value one order may have, in its own currency.
Definition risk.hpp:270